Java集合扩展系列 | 并查集

Java 集合扩展系列(一) | 图框架
Java集合扩展系列(二) |索引堆
Java集合扩展系列(三) |并查集

1、什么是并查集

首先并查集是一个集合一种数据结构, 主要有两个操作,分别是

  • 合并(Union):把两个不相交的集合合并为一个集合。
  • 查询(Find):判断两个元素是否在同一个集合中。

主要用于处理不相交集合的合并及判断连接问题。

2、并查集能做什么

  • 判断连接问题 (社交网络、关系网)
  • 最近公共祖先
  • 渗滤
  • 图像处理
  • 有限状态自动机的等价性
  • Hinley-Milner 的多态类型推断
  • Kruskal 的最小生成树算法
  • 游戏(围棋、十六进制)
  • 在 Fortran 中的编译语句的等价性问题

2、并查集实现结构

并查集的结构有点类似树型结构, 不过与树不同, 指针是指向父节点。并且里面会存在多颗树。 如下图

在这里插入图片描述

3、算法原理

3.1、合并(Union)

即将两个节点合并归属为同一个集合。 假设有两个节点A 和 B, 一般不是将A 和 到 B, 就是将B合并到A。 在实现上其实也就是将 A的根节点的parent指针指向节点B的根节点即可。 不过为了维持这颗树结构的平衡高度, 一般由较矮的节点树 和合并到 较高的节点树。

如下图, 如果要合并 g 和 b, 先找到g和b的根节点a和e, 然后把e的父指针指向a即可完成合并。 在这里插入图片描述

3.2、查询(Find)

即判断两个节点是否有关系, 通过parent指针不断向上寻找该节点的根节点, 然后判断它们两个的根节点是否一样即可, 如果一样说明属于同一个集合, 反之不是。

虽然一般查找根节点这个算法非常简单, 但是一般为了性能优化, 减少每次向上搜索的深度, 在查找find根节点的过程中会进行路径压缩。

压缩路径过程就是, 在向上寻找的过程中如果发现父节点不是根节点, 就把父指针指向爷爷节点, 然后自己直接跳到爷爷节点进行下一次判断,依次类推。 当遍历到根节点后, 整颗路径树就会变得非常的矮壮, 大大降低的搜索的深度。

2.gif

4、代码实现

/**
 * 并查集
 * @author burukeyou
 */
public class UnionFind<T> {

    //parent[i] = x,  表示节点i所指向的父节点为x
    private int[] parent;

    // rank[i]表示以i为根的集合所表示的树的层数。 主要用来在合并集合时作参考,不代表100%的准确性
    private int[] rank;
    
    private Integer nodeKeyIndex = -1;
    
    // 当前并查集存在的节点
    private final Set<T> nodeSet = new HashSet<>();
    
    // Map<节点Key, 实际节点>
    private final Map<Integer,T> keyNodeMap = new HashMap<>();

    // Map<实际节点, 节点Key>
    private final Map<T,Integer> nodeKeyMap = new HashMap<>();

    public UnionFind(int size) {
        parent = new int[size];
        rank = new int[size];

        // 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }


    public int getSize() {
        return parent.length;
    }

    // 查找cur节点的所属根节点
    private int findRootNode(int cur) {
        if(cur < 0 || cur >= parent.length){
            throw new IllegalArgumentException("cur Index Out Of bounds");
        }
        
         // 当父指针指向自己的就是根节点
        while (cur != parent[cur]){
            //压缩路径,指向爷爷节点,以此类推
            parent[cur] = parent[parent[cur]];
            // 更新当前迭代的指针
            cur  = parent[cur];
        }

        return cur;
    }

    // 给新加入到并查集的节点 分配唯一Key
    private Integer distributeNodeKey(T node){
        if (!nodeSet.contains(node)){
            nodeSet.add(node);
            nodeKeyIndex++;
            keyNodeMap.put(nodeKeyIndex,node);
            nodeKeyMap.put(node,nodeKeyIndex);
        }
        return nodeKeyMap.get(node);
    }

    //查看元素x和元素y是否所属一个集合
    public boolean isConnected(T x , T y){
        Integer xKey = nodeKeyMap.get(x);
        Integer yKey = nodeKeyMap.get(y);
        if (xKey == null || yKey == null){
            // 任意一个为空,代表没有加入并查集肯定没关系
            return false;
        }

        // 判断根节点是否一样即可
        return findRootNode(xKey) == findRootNode(yKey);
    }

    public void union(T x, T y) {
        Integer xKey = distributeNodeKey(x);
        Integer yKey = distributeNodeKey(y);
        union(xKey,yKey);
    }


    // 合并元素x和元素y所属的集合,
    private void union(int x, int y) {
        int xRoot = findRootNode(x);
        int yRoot = findRootNode(y);

        if(xRoot == yRoot){
            // 已经属于同一个集合不需要再合并
            return;
        }

        // 将rank低的集合合并到rank高的集合上
        if(rank[xRoot] < rank[yRoot]){
            //低深度向高深度树聚合时, 直接低深度树根节点指向 搞深度树的根结点即可
            parent[xRoot] = yRoot;
        }else if (rank[xRoot] > rank[yRoot]){
            // 同上
            parent[yRoot] = xRoot;
        }else {
            //深度一样时,x合并到y即可。 然后 y树层级加1
            parent[xRoot] = yRoot;
            rank[yRoot] += 1;
        }
    }

}

复制代码

5、测试案例

  • 有以下社交关系网, 判断某两个人是否有关系
       // 创建并查集
        UnionFind<String> unionFind = new UnionFind<String>(30);
        /*
            假设有关系网
                    小明 ----- 小白 ----- 二狗 ---张三
                    |      /
                    小新

                                    小强 ---- 李四

         */
        unionFind.union("小明","小白");
        unionFind.union("小白","小新");
        unionFind.union("小白","二狗");
        unionFind.union("二狗","张三");
        unionFind.union("张三","李四");

        // 判断小新 和 张三 是否有间接好友关系  true
        boolean connected = unionFind.isConnected("小新", "张三");
        
        //  判断小新 和 小强 是否有间接好友关系 false
        boolean connected1 = unionFind.isConnected("小新", "小强");
        System.out.println();
复制代码

Guess you like

Origin juejin.im/post/7077599914737270797