通过哈希表或者数组实现并查集

        并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

例如:在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合, 然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大, 若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高, 根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

本文先通过哈希表实现并查集结构和提供的操作,然后再通过数组实现,数组实现的效率比较高,一般在解题中使用。

通过哈希表实现并查集

本文并查集结构提供的二个操作是:

1)查询二个样本是否在同一个集合

boolean isSameSet(V a,V b);

2)把a所在集合的全体 和 b所在集合的全体 合并成一个集合

void union(V a,V b);

首先将给定样本V类型包成Node

  public static class Node<V> {//给样本v类型包了一层成Node
        V value;
        public Node(V v) {
            value = v;
        }
    }

定义并查集类

                  //并查集结构
    public static class UnionFind<V> {
        //一张表:从a -> Node(a) ,b -> Node(b) 从对象 到 对象包装成Node的 map
        public HashMap<V, Node<V>> nodes;
        public HashMap<Node<V>, Node<V>> parents;//不想在Node上增加指针,用这个表可以做到指针的作用;从孩子节点可以找到父节点
        public HashMap<Node<V>, Integer> sizeMap;//只有一个节点是代表节点,才会在sizeMap中记录这个节点所在集合的大小
                        //代表节点是一个集合最上面的节点
        //并查集的初始化
        public UnionFind(List<V> values) { //将样本中每个对象都建成一个小集合
            nodes = new HashMap<>();
            parents = new HashMap<>();
            sizeMap = new HashMap<>();
            for (V cur : values) {
                Node<V> node = new Node<>(cur);
                nodes.put(cur, node);
                parents.put(node, node); //初始时集合只有一个节点,那么这个节点的父亲节点就是字节(即那个指针循环指向自己)
                sizeMap.put(node, 1); //初始时集合只有一个节点,那么这个节点就是代表节点,会在sizeMap中记录这个节点所在集合的大小
            }
        }

        //findFather: 给你一个节点,请你往上到不能再往上,把代表节点返回
        public Node<V> findFather(Node<V> cur) {
            /* 重要优化:在从一个节点找到往上找到代表节点的过程中,把过程中的每个节点的挂到代表节点上面。 */
            Stack<Node<V>> path = new Stack<>();
            while (cur != parents.get(cur)) { //cur == parents.get(cur)说明找到了代表节点,否则表示没找到
                path.push(cur); //沿途遇见的节点入栈
                cur = parents.get(cur);
            }//循环退出时候,cur就是代表结点
            while (!path.isEmpty()) {
                parents.put(path.pop(), cur); //优化:沿途每个节点全部挂到代表节点上面
            }
            return cur;//返回代表节点
        }
        //isSameSet:查询a样本和b样本在不在一个集合
        public boolean isSameSet(V a, V b) {
            return findFather(nodes.get(a)) == findFather(nodes.get(b));
            //通过nodes这个map找到a和b的node,然后通过findFather找到他们的代表节点,然后判断代表结点是不是相等
        }
        //union:将a所在集合的全体 和 b所在集合的全体 合成一个集合
        public void union(V a, V b) {
            Node<V> aHead = findFather(nodes.get(a));//获取a样本所在集合的代表结点
            Node<V> bHead = findFather(nodes.get(b)); //获取b样本所在集合的代表节点
            if (aHead != bHead) {//如果二者的代表节点不是同一个表示二者不在同一个集合,需要进行union操作。
                int aSetSize = sizeMap.get(aHead);//获取代表节点所在集合的全部个数
                int bSetSize = sizeMap.get(bHead);
                Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
                Node<V> small = big == aHead ? bHead : aHead;//大小集合代表节点重定向
                //小集合代表节点往上直接指向大集合代表节点的头
                parents.put(small, big);//小集合代表结点的父亲设置为大集合代表节点
                sizeMap.put(big, aSetSize + bSetSize);  //大集合的大小将变成二个集合的和
                sizeMap.remove(small); //small挂在了大集合代表节点下面,已经不是代表节点,不需要保存记录了,可以删除。
                //remove的愿意你是sizeMap中只存放代表结点的相关大小记录值
            } //如果二者的代表节点相等就表示都在一个集合了,不用做任何事
        }

        public int sets() {
            return sizeMap.size();
            //可以通过sizeMap中代表节点的数量判断并查集中集合的数量
        }

    }

通过数组实现并查集解leetcode547省份数量 :

 public static int findCircleNum(int[][] M) {
        int N = M.length;
        // {0} {1} {2} {N-1}每个都成一个集合初始化好
        UnionFind unionFind = new UnionFind(N); //
        for (int i = 0; i < N; i++) {            //i=0是行号
            for (int j = i + 1; j < N; j++) {    //j=i+1是列号
                //只遍历右上半区:因为矩阵是对称的,且对角线上肯定是1
                if (M[i][j] == 1) { // i和j互相认识
                    unionFind.union(i, j);//i背后的集合 和 j背后的集合 合并成一个集合
                }
            }
        }//这个双重循环跑完,那么该合并的都合并了。
        return unionFind.sets();//返回并查集里面有多少集合
    }

数组实现并查集结构:

 public static class UnionFind {
        private int[] parent;  // parent[i] = k : i的父亲是k
        private int[] size; //size[i]=k:如果i是代表节点,size[i]才有意义,否则无意义; i所在的集合大小是多少
        private int[] help;   // 辅助结构:用作栈存放元素
        private int sets;      // 记录并查集一共有多少个集合
        public UnionFind(int N) {
            parent = new int[N];
            size = new int[N];
            help = new int[N];
            sets = N;
            for (int i = 0; i < N; i++) {//0 ~N-1每个都变成自己的集合
                parent[i] = i;//当前数的父亲是自己
                size[i] = 1;//当前代表节点所在集合的大小
            }
        }

        // 从i开始一直往上,往上到不能再往上,返回代表节点(同时过程中要做路径压缩)
        private int find(int i) {//从i开始一直往上,往上到
            int hi = 0;
            while (i != parent[i]) {//i只要不等于自己的父亲,i就一直往上
                help[hi++] = i;//把i寻找代表节点的过程中遇到的所有节点都记录到栈中
                i = parent[i];
            }//循环退出后表示找到了代表节点i
            for (hi--; hi >= 0; hi--) {
                parent[help[hi]] = i;//将栈中记录的所有数组的父节点在parent中设置为i
            }
            return i;
        }

        public void union(int i, int j) {
            int f1 = find(i);//获得i所在集合的代表节点
            int f2 = find(j);//获得j所在集合的代表结点
            if (f1 != f2) {//i和j不在同一个结合,需要合并这二个集合
                //小集合挂到大集合的下面,能让树的增长变慢
                if (size[f1] >= size[f2]) {
                    size[f1] += size[f2];
                    parent[f2] = f1;//小集合的父亲设置为大集合的代表节点
                } else {
                    size[f2] += size[f1];
                    parent[f1] = f2;
                }
                sets--;//二个集合合并成一个集合后,集合数量减少1
            }
            //如果f1和f2相等说明i和j在同一个集合中
        }

        public int sets() {
            return sets;
        }
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值