并查集介绍及其原理

什么是并查集

假设我们目前有6个样本 a、b、c、d、e、f。每个样本所在的集合都只有自己({a}、{b}、{c}、{d}、{e}、{f})。
并查集是一个维护着很多集合的一个结构(将所有的样本先建成一个集合,集合中只有自己),并且提供2个方法。
isSameSet(V a,V b)这个方法用来判断传进来的a、b样本所在集合是否在同一个集合,返回true和false。
union(V a,V b)这个方法将a所在集合和b所在集合,变成一个集合。
方法并不少见,但并查集的特点在于,如果有N个样本,调用两个方法很频繁,并查集可以做到均摊下来单次时间复杂度为 O ( 1 ) O(1) O(1)
什么是均摊?
比如说样本量很大,N = 1E,调用两个方法的次数很频繁 , 大于1E,那每次调用的时间复杂度平均下来就是一个常数时间 O ( 1 ) O(1) O(1)

举例
有{a}、{b}、{c}三个样本,每个样本都有属于自己的集合List A, List B,List C。还有一个记录着样本集合对应关系的HashMap,HashMap<A,List A>,HashMap<B,List B>,HashMap<C,List C>。
此时将A 和 C 的集合进行合并。
合并完之后就是{a,c}、{b}、 List A, List B 和 HashMap<A,List A>、HashMap<B,List B>,HashMap<C,List A>的形式。
a,c样本合并的过程,首先要将List集合进行合并,再将Map中索引修改。如果A,C集合中各有100W数据,两个List合成一个,在将其中100W集合的Map索引进行修改,肯定不是一个 O ( 1 ) O(1) O(1)的操作。
List能实现,但是效率不好。

并查集实现

来看看并查集实现集合Union的原理。
同样6个样本a、b、c、d、e、f,并查集会在样本中包上一层,每一个样本有一个指针,指向自己。
在这里插入图片描述
为什么要指向自己呢?
比如,a,c样本所在集合到底是不是一个集合?
一开始肯定都不是,a,c集合顺着节点一路向上找,向上到不能再向上。a还是a,c还是c。这种一直向上找节点,最终找到的节点叫做代表节点也可以理解为父节点。a的代表节点和c的代表节点不是一个,所以说a和c不在一个集合内。
接下来,将a,c两个集合进行合并。

同样找代表节点,并且如果能够获取到两个集合大小的话,比如说a:1, c:1,此时将c的指针指向a,完成合并。再次判断a,c是否是同一个集合,a的代表节点还是a,但是c的代表节点变成了a,是同一个集合。
在这里插入图片描述
再试一次,这回判断c和d是否是同一个集合。
c的代表节点此时是a,d的代表节点是自己d。不是同一个集合,进行合并,c向上到不能再向上找到a,大小 = 2,d向上到不能在向上找到d 大小 = 1,小的挂大的,将d直接挂在a的下面。
在这里插入图片描述
再来。
判断b和e是否是同一个集合,不是,进行合并,大小相同谁挂谁无所谓。合并后如图。
在这里插入图片描述
此时判断,c所在集合和d所在集合,是否是同一个集合,不是,因为c向上找到的代表节点是a,d找到的代表节点是b,进行合并 a = 3, b = 2小的挂大的。代表节点直接过去。完成合并。
在这里插入图片描述
只改了一次指针完成合并,并且通过代表节点来判断是在哪一个集合。

代码

 /*
    * 将给定的样本V封装成Node,图中样本包含的圈
    * */
    public static class Node<V> {
        V val;

        public Node(V value) {
            this.val = value;
        }
    }

    public static class UnionFind<V> {
        //对于用户来讲,不知道你包Node,依然是通过样本V来实现逻辑交互
        //所以nodes是用来存放样本V和封装Node的关系
        HashMap<V, Node<V>> nodes;
        //key的代表节点(parent)是value
        //用这个Map表示图中指针
        HashMap<Node<V>, Node<V>> parentMap;
        //集合大小
        //不是所有节点都有记录,只有代表节点才在这个Map中有记录
        HashMap<Node<V>, Integer> sizeMap;

        public UnionFind(List<V> values) {
            //初始化
            nodes = new HashMap<>();
            parentMap = new HashMap<>();
            sizeMap = new HashMap<>();

            //遍历传进来的样本V,并进行赋值操作
            for (V val : values) {
                //将V封装成Node
                Node node = new Node(val);
                nodes.put(val, node);
                //默认我的parent是我自己
                parentMap.put(node, node);
                //默认大小为1
                sizeMap.put(node, 1);
            }
        }

        /*
        * 根据Node找到代表节点
        * */
        public Node<V> findParent(Node<V> node) {
            Stack<Node> stack = new Stack<>();
            //如果 != ,说明上面还有代表节点,一直找
            //将找到的沿途节点存放到Stack中
            //此时找完node就是它的代表节点,return这个node即可
            while (node != parentMap.get(node)) {
                node = parentMap.get(node);
                stack.push(node);
            }

            //沿途链上所有节点的代表节点都更改为我最上面的代表节点
            //作用是使其扁平化,减少层数
            while (!stack.isEmpty()) {
                parentMap.put(stack.pop(), node);
            }
            return node;
        }
        /*
        * 对于用户来说,依然是用样本V进行交互
        * 看样本a,b的代表节点是否是同一个
        * */
        public boolean isSameSet(V a, V b) {
            return findParent(nodes.get(a)) == findParent(nodes.get(b));
        }

        /*
        * 样本a,b进行合并
        * */
        public void union(V a, V b) {
            Node aNode = findParent(nodes.get(a));
            Node bNode = findParent(nodes.get(b));
            //代表节点不同,说明不是同一个
            if (aNode != bNode) {
                //获取a,b集合大小
                int aSize = sizeMap.get(aNode);
                int bSize = sizeMap.get(bNode);
                //找到大节点和小节点。
                Node big = aSize >= bSize ? aNode : bNode;
                Node small = big == aNode ? bNode : aNode;
                //小挂大
                parentMap.put(small, big);
                //样本中对应集合数量修改
                sizeMap.put(big, aSize + bSize);
                //两个集合进行了合并,small的代表节点发生了变化,remove掉当前的small的大小
                sizeMap.remove(small);
            }
        }

        public int getSize(){
            return sizeMap.size();
        }
    }

总结

并查集的好处就在于小挂大以及代码中的findParent()方法,如果整个集合的链很长,那每次遍历都很耗时,小挂大可以让链增长的缓慢,而findParent()方法在union和isSameSet中都有调用,如果两个方法调用很频繁,每次通过findParent找代表节点,可以使链上结构扁平化的话,每次查找都很快,一次就可以找到代表节点(第一次遍历耗时一些)
两个操作目的都是减少链长度。
结论
如果样本量为N,如果findParent调用次数到达了 O ( N ) O(N) O(N)或者超过了 O ( N ) O(N) O(N),那么单次平均下来查询代价 O ( 1 ) O(1) O(1)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值