一.定义

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

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

java id集合查询 java并查集算法_算法

上例中,0,7是没有路径的;8,9之间有一条可达路径,因此就算是连接的。 (学过图论的人应该不难理解)


二.数学模型

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

三.快速查找

1.数据结构

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

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

java id集合查询 java并查集算法_算法_02

2.查找

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

3.合并

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

4.实现

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

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;
            }
        }
    }

}

5.时间复杂度分析

算法

初始化

合并

查询

quick-find

O(N)

O(N)

O(1)

在这个实现中,可以看到合并操作的代价很高。如果需要在N个对象上进行N次合并,那就是O(n*n)。效率不高。


四.快速合并 

1.数据结构

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

java id集合查询 java并查集算法_算法_03

上图中,0,1,9,6 ,7,8 下标对应的值都是下标本身的值,说明这几个节点是根节点。

2 号下标对应的值是 9 ,根据上述的定义,节点 9 就是节点 2 的父节点,以此类推3,4,5节点的状态。

2.查找

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

3.合并

合并操作就非常简单了。

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

java id集合查询 java并查集算法_父节点_04

java id集合查询 java并查集算法_java id集合查询_05

上面只需要将9的值改为6即可完成合并。

4.实现

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;
    }

}

5.时间复杂度分析

算法

初始化

合并

查询

quick-find

O(N)

O(N)

O(1)

quick-union

O(N)

O(N)

O(N)

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


五.加权快速合并

1.数据结构

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

2.查找

与快速合并完全相同

3.合并

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

4.实现

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

O(N)

O(N)

O(1)

quick-union

O(N)

O(N)

O(N)

Weighted quick-union

O(N)

O(lgN)

O(lgN)

其实还能改进。


六.路径压缩

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

java id集合查询 java并查集算法_数据结构_06

 

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

java id集合查询 java并查集算法_父节点_07

如果要实现完全展平,会很复杂。我们可以选择一种折中的方案,将节点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[父节点] 就是祖父节点。

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