【數據結構】之並查集

1、引言

並查集是一種比較獨特的數據結構,它是由孩子節點指向父親節點的樹形結構。並查集能高效地解決鏈接問題(Connectivity Problem)java

  • 鏈接問題

鏈接問題.png

說明:上圖就是一個鏈接問題,左上角兩個點直接是否被鏈接,肉眼觀察就能給出確定的答案,若是問左上角的一點與右下角的一點是不是鏈接的,用肉眼就很難觀察了。並查集正是高效解決這一問題的數據結構。segmentfault

2、實現

  • 實現方式1(Quick Find)

Quick Find基本數據表示.png

/**
 * 基於數組模擬並查集初版(Quick Find)
 * 查詢操做的時間複雜度是:O(1)
 * 合併操做的時間複雜度是:O(n)
 */
public class UnionFind1 implements  UF {

    private int[] id;

    public UnionFind1(int size) {
        id = new int[size];
        for (int i = 0; i < id.length; i++) {
            id[i] = i;
        }
    }

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

    /**
     * 查找元素p所對應的集合編號
     * @param p 元素ID
     * @return
     */
    private int find(int p) {
        if (p < 0 && p >= id.length) {
            throw new IllegalArgumentException("p is out of bound");
        }
        return id[p];
    }

    /**
     * 查看元素p和元素q是否所屬同一個集合
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    /**
     * 合併元素p和元素q所屬的集合
     * @param p 元素ID
     * @param q 元素ID
     */
    @Override
    public void unionElements(int p, int q) {
        int pID = find(p);
        int qID = find(q);

        if (pID == qID) {
            return;
        }

        //將全部屬於p元素所在的集合編號覆蓋爲q元素所屬的集合編號
        for (int i = 0; i < id.length; i++) {
            if (id[i] == pID) {
                id[i] = qID;
            }
        }
    }
}

說明:以上的實現方式基於數組實現,數組中值不一樣則表示元素屬於不一樣的集合;查詢操做的時間複雜度是O(1),union操做的時間複雜度是O(n),由於須要遍歷數組中各個元素,判斷是否須要修改集合序號。數組

  • 實現方式2(Quick Union)

Quick Union基本數據表示.png

一系列合併操做後的數據表示.png

/**
 * 基於數組模擬並查集第二版
 */
public class UnionFind2 implements UF {

    private int[] parent;

    public UnionFind2(int size) {
        parent = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
        }
    }

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

    /**
     * 查找元素p所對應的根節點集合編號
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p
     * @return
     */
    private int find(int p) {
        if (p < 0 && p >= parent.length) {
            throw new IllegalArgumentException("p is out of bound");
        }
        while (p != parent[p]) {
            p = parent[p];
        }
        return p;
    }

    /**
     * 查看元素p和元素q是否所屬同一個集合
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @Override
    public void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);

        if (pRoot == qRoot) {
            return;
        }

        parent[pRoot] = qRoot;
    }
}

說明:Quick Union是基於數組的樹結構,查詢和合並操做的時間複雜度是O(h),跟樹的深度相關。相比Quick Find方式犧牲了點查詢性能,可是合併性能獲得了提高。數據結構

  • 基於集合樹的size優化

大集合樹指向小集合樹.png

小集合樹指向大集合樹.png

/**
 * 基於數組模擬並查集第三版
 * 基於size元素個數對並查集合並操做進行優化
 */
public class UnionFind3 implements UF {

    private int[] parent;

    /**
     * sz[i]表示以i爲根的集合中元素個數
     */
    private int[] sz;

    public UnionFind3(int size) {
        parent = new int[size];
        sz = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            sz[i] = 1;
        }
    }

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

    /**
     * 查找元素p所對應的根節點集合編號
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p
     * @return
     */
    private int find(int p) {
        if (p < 0 && p >= parent.length) {
            throw new IllegalArgumentException("p is out of bound");
        }
        while (p != parent[p]) {
            p = parent[p];
        }
        return p;
    }

    /**
     * 查看元素p和元素q是否所屬同一個集合
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @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 {
            parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

說明:在Quick Union的基礎上,爲了不樹的深度太大,極端狀況下樹會退化成鏈表形式,因此考慮了樹的size,小集合樹指向大集合樹,而避免了大集合樹指向小集合樹,有效避免了集合樹深度過大問題。dom

  • 集合樹的rank優化

image.png

小集合樹指向大集合樹.png

基於樹的rank深度優化.png

/**
 * 基於數組模擬並查集第四版
 * 基於rank深度對並查集合並操做進行優化
 */
public class UnionFind4 implements UF {

    private int[] parent;

    /**
     * rank[i]表示以i爲根的集合所表示的樹的層數
     */
    private int[] rank;

    public UnionFind4(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    /**
     * 查找元素p所對應的根節點集合編號
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p
     * @return
     */
    private int find(int p) {
        if (p < 0 && p >= parent.length) {
            throw new IllegalArgumentException("p is out of bound");
        }
        while (p != parent[p]) {
            p = parent[p];
        }
        return p;
    }

    /**
     * 查看元素p和元素q是否所屬同一個集合
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @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[pRoot] > rank[qRoot]) {
            parent[qRoot] = pRoot;
        } else {
            parent[qRoot] = pRoot;
            rank[pRoot] += 1;
        }
    }
}

說明:在基於樹size的優化基礎上,進一步優化,樹的size大,可是樹的深度不必定就大,比較合理的優化方式是基於樹的高度優化,高度小的樹指向高度大的樹。這樣合併操做之後儘可能不會增長樹的高度。ide

  • 並查集的路徑壓縮優化(Path Compression)

路徑壓縮.png

壓縮前.png

壓縮後.png

/**
 * 基於數組模擬並查集第四版
 * 基於路徑壓縮對並查集合並操做進行優化
 */
public class UnionFind5 implements UF {

    private int[] parent;

    /**
     * rank[i]表示以i爲根的集合所表示的樹的rank
     */
    private int[] rank;

    public UnionFind5(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    /**
     * 查找元素p所對應的根節點集合編號
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p
     * @return
     */
    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爲樹的高度
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @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[pRoot] > rank[qRoot]) {
            parent[qRoot] = pRoot;
        } else {
            parent[qRoot] = pRoot;
            rank[pRoot] += 1;
        }
    }
}

說明:在查詢操做時,會對樹路徑進行壓縮,將高度比較高的樹壓縮成高度較低的樹。提高查詢和合並的效率。性能

  • 並查集的路徑壓縮2

image.png

/**
 * 基於數組模擬並查集第四版
 * 基於路徑壓縮對並查集合並操做進行優化
 */
public class UnionFind6 implements UF {

    private int[] parent;

    /**
     * rank[i]表示以i爲根的集合所表示的樹的rank
     */
    private int[] rank;

    public UnionFind6(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    /**
     * 查找元素p所對應的根節點集合編號
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p
     * @return
     */
    private int find(int p) {
        if (p < 0 && p >= parent.length) {
            throw new IllegalArgumentException("p is out of bound");
        }
        if (p != parent[p]) {
            //路徑壓縮,遞歸找到p元素的根,而後直接將p元素掛到根上,最大限度的壓縮
            parent[p] = find(parent[p]);
        }
        return parent[p];
    }

    /**
     * 查看元素p和元素q是否所屬同一個集合
     * 時間複雜度爲O(h),h爲樹的高度
     * @param p 元素ID
     * @param q 元素ID
     * @return
     */
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @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[pRoot] > rank[qRoot]) {
            parent[qRoot] = pRoot;
        } else {
            parent[qRoot] = pRoot;
            rank[pRoot] += 1;
        }
    }
}

說明:路徑壓縮的另外一種方式,利用遞歸將樹壓縮到最理想的兩層。最大程度的提高路徑查詢和元素合併操做的性能。測試

3、簡單性能測試

  • 針對以上各類方式的並查集實現,簡單本地測試以下
import java.util.Random;

public class Main {

    public static void main(String[] args) {
        //UnionFind1(size=100000,m=10000) : 0.2367079 s
        testUnionFind1(100000, 10000);

        //UnionFind1(size=100000,m=100000) : 4.482173601 s
        testUnionFind1(100000, 100000);

        //UnionFind2(size=100000,m=10000) : 0.0016591 s
        testUnionFind2(100000, 10000);

        //UnionFind2(size=100000,m=100000) : 9.875075801 s
        testUnionFind2(100000, 100000);

        //UnionFind3(size=20000000,m=20000000) : 9.055730401 s
        testUnionFind3(20000000, 20000000);

        //UnionFind4(size=20000000,m=20000000) : 9.4252049 s
        testUnionFind4(20000000, 20000000);

        //UnionFind5(size=20000000,m=20000000) : 8.417531 s
        testUnionFind5(20000000, 20000000);

        //UnionFind6(size=20000000,m=20000000) : 7.6787675 s
        testUnionFind6(20000000, 20000000);
    }

    private static void testUnionFind1(int size, int m) {
        UnionFind1 uf1 = new UnionFind1(size);
        System.out.println("UnionFind1(size=" + size + ",m=" + m + ") : " + testUF(uf1, m) + " s");

    }

    private static void testUnionFind2(int size, int m) {
        UnionFind2 uf2 = new UnionFind2(size);
        System.out.println("UnionFind2(size=" + size + ",m=" + m + ") : " + testUF(uf2, m) + " s");
    }

    private static void testUnionFind3(int size, int m) {
        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("UnionFind3(size=" + size + ",m=" + m + ") : " + testUF(uf3, m) + " s");
    }

    private static void testUnionFind4(int size, int m) {
        UnionFind4 uf4 = new UnionFind4(size);
        System.out.println("UnionFind4(size=" + size + ",m=" + m + ") : " + testUF(uf4, m) + " s");
    }

    private static void testUnionFind5(int size, int m) {
        UnionFind5 uf5 = new UnionFind5(size);
        System.out.println("UnionFind5(size=" + size + ",m=" + m + ") : " + testUF(uf5, m) + " s");
    }

    private static void testUnionFind6(int size, int m) {
        UnionFind6 uf6 = new UnionFind6(size);
        System.out.println("UnionFind6(size=" + size + ",m=" + m + ") : " + testUF(uf6, m) + " s");
    }

    private static double testUF(UF uf, int m) {
        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();

        //m次合併操做
        for (int i = 0; i < m; i++) {
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a, b);
        }

        for (int i = 0; i < m; i++) {
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a, b);
        }

        long endTime = System.nanoTime();

        return (endTime-startTime) / 1000000000.0;
    }
}

4、其它數據結構

相關文章
相關標籤/搜索