0050数据结构之并查集

Posted xiao1572662

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了0050数据结构之并查集相关的知识,希望对你有一定的参考价值。

-------------------------并查集-------------------------

并查集是一种特殊的树,由孩子指向父亲

用于解决连接问题和路径问题:

判断网络中节点的连接状态

将每一个元素,看做是一个节点,将a和b合并成一个集合的时候,只需要让a所在的根节点指向b所在的根节点即可,而查询两个元素是否在一个集合中,只需要找到各自的根节点,如果两个根节点是同一个根节点,则说明是在同一个集合中:这样查询较快,合并也较快。

并查集接口设计如下:

package unionFind;
public interface UF {
    int getSize();
    boolean isConnected(int p, int q);
    void unionElements(int p, int q);
}

 

quik find实现如下:

package unionFind;

// 我们的第一版Union-Find
public class UnionFind1 implements UF {

    private int[] id;    // 我们的第一版Union-Find本质就是一个数组

   
public UnionFind1(int size) {

        id = new int[size];

        // 初始化, 每一个id[i]指向自己, 没有合并的元素
       
for (int i = 0; i < size; i++)
            id[i] = i;
    }

    @Override
    public int getSize(){
        return id.length;
    }

    // 查找元素p所对应的集合编号
   
// O(1)复杂度
   
private int find(int p) {
        if(p < 0 || p >= id.length)
            throw new IllegalArgumentException("p is out of bound.");

        return id[p];
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(1)复杂度
   
@Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(n) 复杂度
   
@Override
    public void unionElements(int p, int q) {

        int pID = find(p);
        int qID = find(q);

        if (pID == qID)
            return;

        // 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
       
for (int i = 0; i < id.length; i++)
            if (id[i] == pID)
                id[i] = qID;
    }
}

 

quik union实现如下:

package unionFind;

// 我们的第二版Union-Find
public class UnionFind2 implements UF {

    // 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
   
// parent[i]表示第一个元素所指向的父节点
   
private int[] parent;

    // 构造函数
   
public UnionFind2(int size){

        parent = new int[size];

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

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

    // 查找过程, 查找元素p所对应的集合编号
   
// O(h)复杂度, h为树的高度
   
private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不断去查询自己的父亲节点, 直到到达根节点
       
// 根节点的特点: parent[p] == p
        
while(p != parent[p])
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        parent[pRoot] = qRoot;
    }
}

 

基于size的优化:让高度小的树的根节点指向高度比较高的树的根节点,这样做的好处是形成的树的高度不会过高,在寻找某个节点的根节点的时候效率也会较快,如果不进行高度判断,最坏的情况有可能形成的是单链表

package unionFind;

// 我们的第三版Union-Find
public class UnionFind3 implements UF{

    private int[] parent; // parent[i]表示第一个元素所指向的父节点
   
private int[] sz;     // sz[i]表示以i为根的集合中元素个数

    
// 构造函数
   
public UnionFind3(int size){

        parent = new int[size];
        sz = new int[size];

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

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

    // 查找过程, 查找元素p所对应的集合编号
   
// O(h)复杂度, h为树的高度
   
private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不断去查询自己的父亲节点, 直到到达根节点
       
// 根节点的特点: parent[p] == p
       
while( p != parent[p] )
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        // 根据两个元素所在树的元素个数不同判断合并方向
       
// 将元素个数少的集合合并到元素个数多的集合上
       
if(sz[pRoot] < sz[qRoot]){
            parent[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        }
        else{ // sz[qRoot] <= sz[pRoot]
            
parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

 

基于rank的优化:深度比较低的那颗树向深度比较高的那颗树合并

package unionFind;

// 我们的第四版Union-Find
public class UnionFind4 implements UF {

    private int[] rank;   // rank[i]表示以i为根的集合所表示的树的层数
   
private int[] parent; // parent[i]表示第i个元素所指向的父节点

   
// 构造函数
   
public UnionFind4(int size){

        rank = new int[size];
        parent = new int[size];

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

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

    // 查找过程, 查找元素p所对应的集合编号
   
// O(h)复杂度, h为树的高度
   
private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不断去查询自己的父亲节点, 直到到达根节点
       
// 根节点的特点: parent[p] == p
       
while(p != parent[p])
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        // 根据两个元素所在树的rank不同判断合并方向
       
// rank低的集合合并到rank高的集合上
       
if(rank[pRoot] < rank[qRoot])
            parent[pRoot] = qRoot;
        else if(rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
           
parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此时, 我维护rank的值
       
}
    }
}

 

路径压缩优化:parent[p]=parent[parent[p]],解下图中的节点4找根节点的时候,会由下图中的图1变为图2的形状;而如果再次寻找节点4和节点3的时候,会再次由下图中的图2变为图3的关系。这样很方便的降低了树的高度。

改变树的高度的时候,却没有改变rank值,是否合理?是合理的,因为rank实际并不代表树的高度,真正的解释是排名:即上边的树的排名(rank)值大于下边的树的排名,这样的一个规律还是一直存在的。

package unionFind;

// 我们的第五版Union-Find
public class UnionFind5 implements UF {

    // rank[i]表示以i为根的集合所表示的树的层数
   
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
   
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
   
private int[] rank;
    private int[] parent; // parent[i]表示第i个元素所指向的父节点

   
// 构造函数
   
public UnionFind5(int size){

        rank = new int[size];
        parent = new int[size];

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

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

    // 查找过程, 查找元素p所对应的集合编号
   
// O(h)复杂度, h为树的高度
   
private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        while( p != parent[p] ){
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        // 根据两个元素所在树的rank不同判断合并方向
       
// rank低的集合合并到rank高的集合上
       
if( rank[pRoot] < rank[qRoot] )
            parent[pRoot] = qRoot;
        else if( rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
           
parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此时, 我维护rank的值
       
}
    }
}

 

递归调用:让该节点到根节点的路径上的所有节点都指向根节点。其中改动为find方法中的while循环改为递归算法

        if(p != parent[p])

            parent[p] = find(parent[p]);

        return parent[p];

经测试,使用递归的方式效率还要慢于上一版的效率

 

 

package unionFind;

// 我们的第六版Union-Find
public class UnionFind6 implements UF {

    // rank[i]表示以i为根的集合所表示的树的层数
   
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
   
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
   
private int[] rank;
    private int[] parent; // parent[i]表示第i个元素所指向的父节点

   
// 构造函数
   
public UnionFind6(int size){

        rank = new int[size];
        parent = new int[size];

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

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

    // 查找过程, 查找元素p所对应的集合编号
   
// O(h)复杂度, h为树的高度
   
private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // path compression 2, 递归算法
       
if(p != parent[p])
            parent[p] = find(parent[p]);
        return parent[p];
    }

    // 查看元素p和元素q是否所属一个集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
   
// O(h)复杂度, h为树的高度
   
@Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        // 根据两个元素所在树的rank不同判断合并方向
       
// rank低的集合合并到rank高的集合上
       
if( rank[pRoot] < rank[qRoot] )
            parent[pRoot] = qRoot;
        else if( rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
           
parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此时, 我维护rank的值
       
}
    }
}

 

树的4个变种:堆、线段树、trie、并查集

以上是关于0050数据结构之并查集的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之并查集

数据结构之并查集

算法复习之并查集

ACM入门之并查集

数据结构之并查集Union-Find Sets

hiho14 无间道之并查集图论--并查集