算法與數據結構之並查集

主要介紹並查集算法實現以及相關優化。c++

paste image

並查集 Union Find

  1. 圖相關算法的實現。算法

  2. 一種不同的樹形結構數組

鏈接問題 Connectivity Problem

可視化的來看鏈接問題:bash

鏈接問題

左上右下是不是鏈接的呢?微信

意義:實際應用中的做用

  • 網絡中節點間的鏈接狀態網絡

    • 網絡是個抽象的概念:用戶之間造成的網絡
  • 社交網絡:Facebook中用戶a和b中的聯繫(好友關係)。是否能聯繫到。函數

  • 音樂電影書籍,多媒體之間造成網絡。性能

  • 互聯網網頁之間造成的網絡測試

  • 路由器和路由器之間造成的也是網絡優化

  • 道理交通,航班調度都是網絡

數學中的集合類實現

並就是實現並集。& 查詢

鏈接問題 & 路徑問題

比路徑問題要回答的問題少(路徑是什麼,鏈接問題只問有沒有連)

  • 和二分查找做比較:順序查找法順便回答了rank。和前面其餘元素的位置
  • 和select做比較:排好序回答問題更多。快排思路select回答問題更少
  • 和堆做比較:只關心最大最小。

除了回答問題自己以外是否是額外的回答了別的問題。頗有可能就存在
更高效的算法。:由於高效算法不須要回答額外的問題。

實現一個最簡單的並查集 Union Find

對於一組數據,主要支持兩個動做:

  • union( p , q )
  • find( p )

用來回答一個問題

  • isConnected( p , q )

最簡單的表示方式;
數組。0,1.

0-4 5-9

0-4是一組,5-9是一組。組內之間有聯繫,一組內的元素有相同的id

奇偶

奇數是一組,偶數是一組。

namespace UF1 {

    class UnionFind {

    private:
        int *id;
        int count;

    public:
        UnionFind(int n) {
            count = n;
            id = new int[n];
            //初始條件每一個元素都是一組
            for (int i = 0; i < n; i++)
                id[i] = i;
        }

        ~UnionFind() {
            delete[] id;
        }
		//傳入元素p,返回元素對應的id。
        int find(int p) {
            assert(p >= 0 && p < count);
            return id[p];
        }

        bool isConnected(int p, int q) {
            return find(p) == find(q);
        }

        //傳入兩個元素,並
        void unionElements(int p, int q) {

            //找到兩個元素的id
            int pID = find(p);
            int qID = find(q);

            //比較id
            if (pID == qID)
                return;

            for (int i = 0; i < count; i++)
                //從頭至尾的掃描時間複雜度O(n)
                if (id[i] == pID)
                    id[i] = qID;
        }
    };
}
複製代碼

Testhelper.h:

namespace UnionFindTestHelper{

    //n是數據量
    void testUF1( int n ){

		//
        srand( time(NULL) );
        UF1::UnionFind uf = UF1::UnionFind(n);

        time_t startTime = clock();
		
        //O(N*N)的時間複雜度
        for( int i = 0 ; i < n ; i ++ ){
            int a = rand()%n;
            int b = rand()%n;
            uf.unionElements(a,b);
            //O(n)
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = rand()%n;
            int b = rand()%n;
            uf.isConnected(a,b);
            //時間複雜度只有O(1)
        }
        time_t endTime = clock();

        cout<<"UF1, "<<2*n<<" ops, "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
    }
}
複製代碼

main.cpp:

int main() {

    int n = 100000;

    UnionFindTestHelper::testUF1(n);

    return 0;
}
複製代碼

運行結果:

UF1, 200000 ops, 32.3533 s
[Finished in 39.7s]
複製代碼

quick find 查找時只須要O(1)級別。可是並確很慢

並查集的另外一種實現思路

常規實現思路

將每個元素,看作是一個節點。

元素節點

每一個元素擁有一個指向父節點的指針。而後最上面的父節點指針指向本身。

Quick Union

數組存放父親

parent(i) = i;

初始狀態

union 3 4

union 3 8

union 6 5

union 9 4

要將9鏈接到4的根節點8上去。數組中:4-3-8-8 8是4的根節點。9指向8.
4和9鏈接在一塊兒:由於根相同。

成果

  • 其中6和2鏈接是6的根0和2的根1選取了1將0掛上。

代碼實現

namespace UF2{

    class UnionFind{

    private:
        int* parent;
        int count;

    public:
        UnionFind(int count){
            parent = new int[count];
            this->count = count;
            for( int i = 0 ; i < count ; i ++ )
                parent[i] = i;
        }

        ~UnionFind(){
            delete[] parent;
        }

        //不斷向上找父親
        int find(int p){
            assert( p >= 0 && p < count );
            while( p != parent[p] )
                p = parent[p];
            return p;
        }

        //看是否能找到一樣的根
        bool isConnected( int p , int q ){
            return find(p) == find(q);
        }

        //找到p的根,和q的根
        void unionElements(int p, int q){

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

            if( pRoot == qRoot )
                return;
        //把根掛到另外一個的根
            parent[pRoot] = qRoot;
        }
    };
}
複製代碼

運行結果:

UF1, 20000 ops, 0.246341 s
UF2, 20000 ops, 0.059387 s
複製代碼

當n大的時候,方法1更優了。

並查集的優化

問題1:

union 9,4 & union 4 9

union 9 4

9的元素少,將它指向4的根節點。造成的樹層數低。

// 咱們的第三版Union-Find
namespace UF3{

    class UnionFind{

    private:
        int* parent; // parent[i]表示第i個元素所指向的父節點
        int* sz;     // sz[i]表示以i爲根的集合中元素個數
        int count;   // 數據個數

    public:
        // 構造函數
        UnionFind(int count){
            parent = new int[count];
            sz = new int[count];
            this->count = count;
            for( int i = 0 ; i < count ; i ++ ){
                parent[i] = i;
                sz[i] = 1;
            }
        }

        // 析構函數
        ~UnionFind(){
            delete[] parent;
            delete[] sz;
        }

        // 查找過程, 查找元素p所對應的集合編號
        // O(h)複雜度, h爲樹的高度
        int find(int p){
            assert( p >= 0 && p < count );
            // 不斷去查詢本身的父親節點, 直到到達根節點
            // 根節點的特色: parent[p] == p
            while( p != parent[p] )
                p = parent[p];
            return p;
        }

        // 查看元素p和元素q是否所屬一個集合
        // O(h)複雜度, h爲樹的高度
        bool isConnected( int p , int q ){
            return find(p) == find(q);
        }

        // 合併元素p和元素q所屬的集合
        // O(h)複雜度, h爲樹的高度
        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];
            }
        }
    };
}
複製代碼

運行結果:

UF2, 200000 ops, 19.3316 s
UF3, 200000 ops, 0.0184 s
複製代碼

分析

  • 對於UF1來講,雖然isConnected只須要O(1)的時間, 但因爲union操做須要O(n)的時間;整體測試過程的算法複雜度是O(n^2)的
  • 對於UF2來講, 其時間性能是O(n*h)的, h爲並查集表達的樹的最大高度
    • 這裏嚴格來說, h和logn沒有關係, 不過你們能夠簡單這麼理解
    • 咱們後續內容會對h進行優化, 整體而言, 這個h是遠小於n的
    • 因此咱們實現的UF2測試結果遠遠好於UF1, n越大越明顯:)
  • 對於UF3來講, 其時間性能依然是O(n*h)的, h爲並查集表達的樹的最大高度
    • 但因爲UF3能更高几率的保證樹的平衡, 因此性能更優

基於rank的並查集優化

分析

上面合併4和2 依靠集合的size來決定誰指向誰並不徹底合理。根據層數才最合理。

基於rank的優化

用rank[i] 表示根節點爲i的樹的高度

namespace UF4{

    class UnionFind{

    private:
        int* rank;   // rank[i]表示以i爲根的集合所表示的樹的層數
        int* parent; // parent[i]表示第i個元素所指向的父節點
        int count;   // 數據個數

    public:
        // 構造函數
        UnionFind(int count){
            parent = new int[count];
            rank = new int[count];
            this->count = count;
            for( int i = 0 ; i < count ; i ++ ){
                parent[i] = i;
                rank[i] = 1;
            }
        }

        // 析構函數
        ~UnionFind(){
            delete[] parent;
            delete[] rank;
        }

        // 查找過程, 查找元素p所對應的集合編號
        // O(h)複雜度, h爲樹的高度
        int find(int p){
            assert( p >= 0 && p < count );
            // 不斷去查詢本身的父親節點, 直到到達根節點
            // 根節點的特色: parent[p] == p
            while( p != parent[p] )
                p = parent[p];
            return p;
        }

        // 查看元素p和元素q是否所屬一個集合
        // O(h)複雜度, h爲樹的高度
        bool isConnected( int p , int q ){
            return find(p) == find(q);
        }

        // 合併元素p和元素q所屬的集合
        // O(h)複雜度, h爲樹的高度
        void unionElements(int p, int q){

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

            if( pRoot == qRoot )
                return;

            // 根據兩個元素所在樹的元素個數不一樣判斷合併方向
            // 將元素個數少的集合合併到元素個數多的集合上
            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的值
            }
        }
    };
}
複製代碼

分析

  • 對於UF3來講, 其時間性能依然是O(n*h)的, h爲並查集表達的樹的最大高度,但因爲UF3能更高几率的保證樹的平衡, 因此性能更優

  • UF4雖然相對UF3進行有了優化, 但優化的地方出現的狀況較少,因此性能更優表現的不明顯, 甚至在一些數據下性能會更差,由於判斷更多了。

運行結果

2000000 ops, 0.313945 s
複製代碼

路徑壓縮(path Compression)

前面咱們都在優化union。其實Find咱們也能夠進行優化。因爲每一個節點存的都是它的父親節點,全部每一個節點均可以有無數個(多個)孩子。在search值的時候,對於沒有找到的根的節點,能夠往上挪一挪。

分析

好比咱們要find4

咱們將4的父親節點鏈接爲4的父親的父親(若是出現3就是根節點,也沒有關係,由於對於根節點來講,3的父親仍是3)

下面考慮4的parent:2 (此時跳過了3,跳2級是沒有問題的)

最後的結果:

修改 find函數

int find(int p){
assert( p >= 0 && p < count );

  // path compression 1
  while( p != parent[p] ){
    parent[p] = parent[parent[p]];
    p = parent[p];
  }
}
複製代碼

最優結果的代碼實現

//path compression 2, 遞歸算法
  if( p != parent[p] )
  parent[p] = find( parent[p] );
  return parent[p];
複製代碼

最後的狀況

  • 寫一個遞歸的函數:調用findx,返回的就是x節點的根。讓每一個parentx指向findx的結果。findx的結果也是Findparentx的結果。找x的時候,將x的Findparent的結果,指向父親的結果。

優化狀況並不明顯。甚至由於遞歸的消耗。因此理論最優不必定實際好。

通過並查集的優化,並查集的操做,時間複雜度近乎是O(1)的


-------------------------華麗的分割線--------------------

看完的朋友能夠點個喜歡/關注,您的支持是對我最大的鼓勵。

我的博客番茄技術小棧掘金主頁

想了解更多,歡迎關注個人微信公衆號:番茄技術小棧

番茄技術小棧
相關文章
相關標籤/搜索