数据结构基础_并查集(UnionFind)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jt102605/article/details/84331088

一. 认识并查集

  • 可以高效的解决连接问题(Connectivity Problem)
  • 检查网络中节点间的连接状态(网络是个抽象概念:用户之间形成的网络)
  • 数学中的集合类实现(合并问题)
  • 连接问题和路径问题:连接问题只需回答是或否,而路径问题要回答出具体的路径;
  • 对于一组数据,并查集主要支持三个动作:
  1. union(p, q) -----------------并操作,将元素p,q并入同一个组内
  2. find(p)------------------------查操作,返回元素所在组号,通常是一个private的辅助函数
  3. isConnected(p, q)---------查操作,查询元素p,q是否在一个组内(同一个组内的元素是相互连接的)

二.并查集的实现

1. Quick Find并查集:

基本数据表示:

  • 并查集内部实际是存储了一个数组,数组的索引(id)表示元素的编号(这里的元素可以是各种类型),索引存储的值表示该元素所属的集合,而属于同一集合的元素即是连接起来的元素(下图,id为0,2,4,6,8的元素同属于集合0,是连接起来的)。

  • Quick Find并查集,实现一个了一个find函数,用来支持快速查询操作,查询操作时间复杂度为O(1), 合并操作时间复杂度为O(n);

package UFs;

import UnionFind.UnionFind;

public class quickFindUF implements UnionFind {
    //维护一个数组,数组的索引表示不同的元素(元素可以是各种类型),索引所对应的值表示该索引所属的集合
    private int[] id;

    public quickFindUF(int size){
        id = new int[size];

        //初始化每个元素都属于不同的集合
        for(int i=0; i<id.length; i++){
            id[i] = i;
        }
    }

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


    //查看p元素和q元素是否属于同一个集合
    @Override
    public boolean isConnected(int p, int q){
        return id[p] == id[q];
    }

    //合并p元素和q元素所属集合
    @Override
    public void unionElements(int p, int q){
        int pUnion = id[p];
        int qUnion = id[q];

        if(pUnion == qUnion){
            return;
        }

        for(int i=0; i<id.length; i++){
            if(id[i] == pUnion){
                id[i] = qUnion;
            }
        }
    }
}

2. Quick Union并查集:

基本数据表示:

  • 将每一个元素,看作是一个节点,是一种奇怪的树结构,是由孩子节点指向父亲节点
  • 根节点的指针指向自己,合并两个元素,就是将其中一个元素的根节点指向另一个元素的根节点

  • 底层仍然可以通过维护一个数组来实现,这里,数组的索引仍然表示元素的编号(元素可以是各种类型),但是,索引下存储的值表示的是该元素指向的父亲节点。注意:这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
  • 查询操作时间复杂度为O(h), 合并操作时间复杂度也为O(h)  (h为当前树的深度);

  • 初始化时:

package UFs;

import UnionFind.UnionFind;

public class quickUnionUF implements UnionFind {
    //内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
    //合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
    //这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
    private int[] parent;

    public quickUnionUF(int size){
        parent = new int[size];

        //初始化并查集,将每个元素的父亲节点都设为自己
        for(int i=0; i<size; i++){
            parent[i] = i;
        }
    }


    //查找过程,查找元素i所对应的集合编号,即元素i的根节点
    //O(h)的复杂度,h为当前树的高度
    private int find(int i){
        //i不是根节点时,就上移i
        while(i!=parent[i]){
            i = parent[i];
        }
        return i;
    }

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

    //查看元素p和元素q是否属于同一个集合,即根节点是否相同
    //O(h)的复杂度,h为当前树的高度
    @Override
    public boolean isConnected(int p, int q){
        int pRoot = find(p);
        int qRoot = find(q);

        return pRoot == qRoot;
    }

    //合并元素p和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;
        }

        //将pRoot的父亲节点设为qRoot,待优化
        //优化思路,合并时,将深度小的树合并到深度大的树上,降低整体树的深度
        parent[pRoot] = qRoot;

    }
}

quickUnion并查集基于size的优化

  • 基于size的优化,即维护一个sz数组,记录每个根节点所包含元素的个数,在合并元素时,将个数小的树合并到个数大的树上,避免合并后整棵树深度太深
  • sz[i]表示以i为根的集合所表示的树的总节点个数
package UFs;

import UnionFind.UnionFind;

//基于size的优化,即维护一个sz数组,记录每个根节点所包含元素的个数,在合并元素时,将个数小的树合并到个数大的树上,避免合并后整棵树深度太深
public class quickUnionUFII implements UnionFind {
    //内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
    //合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
    //这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
    private int[] parent;
    private int[] sz;  //用于记录每个根节点所拥有的节点个数,sz[i]表示以i为根的集合所表示的树的节点的总个数

    public quickUnionUFII(int size){
        parent = new int[size];
        sz = new int[size];

        for(int i=0; i<size; i++){
            parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
            sz[i] = 1;     //初始每个节点只包含自己1个节点
        }
    }


    //查找过程,查找元素i所对应的集合编号,即元素i的根节点
    //O(h)的复杂度,h为当前树的高度
    private int find(int p){
        while(parent[p]!=p){
            p = parent[p];
        }
        return p;
    }


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

    //查看元素p和元素q是否属于同一个集合,即根节点是否相同
    //O(h)的复杂度,h为当前树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    //合并元素p和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;
        }

        //合并过程,基于size的优化
        if(sz[pRoot]<sz[qRoot]){
            parent[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        }else{
            parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

quickUnion并查集基于rank的优化

  • 好基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
  • rank[i]表示以i为根的集合所表示的树的层数
package UFs;

import UnionFind.UnionFind;

//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
public class quickUnionUFIII implements UnionFind {
    //内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
    //合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
    //这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
    private int[] parent;
    private int[] rank;  //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数

    public quickUnionUFIII(int size){
        parent = new int[size];
        rank = new int[size];

        for(int i=0; i<size; i++){
            parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
            rank[i] = 1;     //初始每个节点自由一层
        }
    }


    //查找过程,查找元素i所对应的集合编号,即元素i的根节点
    //O(h)的复杂度,h为当前树的高度
    private int find(int p){
        while(parent[p]!=p){
            p = parent[p];
        }
        return p;
    }


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

    //查看元素p和元素q是否属于同一个集合,即根节点是否相同
    //O(h)的复杂度,h为当前树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    //合并元素p和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的优化,将层数小的树合并到层数大的树中
        if(rank[pRoot]<rank[qRoot]){
            parent[pRoot] = qRoot;
        }else if(rank[pRoot]>rank[qRoot]){
            parent[qRoot] = pRoot;
        }else {
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }
}

quickUnion并查集经典的“路径压缩”的优化

  • 在find过程中实现

package UFs;

import UnionFind.UnionFind;

//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
//同时做了一个经典的优化,在find过程中添加了路径压缩优化
public class quickUnionUFIV implements UnionFind {

    //内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
    //合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
    //这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
    private int[] parent;
    private int[] rank;  //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数

    public quickUnionUFIV(int size){
        parent = new int[size];
        rank = new int[size];

        for(int i=0; i<size; i++){
            parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
            rank[i] = 1;     //初始每个节点自由一层
        }
    }


    //查找过程,查找元素i所对应的集合编号,即元素i的根节点
    //O(h)的复杂度,h为当前树的高度
    private int find(int p){
        while(parent[p]!=p){
            //路径压缩优化, 若i为根节点,则其parent是其本身,即parent[i] = i
            parent[p] = parent[parent[p]];

            p = parent[p];
        }
        return p;
    }


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

    //查看元素p和元素q是否属于同一个集合,即根节点是否相同
    //O(h)的复杂度,h为当前树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    //合并元素p和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的优化,将层数小的树合并到层数大的树中
        if(rank[pRoot]<rank[qRoot]){
            parent[pRoot] = qRoot;
        }else if(rank[pRoot]>rank[qRoot]){
            parent[qRoot] = pRoot;
        }else {
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }
}

quickUnion并查集经典的“路径压缩”的优化,递归实现

  • 可以在find(i)时,直接将i的parent设为根节点,时间性能上稍逊于非递归实现的“压缩路径”;
  • 非递归的“压缩路径”虽然不能在一次调用find(i)后就达到将i的parent设为根节点的效果,但多次对i调用find(i)后是可以的,避免了递归过程中花费的开销

package UFs;

import UnionFind.UnionFind;

//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
//"压缩路径"优化的递归实现,可以在find(i)时,直接将i的parent设为根节点,性能上稍微逊与非递归的“压缩路径”
//因为非递归的“压缩路径”在多次对i进行find(i)后,也可达到将i的parent设为根节点的效果,避免了递归的开销。
public class quickUnionUFV implements UnionFind {

    //内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
    //合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
    //这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
    private int[] parent;
    private int[] rank;  //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数

    public quickUnionUFV(int size){
        parent = new int[size];
        rank = new int[size];

        for(int i=0; i<size; i++){
            parent[i] = i;   //初始化并查集,将每个元素的父亲节点都设为自己
            rank[i] = 1;     //初始每个节点自由一层
        }
    }


    //查找过程,查找元素i所对应的集合编号,即元素i的根节点
    //O(h)的复杂度,h为当前树的高度
    private int find(int p){
        if(parent[p]!=p){
            //递归实现"路径压缩"优化, 若i为根节点,则其parent是其本身,即parent[i] = i
            //直接将i的parent设为根节点
            parent[p] = find(parent[p]);
        }
        return parent[p];
    }


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

    //查看元素p和元素q是否属于同一个集合,即根节点是否相同
    //O(h)的复杂度,h为当前树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    //合并元素p和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的优化,将层数小的树合并到层数大的树中
        if(rank[pRoot]<rank[qRoot]){
            parent[pRoot] = qRoot;
        }else if(rank[pRoot]>rank[qRoot]){
            parent[qRoot] = pRoot;
        }else {
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }
}

并查集的时间复杂度分析:

猜你喜欢

转载自blog.csdn.net/jt102605/article/details/84331088