【数据结构】并查集(leetcode-547.朋友圈)

博客围绕求解学生朋友圈总数展开。给定表示学生朋友关系的二维矩阵,通过将问题简化到一维数组求解。创建数组并初始化,遍历二维数组调整一维数组,利用并查集的特性,最终以数组中元素为负的个数确定朋友圈数量,还提及合并时的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/friend-circles

案例分析

假设存在以下10名同学,朋友圈q1={0,6,7,8},朋友圈q2={1,4,9},朋友圈q3={2,3,5},用下面的森林来表示他们之间的关系
在这里插入图片描述
当然以上的根节点不做要求,只要是在一个朋友圈中的任意一个都可以作为根,这里为了方便实现,将号码小的作为根
而如何来实现在给定的二维数组里面找出朋友圈的数量?
在这里插入图片描述
为了简化这个问题,我们不妨在一维数据里面进行求解:
第一步、创建大小为学生数的数组,并初始化为-1
在这里插入图片描述
由于朋友圈的关系是一个无向图,也就意味着我跟你是朋友,同样你跟我也是朋友,然后遍历上面的二维数组,只要对应的数字为1表示彼此就是朋友,然后再下面的一位数组里面进行调整:0和6是朋友,将一位数组中下标为6的数字加等到为0的下标上,然后置该元素为下标0
在这里插入图片描述
在遍历二维数组的过程中跟定会有半数的元素需要重复考虑,在代码实现的时候只需要进行条件判断是否已经在一个朋友圈中了就可以避免,经过调整,最后的数组如下
在这里插入图片描述
可以发现,调整过后一位数组中元素为负的个数即是朋友圈的数量
而这个一维数组就是所谓的并查集

将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集 合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集 合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)

它一般能解决:

  • 查找元素属于哪个集合 沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)
  • 查看两个元素是否属于同一个集合 沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在
  • 将两个集合归并成一个集合 将两个集合中的元素合并 将一个集合名称改成另一个集合的名称
  • 集合的个数 遍历数组,数组中元素为负数的个数即为集合的个数。

代码实现

并查集

在合并的时候需要注意,合并两个朋友圈内的人建立联系,不过两个人不是根节点,要分别找到根节点,再进行合并

class UnionFindSet {
    int[] arr;
    int size;

    public UnionFindSet(int size) {
        this.arr = new int[size];
        for (int i = 0; i<size; i++){
            arr[i] = -1;
        }
        this.size = size;
    }
    //寻找父节点,没有就返回自己
    public int find(int x){
        while (arr[x] >= 0){
            x = arr[x];
        }
        return x;
    }
    public boolean isInSameSet(int a, int b){
        return find(a) == find(b);
    }
    //合并的时候先找根节点
    public void union(int a, int b){
        int rootA = find(a);
        int rootB = find(b);
        arr[rootA] += arr[rootB];
        arr[rootB] = rootA;
    }
    public int getSize(){
        int count = 0;
        for (int i : arr){
            if (i < 0){
                count++;
            }
        }
        return count;
    }
}

如果上面的3和8成为了朋友
则森林变成如下状态
在这里插入图片描述
一维数组变成如下状态
在这里插入图片描述

问题解决

在合并的时候进行判断,如果两个同学已经在同一个朋友圈中,则不再进行合并

public class Solution {
    public static int findCircleNum(int[][] M) {
        int n = M.length;
        UnionFindSet ufs = new UnionFindSet(n);
        for (int i = 0; i<n; i++){
            for (int j = 0; j<n; j++){
                if (M[i][j] == 1 && !ufs.isInSameSet(i, j)){
                    ufs.union(i, j);
                }
            }
        }
        return ufs.getSize();
    }
}
<think>嗯,用户想了解并查集的概念和用法。首先,我得回忆一下并查集的基本定义。并查集,Union-Find,是一种处理不相交集合的数据结构,主要用于合并集合和查询元素所属集合。核心操作包括find和union,可能还有路径压缩和按秩合并的优化。 接下来要解释清楚并查集的结构。通常用数组或哈希表来存储每个元素的父节点,初始时每个元素都是自己的根。find操作是找到根节点,而union则是合并两个集合。需要举例说明,比如处理图的连通性问,或者朋友圈。 然后要讲使用方法,比如初始化、查找、合并的步骤。可能需要用代码示例,比如Java或Python的实现。用户给的示例中有提到路径压缩和按秩合并,所以得详细说明这些优化如何提高效率。 还要考虑用户可能的应用场景,比如Kruskal算法中的最小生成树,或者社交网络中的朋友关系。这部分可以结合引用中的内容,比如动态树或图的存储优化,不过并查集本身的应用更基础。 用户提供的引用1提到了动态树和图的存储优化,引用2是关于带权并查集的,引用3是代码地址。我需要确保回答中的引用正确,比如在路径压缩部分加上引用2,或者在应用场景提到动态树时引用1。 另外,用户可能想知道如何实际编码并查集,所以给出一个Java或Python的示例会好。比如初始化父数组,find函数,union函数,然后以LeetCode目为例,比如547省份数量,展示如何使用并查集解决问。 最后,生成相关问时要覆盖概念、优化、应用和目,如比较其他数据结构,优化方法,实际应用,以及常见目。</think>### 并查集概念与用法详解 #### 一、基本概念 **并查集(Union-Find)** 是一种用于管理 **不相交集合** 的数据结构,支持以下两种核心操作: 1. **查询(Find)**:判断两个元素是否属于同一集合(通过根节点是否相同实现)。 2. **合并(Union)**:将两个不相交的集合合并为一个。 其底层通常通过 **数组或哈希表** 实现,每个元素指向其父节点,形成树结构。初始状态下,每个元素独立为一个集合,父节点指向自己。 #### 二、核心操作与优化 ##### 1. 基础实现 - **初始化**:每个元素的父节点指向自身,例如:$p[x] = x$。 - **查询操作(Find)**:递归或迭代查找元素的根节点。 ```python def find(x): if p[x] != x: return find(p[x]) return x ``` - **合并操作(Union)**:将两个集合的根节点连接。 ```python def union(x, y): root_x = find(x) root_y = find(y) if root_x != root_y: p[root_y] = root_x # 将y的根节点指向x的根节点 ``` ##### 2. 优化方法 - **路径压缩**:在查询时缩短树的高度,使后续操作更快[^2]。 ```python def find(x): if p[x] != x: p[x] = find(p[x]) # 直接指向根节点 return p[x] ``` - **按秩合并**:根据树的高度或大小决定合并方向,避免退化为链表。 #### 三、应用场景 1. **图的连通性判断**:例如判断无向图中两个节点是否连通。 2. **Kruskal算法**:动态维护最小生成树的边集合[^1]。 3. **社交网络**:快速判断用户是否属于同一朋友圈。 4. **动态等价类划分**:如变量别名管理。 #### 四、示例代码(LeetCode 547. 省份数量) ```python class UnionFind: def __init__(self, size): self.parent = list(range(size)) self.count = size # 初始集合数量 def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x != root_y: self.parent[root_y] = root_x self.count -= 1 def findCircleNum(isConnected): n = len(isConnected) uf = UnionFind(n) for i in range(n): for j in range(n): if isConnected[i][j] == 1: uf.union(i, j) return uf.count ``` 此代码通过并查集统计无向图中连通分量的数量,时间复杂度接近 $O(n \alpha(n))$($\alpha$为阿克曼函数的反函数)。 #### 五、性能对比 | 操作 | 无优化时间复杂度 | 带优化的时间复杂度 | |---------------|------------------|--------------------| | Find | $O(\log n)$ | $O(\alpha(n))$ | | Union | $O(\log n)$ | $O(\alpha(n))$ |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值