並查集(一)並查集的幾種實現

概述

並查集是一種特別的數據結構,在解決連通性問題屢試不爽。如下代碼均爲java語言的實現java

並查集的做用先整體說一下算法

  • 一、將兩個元素聯通起來(union)起來,造成一個通路
  • 二、檢查任意兩個元素是不是連通的
  • 三、連通後,若是把連通的一組數當作一組,那麼還能記錄一共有多少組數
  • 四、固然也還能求組員數最大、最小的組的數量【經過計數變形】

對外基礎方法提供2個方法

  • public void join(int a, int b) 連通a節點和b節點
  • public boolean isJoined(int a, int b) 判斷任意兩個節點是不是連通的

內部須要一個輔助方法

來查詢任意節點的根節點,自己你能夠理解並查集是一顆樹,但這顆是反向尋找,並非像一般的樹,自頂向下dfs的,而是一直向上找尋根節點數據結構

  • private int findP(int x) 查詢x元素的根節點

快速查詢版本

將連通節點的父節點都設置爲相同節點(索引),查詢的時候 f(x)=parent[x],但更新比較麻煩,每次更新須要遍歷全部元素O(n)的複雜度,n是總的元素個數性能

/**
 * 並查集  快速查詢
 */
public class UFQuickFind {

    int[] parent;

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


    private int findP(int x) {//查詢操做,實際上是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接拋出異常

        return parent[x];   //直接返回 parent就能夠了,由於全部  鏈接 的節點parent值都相同
    }

    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            for (int i = 0; i < parent.length; i++) {
                if (parent[i] == ap)
                    parent[i] = bp; //修改爲相同的 parent
            }
        }
    }

    public boolean isJoined(int a, int b) { //兩個節點是不是  鏈接的
        return findP(a) == findP(b);
    }


}

快速合併版

合併的時候,a,b元素,直接讓將a的父親指向b的父親。【通俗一點 a節點的父親 認b節點的根節點作父親了。a節點認祖歸宗了】ui

/**
 * 快速合併
 */
public class UFQuickUnion {

    int[] parent;

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

    private int findP(int x) {//查詢操做,實際上是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接拋出異常

        while (parent[x] != x)//一直搜索到根節點
            x = parent[x];

        return x;
    }

    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            parent[ap] = bp;//讓其中一個節點的根節點 指向  另一個節點的根節點
        }
    }

    public boolean isJoined(int a, int b) { //兩個節點是不是  鏈接的
        return findP(a) == findP(b);
    }

}

基於每株節點數的合併

因爲快速合併的過程,合併的過程是隨機的,若是全部節點都合併到一塊兒,那麼最後這顆樹可能變成一個鏈表【就是接龍,成爲一條線】,經過節點數來改進,節點數少的接到節點數多的上面,這樣確定不會成一個鏈表code

public class UFNumsUnion {


    int[] parent;
    int[] nums;//節點數

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

    private int findP(int x) {//查詢操做,實際上是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接拋出異常

        while (parent[x] != x)//一直搜索到根節點
            x = parent[x];

        return x;
    }

    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            //a節點多,就將 b節點往a節點上合併,這樣的話能夠減小樹的高度
            if (nums[ap] > nums[bp]) {
                parent[bp] = ap;
                nums[ap] += nums[bp];    //根節點的節點數  增長了  被合併節點的節點數
            } else {
                parent[ap] = bp;
                nums[bp] += nums[ap];
            }
        }
    }


    public boolean isJoined(int a, int b) { //兩個節點是不是  鏈接的
        return findP(a) == findP(b);
    }

}

基於層數的實現

【基於每株節點數的合併】雖然已經挺好的了,但偶爾也會不太合理的狀況以下所示索引

A:      o
        / | \ \
       o  o  o o        5個節點,兩層
      
  
   B:    o
       / | 
      o  o              4個節點  3層
     /
    o

上面這種狀況按照基於節點數的合併,會獲得一個4層的樹(層的深度增長),而更合理的方式是讓A接到B上,這樣最後總體層數保持在3層。層數越小,那麼findP方法就會變快,而合併也由於調用findP方法也會加快。get

基於層數的實現,永遠讓層數矮的接到層數高的樹上io

public class UFRankUnion {

    int[] parent;
    int[] rank;//樹的層數
    int plant;  //一共有多少株樹

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


    private int findP(int x) {//查詢操做,實際上是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接拋出異常

        while (parent[x] != x)//一直搜索到根節點
            x = parent[x];

        return x;
    }


    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            plant--;
            //a的層數越高,就將層數少的合併到層數高的上面
            if (rank[ap] > rank[bp])
                parent[bp] = ap;
            else if (rank[ap] < rank[bp]) {
                parent[ap] = bp;
            } else {
                //相同狀況的話,隨便就能夠,但整體層高會增長1,畫一個相同層的樹合併一下就知道
                parent[ap] = bp;
                rank[bp]++;
            }
        }
    }

    public int getPlant() {
        return plant;
    }

    public boolean isJoined(int a, int b) { //兩個節點是不是  鏈接的
        return findP(a) == findP(b);
    }


}

路徑壓縮

咱們設想一下並查集的理想狀態,全部樹都是深度爲1的樹,這種理想情況的findP的時間複雜度爲O(1),大大提升了查詢性能,以下圖所示class

o             o
        / | \ \        / | \ 
       o  o  o o      o  o  o
       
     
       全部的樹都是以這種方式去組合的

可是咱們知道,若是A和B合併,層高是否是又變成2層了,如何才能讓層高再次恢復到1層呢。這裏路徑壓縮的一種方式是在查詢的時候,將查詢的節點不斷往上提升,直接接到根節點上

節點1  o         
           / | \       
   節點2  o  o  o
         /
 節點3  o 節點1

查詢節點3的時候,是否是讓節點3接到 節點2的父節點上 parent[節點3]= parent[節點2]

public class UFRankUnionCompressPath {


    int[] parent;
    int[] rank;//樹的層數
    int plant;  //一共有多少株樹

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

    private int findP(int x) {//查詢操做,實際上是查詢  根節點
        if (x < 0 || x >= parent.length)
            return -1;  //或者直接拋出異常

        while (parent[x] != parent[parent[x]])
        //若是parent[x]==parent[parent[x]] 說明這顆樹到了第二層,或者第一層
        //第一層 由於parent[x]=x因此有  parent[x]==parent[parent[x]]
        //第二層 由於第二層的parent是根節點 有: parent[x]= root  
        //因此有 parent[parent[x]]= parent[root]  而自己 parent[root]=root
        {
            // x = parent[x];  //
            parent[x] = parent[parent[x]]; //將下層節點往頂層提高,最終
        }
        return parent[x];
    }


    public void join(int a, int b) {
        int ap = findP(a);
        int bp = findP(b);
        if (ap != bp) {
            plant--;
            //a的層數越高,就將層數少的合併到層數高的上面
            if (rank[ap] > rank[bp])
                parent[bp] = ap;
            else if (rank[ap] < rank[bp]) {
                parent[ap] = bp;
            } else {
                //相同狀況的話,隨便就能夠
                parent[ap] = bp;
                rank[bp]++;
            }
        }
    }

    public int getPlant() {
        return plant;
    }

    public boolean isJoined(int a, int b) { //兩個節點是不是  鏈接的
        return findP(a) == findP(b);
    }
}

期待下一篇,並查集運用相關的算法題及題解

相關文章
相關標籤/搜索