并查集Java实现

定义

就是有“合并集合”和“查找集合中的元素”两种操作的关于数据结构的一种算法。

  • 连接两个对象
  • 判断是否这两个对象是连接的

这里写图片描述

上例中,0,7是没有路径的;8,9之间有一条可达路径,因此是连接的。

建模

关于连接

  • 等价性: p连接到p,每个对象都能连接到自己
  • 对称性: p连接到q;等价于q连接到p
  • 传递性: 如果p连接到q,q连接到r,那么,p连接到r。

快速查找

数据结构

这里用到的数据结构是数组。

  • 对象索引整数数组,用索引来表示N个对象,0表示第一个对象,以此类推。
  • 如果p和q是连接的,那么它们数组的值相同。

这里写图片描述

查找

只需要检查p和q是否有相同的值。

合并

将它们的值设为相同的。假如需要合并两个分别包含p和q的子集,将所有值等于idp的对象的值改为id[q],即改为q的值。

实现

初始化、判断连接以及合并

public class QuickFindUF {
    private int [] id;
    public QuickFindUF(int N){
        id = new int[N];
        for (int i = 0; i < N; i++) {
            id[i] = i;  //初始化
        }
    }

    /**
     * 判断p和q是否是连接的
     * @param p
     * @param q
     * @return
     */
    public boolean connected(int p,int q){
        return id[p] == id[q];
    }

    /**
     * 合并p的子集和q的子集
     * 将所有值等于id[p]的对象的值改为id[q]
     * @param p
     * @param q
     */
    public void union(int p,int q){
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < id.length; i++) {
            if(id[i] == pid){
                id[i] = qid;
            }
        }
    }

}

时间复杂度分析

算法 初始化 合并 查询
quick-find N N 1

在这个实现中,合并操作的代价太高了。如果需要在N个对象上进行N次合并,那就是O(n^2)。效率不高。

快速合并

数据结构

也是数组。

  • 对象索引整数数组,用索引来表示N个对象,0表示第一个对象,以此类推。
  • id[i]存放的值代表i的父节点索引
  • 若id[i]==i,则说明i是根节点

这里写图片描述

上面3的父节点是4,4的父节点是9,9的父节点也是9,9为根节点。

查找

检查p和q根节点是否相同。

合并

合并操作就非常简单了。

要合并p,q两个对象的分量(合并树),将p的根节点设为q的根节点(p=>q p的根节点指向q)。

这里写图片描述

这里写图片描述

上面只需要将9的值改为6即可。

实现

public class QuickUnionUF {
    private int [] id;

    public QuickUnionUF(int N){
        id = new int[N];
        for (int i = 0; i < N; i++) {
            id[i] = i;
        }
    }

    /**
     * 找到i的根节点
     * @param i
     * @return
     */
    private int root(int i){
        while(i != id[i]){
            i = id[i];//i和id[i] 一起更新
        }
        return i;
    }

    /**
     * 判断p和q是否是连接的
     * @param p
     * @param q
     * @return
     */
    public boolean connected(int p,int q){
        return root(p) == root(q);
    }

    /**
     * 合并p和q
     * 将p的根节点设为q的根节点(p=>q p的根节点指向q)。
     * @param p
     * @param q
     */
    public void union(int p,int q){
        int qroot = root(q);//找到q的根节点
        int proot = root(p);
        id[proot] = qroot;
    }

}

时间复杂度分析

算法 初始化 合并 查询
quick-find N N 1
quick-union N N N

最坏的情况下,会生成一颗瘦长树。

加权快速合并(Weighted quick-union)

数据结构

与快速合并类似,但是维护了一个额外的数组sz[i],它包含了以i为根节点的树的节点数量。

查找

与快速合并完全相同

合并

  • 将小树指向大树的根节点
  • 更新sz[]数组

实现

package com.algorithms.UnionFind;

public class WeightedQuickUnionUF {
    private int [] id;
    private int [] sz;//size of component for roots
    private int count;//number of components

    public WeightedQuickUnionUF(int N){
        count = N;
        id = new int[N];
        for (int i = 0; i < N; i++) id[i] = i;
        sz = new int[N];
        for (int i = 0; i < N; i++) sz[i] = 1;
    }

    public int count(){
        return count;
    }

    /**
     * 找到i的根节点
     * @param i
     * @return
     */
    private int root(int i){
        while(i != id[i]){
            i = id[i];//i和id[i] 一起更新
        }
        return i;
    }

    /**
     * 判断p和q是否是连接的
     * @param p
     * @param q
     * @return
     */
    public boolean connected(int p,int q){
        return root(p) == root(q);
    }

    /**
     * 合并p和q
     * 将p的根节点设为q的根节点(p=>q p的根节点指向q)。
     * @param p
     * @param q
     */
    public void union(int p,int q){
        int i = root(q);
        int j = root(p);
        if( i == j) return;

        //Make smaller root point to larger one.
        if(sz[i] <sz[j]){
            id[i] = j;
            sz[j] += sz[i];
        }else{
            id[j] = i;
            sz[i] += sz[j];
        }
        count --;
    }
}
算法 初始化 合并 查询
quick-find N N 1
quick-union N N+ N
Weighted quick-union N lgN+ lgN

+ 加上查找根节点的消耗
* 树中任意节点的深度,最大是lgN (以2为底)

但是,还能再改进!

路径压缩

在计算p节点的根后,将每个经过的节点都指向根。

这里写图片描述

上图中,从p找到根0,需要经过9(当然要包括自己),6,3,1(已经指向根节点了)这些节点。让它们都指向根:

这里写图片描述

如果要实现完全展平,会很复杂。我们可以选择一种折中的方案,将节点p指向节点p的祖父(父节点的父节点)节点。

神奇的是,代码只需要在加权快速合并的基础上,进行一些小的改动:
将路径上的每个节点指向它的祖父节点:

private int root(int i){
        while(i != id[i]){
            id[i] = id[id[i]];//(父节点的父节点)
            i = id[i];//i和id[i] 一起更新
        }
        return i;
}

增加了id[i] = id[id[i]]
id[i]代表i的父节点,id[父节点] 就是祖父节点。

虽然这不如完全展平那么好(如上图所示的那样),但是,在实际应用中,两者的效果差不多。

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/78929640