《并查集》

1. 概念

        并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。 在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合进行合并。在此过程中要反复用到查询某一个元素归属于具体集合的运算。

2.原理

        下面通过一个具体的场景描述一下并查集的原理

        总公司组织下属子公司员工进行团建,其中A公司4人,B公司3人,C公司3人,10个人分别来自不同的组,起先互不相识,每个员工都是一个独立的小队,现给这些员工进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};每个子公司的员工自发组织成小队一起到达目的地,于是:A公司小队s1={0,3,5,7},B公司小队s2={1,2,8},C公司小队s3={4,6,9}就相互认识了,10个人形成了三个小团队。

        按照概念将三组小队用树表示,以编号最小的员工作为根节点

        使用数组进行后续存储,将元素的初始化为对应的下标,初始化的数据如下图所示:

2.1合并节点

        现有分组:

        A公司小队s1={0,3,5,7},B公司小队s2={1,2,8},C公司小队s3={4,6,9},

        合并策略

        将子节点的下标调整为最上层的根节点下标,根节点的下标保持不变,此时的数组会变成下图所示

        此时数组数组存在以下特点:

1. 数组的下标对应集合中元素的编号

2. 对于每个位置来说,如果它存的值是其下标,那它就是根,如果不是其下标,那它存的就是它的父结点的下标。

        情景继续:

        团建了一段时间后,A公司3号员工与B公司2号员工慢慢熟悉起来了,两个小队的其他员工也经过相互介绍,最后融合成了一个小队

        那上面这种情况对应到数组中该怎么做呢?

        是否可以和上述操作一样,直接将3号元素的坐标修改2?

        目前看这样显然不行,这样等于把3从原先的小队中脱离,然后加到2这棵树里面。 但是目的是要把这两个小队进行合并啊。

        那正确的应该这样搞: 要找着两棵树的根节点,把它们的两个根合并了,这两棵树不就合并了嘛!

2.2找根节点

        如何找根节点:

        那很简单,看这个位置存的值是不是对应的下标,是对应下标的话就是根了;不是对应下标的话,存的就是其父节点的下标,那就顺着父结点往上找,直到值为对应下标就是最上面的根了

        

        3找到根是0,2找到根是1然后让这两个根合并就行了,如图所示

3.并查集

        3.1简单代码

public class UnionFind {
    int[] cache ;
    public UnionFind(int n){
        cache = new int[n];
        for(int i = 0 ; i < n ; i++){
            cache[i] = i;
        }
    }
    public int find(int x){
        int root = x;
        while (cache[root] != root){
            root = cache[root];
        }
        return root;
    }
    public void union(int x,int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            if (rootX > rootY){
                cache[rootX] = rootY;
            }else {
                cache[rootY] = rootX;
            }
        }
    }
    public static void main(String[] args) {
        // 构建并查集
        int[][] m = {{0,3},{0,5},{0,7},{1,2},{1,8},{4,6},{4,9}};
        UnionFind uf = new UnionFind(10);
        for(int[] edge : m){
            uf.union(edge[0],edge[1]);
        }
        //结果打印
        //0	1	1	0	4	0	4	0	1	4
        for (int  i = 0 ; i < 10 ; i++){
            System.out.print(uf.find(i)+"\t");
        }
        	
        System.out.println();
        //3,2合并
        uf.union(3,2);
        //结果打印
        //0	0	0	0	4	0	4	0	0	4	
        for (int  i = 0 ; i < 10 ; i++){
            System.out.print(uf.find(i)+"\t");
        }
    }
}

3.2 常用场景

1. 查找元素属于哪个集合(找根) 沿着数组表示的树形关系往上一直找到根(即:树中中元素为其下标的位置)

2. 查看两个元素是否属于同一个集合 沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在

3. 将两个集合归并成一个集合

4. 集合的个数 遍历数组,数组中元素为其下标的个数即为集合的个数

3.3 例题

. - 力扣(LeetCode)

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例 1:

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

        整体思路:

        1.初始化长度为n的数组,每个元素的值为其下标

        2.依次遍历每个城市与其他城市连接情况

        3.将连接的城市构建一个并查集

        4.查找数组中元素为其下标的个数

        代码:

class Solution {
    int[] cache ;
    public int find(int x){
        int root = x;
        while (cache[root] != root){
            root = cache[root];
        }
        return root;
    }
    public void union(int x,int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            if (rootX > rootY){
                cache[rootX] = rootY;
            }else {
                cache[rootY] = rootX;
            }
        }
    }
    public int findCircleNum(int[][] isConnected) {
        int n =  isConnected.length;
        cache = new int[n];
        for (int i = 0 ; i < n ; i++){
            cache[i] = i;
        }
        for(int i = 0 ; i < n ; i++){
            for(int j = i + 1 ; j < n ; j++){
                if (isConnected[i][j] == 1){
                    union(i,j);
                }
            }
        }
        int res = 0;
        for(int i = 0 ; i < n ; i++){
            if(cache[i] == i){
                res++;
            }
        }
        return res;
    }
}

3.4压缩优化

        数据量特别大的时候,可能有些路径会比较长,导致效率变慢,这时候可以考虑进行一下压缩。

        

        那压缩的话就是查找谁就去压缩哪条路径。 比如Find3的话,那里面判断一下,3的父亲直接就是0,就一层,那就不需要压缩。

        再比如,查找9那最后发现它返回的root是0,但是9直接的上一层的父亲并不是0, 那说明它们之间有间隔层,那就可以对这条路径压缩一下。 可以直接把9变成0的孩子,那后续再查找9的话就快了。 然后也可以直接把9的上一层父亲,6直接变成0的孩子,依次上传,直到0-9这条路径上的元素的父节点都变成0

需要在find方法再加一个压缩路径的代码其实就是先找到根结点,然后把这条查找路径上所有的结点都直接链接到根结点上。

        代码改造如下:

public int find(int x){
        int root = x;
        while (cache[root] != root){
            root = cache[root];
        }
        while (x != root){
            //记录父节点
            int parent = cache[x];
            //父节点存放的下标等于根节点
            cache[x] = root;
            //调整当前值
            x = parent;
        }
        return root;
    }

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值