数据结构----并查集Java

并查集:(union-find sets)是一种简单的用途广泛的集合. 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多。

应用场景:

  • 网络连接判断:
    如果每个pair中的两个整数分别代表一个网络节点,那么该pair就是用来表示这两个节点是需要连通的。那么为所有的pairs建立了动态连通图后,就能够尽可能少的减少布线的需要,因为已经连通的两个节点会被直接忽略掉。
  • 变量名等同性(类似于指针的概念):
    在程序中,可以声明多个引用来指向同一对象,这个时候就可以通过为程序中声明的引用和实际对象建立动态连通图来判断哪些引用实际上是指向同一对象。

换句话说就是,判断两个双向节点是否连通,并且不需要给出路径的问题,都可以用本数据结构快速解决。

Java实现及分析

并查集的api设计如下:

union(p,q); //合并p、q两点使他们两个连通.
find(p); //找到节点q的连通性,(处在什么状态合谁联通)
isConnected(p,q);//通过find的api,我们可以找到两个节点是否会连通的,即api

Quick-Find实现方式

分析以上的API,方法connected和union都依赖于find,connected对两个参数调用两次find方法,而union在真正执行union之前也需要判断是否连通,这又是两次调用find方法。因此我们需要把find方法的实现设计的尽可能的高效。所以就有了下面的Quick-Find实现。

/**
 * 简单的数组并查集
 *      通过数组来维护区域是否连通,相同区域id的数据连通
 *      find时间复杂度为O(1)
 *      Union时间复杂度为O(n)
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS01 {
    //用一个数组来表示节点的连通性,有相同id内容的节点是连通的
    private int[] mIds;
    //节点个数
    private int mcount;

    /**
     * 初始化状态,并设置每个节点互不连通
     * @param capcity
     */
    public UFS01(int capcity){
        mIds = new int[capcity];
        mcount = capcity;
        for (int i = 0; i < capcity; i++) {
            mIds[i] = i;//各自节点的id都不一样
        }
    }

    /**
     * 返回当前节点的连通id
     * @param p
     * @return
     */
    public int find(int p){
        if (p<0 || p>=mcount){
            throw new RuntimeException("越界喽");
        }
        return mIds[p];
    }

    /**
     * 判断a,b节点是否连通
     * @param a
     * @param b
     * @return
     */
    public boolean isConnect(int a, int b){
        return find(a)==find(b);
    }

    /**
     * 连通a,b节点
     *      联合的整体思路:
     *          要么把a索引在mIds中的状态变成b的,
     *          要么把b索引在mIds中的状态变成a的
     * @param a
     * @param b
     */
    public void union(int a, int b){
        int aId = find(a);
        int bId = find(b);

        //如果已经连通,就不管了
        if (aId == bId){
            return;
        }

        //将bId的全部变成aId,需要将每个节点的id都变过来的
        for (int i = 0; i < mIds.length; i++) {
            if (mIds[i] == bId){
                mIds[i] = aId;
            }
        }
    }
}

分析:
可以看到find方法是O(1)的复杂度,十分的快。但是union方法需要一次判断每个节点的区域并改过来,调用一次就是O(n),如果有m对相连的关系,时间复杂度小于O(nm),也是O(n^2)级别的。

Quick-Union实现方式

上述方法为什么会复杂度较高?每个节点的区域编号都是单独记录的,各自为政的,当需要修改的时候,就只能一个一个去通知了。如果我们将节点有效地组织起来,方便查找和修改就好了,分析:链表、图、树什么的,最后我们可以确定树查找和修改效率好一点。

我们假设刚开始每个节点都是区域头结点,即自己的父节点是自己。然后将相连的节点连接起来,每个节点最终都会指向一个区域头节点,那就是他所在的区域。

/**
 * 类似树的并查集
 *      通过指向父节点的指针来维护区域是否连通
 *      时间复杂度不定,如果组成了线性的树,时间复杂度偏高。
 *
 *      可以改进的方向:维护每个节点的下面层数 或者 子节点 个数,union的时候,将个数少的节点连接到个数多的节点上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS02 {
    /**
     * 维护指向父节点的指针
     */
    private int[] mParents;
    private int mCount;

    /**
     * 初始化数组,默认每个节点都是区域头节点,即指针指向自己
     * @param capacity
     */
    public UFS02(int capacity){
        mCount = capacity;
        mParents = new int[capacity];
        for (int i = 0; i < capacity; i++) {
            mParents[i] = i;
        }
    }

    /**
     * 查找P节点的区域头结点
     * @param p
     * @return
     */
    public int find(int p){
        if (p<0 || p>=mCount){
            throw new RuntimeException("越界喽");
        }
        /**
         * 向上查找,直到是一个区域头结点
         */
        while (p != mParents[p]){
            p = mParents[p];
        }
        return p;
    }

    public boolean isConnect(int a, int b){
        return find(a)==find(b);
    }

    /**
     * 联合,将a,b节点的区域头结点联合即可
     * @param a
     * @param b
     */
    public void union(int a, int b){
        int aRoot = find(a);
        int bRoot = find(b);
        if (aRoot == bRoot){
            return;
        }
        mParents[bRoot] = aRoot;
    }
}

分析:
当然,用树结构的话就得小心,如果树结构退化成了链表怎么办,那岂不是效率也挺低?当然,我们可以用红黑树,AVL树等,我们此处稍加分析,可以发现,union的时候稍加判断,可以提高效率啊:可以改进的方向,维护每个节点的下面层数 或者 子节点 个数,union的时候,将个数少的节点连接到个数多的节点上面。

Weighted Quick-Union 实现方式

如上所说,维护每个节点的下面层数 或者 子节点 个数,union的时候,将个数少的节点连接到个数多的节点上面。

/**
 * 可以改进的方向:维护每个节点的子节点 个数,union的时候,将个数少的节点连接到个数多的节点上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS04 {
    private int[] mParents;
    //新加一个数组用来记录每一个节点,以它为根的元素的个数。
    //mSize[i]表示以i为根的树结构中的元素个数。
    private int[] mSize;
    private int mCount;

    public UFS04(int capacity){
        mCount = capacity;
        mParents = new int[mCount];
        mSize = new int[mCount];
        for (int i = 0; i < mCount; i++) {
            mParents[i] = i;
            //默认每个都是1:独立的时候含有一个元素.
            mSize[i] = 1;
        }
    }
    //以下find和isConnected都用不到mSize.
    public int find(int p){
        if( p<0 || p>=mCount){
            //...做一些异常处理
        }
        while(p!=mParents[p]){
            p = mParents[p];
        }
        return p;
    }
    public boolean isConnected(int p,int q){
        return find(p)==find(q);
    }
    //联合的时候就需要用到mSize了.看看那个节点为根的树形集合中元素多,
    //然后把少的那个节点对应的根,指向多的那个节点对应的根。
    public void union(int p,int q){
        //前两步不变
        int pRoot= find(p);
        int qRoot = find(q);
        if(pRoot == qRoot){
            return;
        }
        int pSize = mSize[pRoot];//初始事都是根,为1
        int qSize = mSize[qRoot];
        //如果pRoot为根的树形集合含有的元素比qRoot的多
        if(pSize > qSize){
            //注意是少的索引的父节点指向多的
            mParents[qRoot] = pRoot;
            //注意此时mSize的改变,由于qRoot归并到了pRoot当中那么
            //需要加上相应数量的size,注意qRoot对应的size并没有改变
            mSize[pRoot] = pSize+qSize;
        }/*else if(pSize < qSize){//同理
            mParents[pRoot] = qRoot;
            mSize[qRoot] = pSize+qSize;
        }else{//如果两个相等那么就无所谓了,谁先合并到谁都可以.
            mParents[qRoot] = pRoot;
            mSize[pRoot] = pSize+qSize;
        }*/
        //然后就可以把等于的合入到大于或者小于的里面.
        else{//此处把小于和等于合到一块
            mParents[pRoot] = qRoot;
            mSize[qRoot] = pSize+qSize;
        }
    }
}
/**
 * 可以改进的方向:维护每个节点的下面层数,union的时候,将个数少的节点连接到个数多的节点上面
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS05 {
    private int[] mParents;
    //mRank[i]表示以i为根节点的集合所表示的树的层数
    private int[] mRank;
    private int mCount;
    public UFS05(int capacity){
        mCount = capacity;
        mParents = new int[mCount];
        mRank = new int[mCount];
        for (int i = 0; i < mCount; i++) {
            mParents[i] = i;
            //默认每个都是1:表示深度为1层
            mRank[i] = 1;
        }
    }
    //以下find和isConnected都用不到mRank.
    public int find(int p){
        if( p<0 || p>=mCount){
            //...做一些异常处理
        }
        while(p!=mParents[p]){
            p = mParents[p];
        }
        return p;
    }
    public boolean isConnected(int p,int q){
        return find(p)==find(q);
    }
    //找到p、q节点所在的树形集合的根节点,它的深度。然后把深度小的根节点合入到深度大的根节点当中
    public void union(int p,int q){
        //前两步不变
        int pRoot= find(p);
        int qRoot = find(q);
        if(pRoot == qRoot){
            return;
        }
        int pRank = mRank[pRoot];//初始事都是深度为1
        int qRank= mRank[qRoot];
        //如果p的深度比q的深度大.
        if(pRank > qRank){
            //注意是小的指向大的,也就是为小的重新读之
            mParents[qRoot] = pRoot;
            //此时把并不需要维护pRank,因为qRank是比pRank小的
            //也就是q更浅,它不会增加p的深度,只会增加去p的宽度
        }else if(pRank < qRank){
            mParents[pRoot] = qRoot;
            //同样的道理不需要维护qRank,p只会增加它的宽度
        }else{
            //当两个深度相同的时候,谁指向谁都可以,但是注意此时的深度维护
            //被指向的那个的深度需要加1.
            //此时让qRoot指向pRoot吧.
            mParents[qRoot] = pRoot;
            mRank[pRoot]++;
        }
    }
}

分析:
加权之后,树的高度可以大幅下降,find方法的效率就更高了,运行效率也提升了。

Weighted Quick-Union With Path Compression实现方式

我们来想一个更狠的,如果将树弄得特别扁平,只有 区域节点-其他节点 两层怎么样?
只需在每次find的时候,将第二层下面的节点指向上层的上层,多次find之后,就是一个二层树了。

/**
        带路径压缩的并查集
 * @author xuexiaolei
 * @version 2017年12月13日
 */
public class UFS06 {
    /**
     * 维护指向父节点的指针
     */
    private int[] mParents;
    private int mCount;

    /**
     * 初始化数组,默认每个节点都是区域头节点,即指针指向自己
     * @param capacity
     */
    public UFS06(int capacity){
        mCount = capacity;
        mParents = new int[capacity];
        for (int i = 0; i < capacity; i++) {
            mParents[i] = i;
        }
    }

    /**
     * 查找P节点的区域头结点
     * @param p
     * @return
     */
    public int find(int p){
        if (p<0 || p>=mCount){
            throw new RuntimeException("越界喽");
        }
        while (p != mParents[p])
        {
            // 将p节点的父节点设置为它的爷爷节点
            mParents[p] = mParents[mParents[p]];
            p = mParents[p];
        }
        return p;
    }

    public boolean isConnect(int a, int b){
        return find(a)==find(b);
    }

    /**
     * 联合,将a,b节点的区域头结点联合即可
     * @param a
     * @param b
     */
    public void union(int a, int b){
        int aRoot = find(a);
        int bRoot = find(b);
        if (aRoot == bRoot){
            return;
        }
        mParents[bRoot] = aRoot;
    }
}

复杂度分析

Algorithm Union Find
Quick-Find N 1
Quick-Union Tree height Tree height
Weighted Quick-Union lgN lgN
Weighted Quick-Union With Path Compression Very near to 1 (amortized) Very near to 1 (amortized)

你可能感兴趣的:(算法)