【從蛋殼到滿天飛】JS 數據結構解析和算法實現-並查集(一)

思惟導圖

前言

【從蛋殼到滿天飛】JS 數據結構解析和算法實現,所有文章大概的內容以下: Arrays(數組)、Stacks(棧)、Queues(隊列)、LinkedList(鏈表)、Recursion(遞歸思想)、BinarySearchTree(二分搜索樹)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(優先隊列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(哈希表)html

源代碼有三個:ES6(單個單個的 class 類型的 js 文件) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)git

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。github

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。算法

並查集 Union Find

  1. 並查集是一種很不同的樹形結構
    1. 以前的樹結構都是由父親指向孩子,
    2. 可是並查集是由孩子指向父親而造成的這樣的一種樹結構,
    3. 這樣一種奇怪的樹結構能夠很是高效的來解決某一類問題,
    4. 這類問題就是鏈接問題(Connectivity Problem),
    5. 並查集是一種能夠高效的回答鏈接問題的這樣的一種數據結構。
  2. 鏈接問題
    1. 給出一個圖中任意的兩點,
    2. 這兩點之間是否能夠經過一個路徑鏈接起來,
    3. 簡單的使用肉眼觀察距離很近的兩點,是能夠觀察出來的,
    4. 若是兩點的距離很遠,兩點之間隔着還有無數個點,
    5. 那麼你就很難用肉眼觀察出來它們是不是相連的,
    6. 此時就須要藉助必定的數據結構,
    7. 而並查集就是回答這種鏈接問題一個很是好一種數據結構。
  3. 並查集能夠很是快的判斷網絡中節點的鏈接狀態
    1. 這裏的網絡其實是一個抽象的概念,
    2. 不只僅是在計算機領域所使用的互聯網這樣的一個網絡,
    3. 最典型的一個例子,如社交網絡、微博、微信、facebook,
    4. 他們之間其實就是由一個一個的人做爲節點造成的一個網絡,
    5. 在這種時候就能夠把每兩個用戶之間是否是好友關係,
    6. 這樣的一個概念給抽象成兩個節點之間的邊,
    7. 若是能夠這樣的創建一個網絡的話,相應的就會產生鏈接問題,
    8. 好比兩個用戶 A 和 B,他們原本多是互不認識的,
    9. 那麼經過這個網絡是否有可能經過認識的人他認識的人,
    10. 這樣一點點的擴散,最終接觸到那個你原本徹底不認識的人,
    11. 這樣的一個問題其實就是在社交網絡中相應的鏈接問題。
  4. 網絡這樣的一種結構不只僅是用在社交網絡
    1. 不少信息網絡,好比說亞馬遜的商品、豆瓣兒的圖書、
    2. 或者音樂網站的一些音樂專輯,這些內容均可以造成節點,
    3. 節點之間均可以以某種形式來定義邊,從而造成一個巨大的網絡,
    4. 能夠在這樣的網絡中作很是多的事情,
    5. 好比交通系統、公交車、火車、飛機等航班與航線之間他們全都是網絡,
    6. 更不用提計算機的網絡,每個路由器都是一個節點,
    7. 其實網絡自己是一個應用很是普遍的概念,
    8. 在實際中處理的不少問題,把它抽象出來可能都是一個網絡上的問題,
  5. 在回答網絡中的節點的鏈接狀態這樣的一個問題的時候,
    1. 並查集就是一個很是強力的性能很是高效的數據結構,
    2. 並查集除了能夠高效的回答網絡中節點間的鏈接狀態的問題以外,
    3. 仍是數學中集合這種類的一個很好的實現,
    4. 若是你使用的集合主要的操做是在求兩個集合的並集的時候,
    5. 並查集中其實就是集合中的這樣的概念,
    6. 相應的查就是一個查詢操做。
  6. 對於並查集來講他們很是高效的來回答在網絡中兩個節點是否鏈接的問題
    1. 在一個網絡也是能夠兩個節點他們之間的路徑是怎樣的,
    2. 既然能夠求出兩個節點之間的路徑,其實就回答了鏈接的問題,
    3. 兩個節點之間若是存在一個路徑,那麼就必定是鏈接的,
    4. 若是這個路徑根本就不存在,那麼它確定是不鏈接的,
    5. 這樣的一個思路確定是正確的,
    6. 若是想要回答兩個節點之間的鏈接問題,
    7. 這個答案實際上是比回答兩個節點之間的路徑問題回答的內容要少的,
    8. 由於只須要返回 true 或者 false 就行了,
    9. 可是若是要問 A 和 B 之間的路徑是什麼的話,
    10. 那麼相應的就要獲得一個從 A 節點出發一步一步達到節點 B,
    11. 這樣一個具體的路徑,換句話說其實回答路徑問題的方式
    12. 來回答鏈接問題,那麼真正回答的內容是更加的多了,
    13. 這樣會致使結果消耗了一些額外的性能求出了當前不關心的內容,
    14. 那個內容就是 A 和 B 之間的具體路徑是什麼。
  7. 當你深刻的學習數據結構和算法
    1. 慢慢的就會發現不少問題都會存在這樣的狀況,
    2. 你徹底可使用一個複雜度更高的算法來把這個問題求解出來,
    3. 可是這個算法之因此複雜度比較高,
    4. 就是由於其實它求出了你問的那個問題並不關心的內容,
    5. 例如本身實現的堆,徹底可使用順序表示這樣的結構,
    6. 或者直接使用一個線性結構數組或鏈表,
    7. 而後保持這個線性結構中全部元素都是有序的,
    8. 堆這種結構每次都要取出最大或最小的那個元素,
    9. 使用這種順序表示是很是容易實現的,
    10. 但關鍵在於使用順序表示不只僅能夠很是高效的取出
    11. 那個最大的元素或者最小的元素,還能夠很是高效的取出
    12. 你存儲的第二大的元素或者第二小的元素,
    13. 而這些內容都是在應用堆這種數據結構的時候其實不關心的,
    14. 在系統調度的時候只關係那個優先級最大的任務,
    15. 在醫院醫生決定作手術的時候只關心那個當前優先級最高的患者,
    16. 爲他來準備手術,在涉及一個遊戲 AI 的時候,
    17. 當前控制的那個小機器人只能選擇一個對你威脅最大的敵人來攻擊,
    18. 因此在這種狀況下使用順序表示,它其實維護了不少這些應用中並
    19. 不須要的信息,爲了維護這些信息,它就須要有額外性能消耗,
    20. 要維持一個徹底的順序表示,在插入元素的時候時間複雜度是O(n)
    21. 這個級別的,之因此會產生這樣的狀況,
    22. 由於它不只僅是維護了當前數據中最大的或者最小的那個元素,
    23. 而堆這種數據結構除了你關心的那個最大的元素和最小的元素以外,
    24. 無論其它元素之間的順序,這才使得堆這種數據結構相比順序表來講,
    25. 總體大大提升了它的性能。
  8. 鏈接問題和路徑問題也是同樣的
    1. 雖然可使用求解路徑的思路來看 A 和 B 這兩個點是否鏈接,
    2. 可是因爲它回答了額外的問題,A 和 B 之間具體怎麼鏈接都回答出來了,
    3. 在不少時候並不關心 A 和 B 之間怎麼鏈接,只要看他是否鏈接,
    4. 此時並查集就是一種更好的選擇,對於這一點,
    5. 不少算法或者數據結構它們所解決的問題之間的差異是很是微妙的,
    6. 須要不斷的積累不斷的實踐,慢慢的瞭解每種不一樣的算法或者不一樣的數據結構,
    7. 它們所解決的那個問題以及具體的不一樣點在哪裏,
    8. 時間久了就能夠慢慢的很是快速的反應出對於某一些具體問題
    9. 最好的應該使用哪一種算法或者哪一種數據結構來進行解決。
  9. 具體來說對於並查集這種數據結構來講
    1. 存儲一組數據,它主要能夠支持兩個動做,
    2. union(p, q),也就是並的操做,傳入兩個參數 p 和 q,
    3. 而後在並查集內部將這兩個數據以及他們所在的集合給合併起來,
    4. 另一個動做就是isConnected(p, q)
    5. 也就是查詢對於給定的兩個數據,他們是否屬於同一個集合,
    6. 並查集主要支持這樣的兩種操做。
  10. 須要設計這樣的一種並查集接口
    1. 也就是說並查集也能夠有不一樣的底層實現,
    2. 經過實現不一樣的並查集,
    3. 能夠一點點的優化本身實現的並查集,
    4. 隨着你不斷的優化,
    5. 本身編寫的這個並查集在具體的解決鏈接問題的時候,
    6. 效率會愈來愈高。

並查集 簡單實現

  1. MyUnionFind
    1. unionElements(p, q):將這兩個數據以及他們所在的集合進行合併。
    2. isConnected(p, q):查詢兩個數據是否在同一個集合中。
    3. getSize():當前並查集一共考慮多少個元素
  2. isConnected 方法中傳入的 p 和 q 都是 int 型,
    1. 對於具體元素是誰,在並查集的內部並不關心,
    2. 在使用並查集的時候能夠將元素和一個數組相對應的數組索引作一個映射
    3. 至關於真正關心的是一個 id 爲 p 和 id 爲 q 這樣的兩個元素它們是否相連,
    4. 對於 id 爲 p 這樣的元素它具體對應的是什麼樣的一個元素並不關心。
  3. unionElements 方法中傳入的 p 和 q 都是 int 型。
  4. 向線段樹同樣,並不考慮添加一個元素或者刪除一個元素
    1. 考慮的是對於當下固定的元素來講,
    2. 進行並或者查這樣的兩個操做。

代碼示例

  1. MyUnionFind編程

    // 自定義並查集 UnionFind
    class MyUnionFind {
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       unionElements(q, p) {}
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       isConnected(q, p) {}
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {}
    }
    複製代碼

並查集 簡單實現 Quick Find

  1. 對於並查集主要實現兩個操做api

    1. union 操做將兩個元素合併在一塊兒變成在一個集合中的元素,
    2. isConnected 操做查看兩個元素是不是相連的
  2. 並查集的基本數據表示數組

    1. 能夠直接給每一個數據作一個編號,
    2. 0-9 就表示 10 個不一樣的數據,
    3. 這是一種抽象的表示,
    4. 具體這十個編號多是十我的或者是十部車或者是十本書,
    5. 這是由你的業務邏輯所決定的,
    6. 可是在並查集的內部只存 0-9 這是個編號,
    7. 它表示十個具體的元素
    8. 對於每個元素它存儲的是對應的集合的 ID。
    9. 例以下圖並查集一中編號 0-4 這五個數據它們所對應的 ID 爲 0,
    10. 編號爲 5-9 這五個數據它們所對應的 ID 爲 1,
    11. 不一樣的 ID 值就是不一樣的集合所對應的那個編號,
    12. 在並查集中就能夠表示爲 將這個十個數據分紅了兩個集合,
    13. 其中 0-4 這五個元素在一個集合中,5-9 這個五個元素在另外一個集合中。
    14. 若是是下圖並查集二中這樣子,
    15. 其中 0、二、四、六、8 這五個元素在一個集合中,
    16. 而 一、三、五、七、9 這五個元素在一個集合中,
    17. 在具體的編程中會把這樣的一個數組稱之爲 id,
    18. 經過這樣的一個數組就能夠很是容易的來回答所謂的鏈接問題,
    19. 在並查集圖二中,0 和 2 就是相鏈接的,
    20. 或者說 0 和 2 是同屬於一個集合的,由於他們所對應的 id 的值都是 0,
    21. 1 和 3 也屬於同一個同一個集合,由於他們所對應的 id 值都爲 1,
    22. 相應的能夠想象 1 和 2 都屬於不一樣的集合,由於他們對應的 id 值是不一樣的。
    // 並查集 一
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 0 0 0 0 1 1 1 1 1
    
    // 並查集 二
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 1 0 1 0 1 0 1 0 1
    複製代碼
  3. 使用 id 這樣的一個數組來存儲你的數據微信

    1. 是能夠很容易的回答 isConnected 的這個問題的,
    2. 只須要直接來看 p 和 q 這兩個值所對應的 id 值是否同樣就行了,
    3. 將查詢 p 或者 q 每一個元素背後所對應的那個集合的 id 是誰也
    4. 抽象成一個函數,這個函數就叫作 find,
    5. 只須要看find(p)是否等於find(q)就行了。
  4. 當你使用 find 函數進行操做的時候只須要O(1)的時間複雜度網絡

    1. 直接取出 id 這個數組所對應的這個數據的 Index 相應值便可,
    2. 因此對於這種存儲方式在並查集上進行 find 操做時是很是快速的,
    3. 這種並查集的方式一般稱爲 QuickFind,
    4. 也就是對於 find 這種操做運算速度是很是快的。
      // 並查集
      // 0 1 2 3 4 5 6 7 8 9
      // -------------------------------------
      // id 0 1 0 1 0 1 0 1 0 1
      複製代碼
  5. QuickFind 方式的並查集中實現 union數據結構

    1. 若是想要合併 1 和 4 這兩個索引所對應的元素,也就是union(1, 4)
    2. 1 所對應的集合的 id 是 1,4 所對應的集合的 id 是 0,
    3. 在這種狀況下將 1 和 4 這兩個元素合併之後,
    4. 其實 1 所屬的那個集合和 4 所屬的那個集合每個元素至關於也鏈接了起來,
    5. 原本 一、三、五、七、9 它們是鏈接在一塊兒的,0、二、四、六、8 它們是鏈接在一塊兒的,
    6. 而 1 和 4 並無鏈接起來,可是一旦你將 1 和 4 鏈接起來以後,
    7. 本來和 1 鏈接的其它元素以及本來和 4 鏈接的其它元素,
    8. 好比 5 和 2,它們其實也就都鏈接起來了,通過這樣的操做以後,
    9. 全部的奇數所表示的元素和全部的偶數所表示的元素它們所對應的集合
    10. 的 id 值應該都會變成同樣的,應該都是 0 或者都是 1,
    11. 具體取 0 仍是取 1 都是無所謂的,只要他們的值是同樣的就行了,
    12. 就會變成下圖union(1, 4)後的並查集,
    13. 具體實現是對整個 id 數組進行一遍循環,
    14. 在循環的過程當中將全部的 id 值等於 0 所對應的那個元素的 id 值都改寫成 1,
    15. 正是由於如此 QuickFind 方式的並查集實現的 union 的時間複雜度是O(n)
    16. 因此這個 union 操做須要改進,也就是建立一棵樹,這棵樹很是的奇怪,
    17. 是由孩子指向父親的,而當前實現的這個並查集只是用數組模擬了一下而已。
    // 並查集
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 1 0 1 0 1 0 1 0 1
    
    // 並查集 union(1, 4)以後的並查集
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 1 1 1 1 1 1 1 1 1 1
    複製代碼

代碼示例

  1. MyUnionFindOne

    // 自定義並查集 UnionFind 第一個版本 QuickFind版
    // isConnected 操做很快
    class MyUnionFindOne {
       constructor(size) {
          // 存儲數據所對應的集合的編號
          this.ids = new Array(size);
    
          // 模擬存入數據
          const len = this.ids.length;
          for (var i = 0; i < len; i++) this.ids[i] = i;
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(n)
       unionElements(q, p) {
          const qId = this.find(q);
          const pId = this.find(p);
    
          if (qId === pId) return;
    
          for (var i = 0; i < this.ids.length; i++)
             if (pId === this.ids[i]) this.ids[i] = qId;
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(1)
       isConnected(q, p) {
          return this.ids[q] === this.ids[p];
       }
    
       // 查找元素所對應的集合編號
       find(index) {
          if (index < 0 || index >= this.ids.length)
             throw new Error('index is out of bound.');
          return this.ids[index];
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.ids.length;
       }
    }
    複製代碼

並查集 簡單實現 Quick Union

  1. QuickFind 的方式實現的並查集查找速度很是快

    1. 可是一般在標準狀況下都是使用 QuickUnion 的方式實現並查集。
  2. QuickUnion 的方式實現並查集思路

    1. 將每個元素,看做是一個節點,而節點之間相鏈接造成了一個樹結構,
    2. 這棵樹和以前實現的全部的樹都不同,
    3. 在並查集上實現的樹結構是孩子指向父親,
    4. 例如節點 3 指向節點 2,那麼節點 2 就是這棵樹的根節點,
    5. 雖然節點 2 是一個根節點,可是它也有一個指針,這個指針指向的是本身,
    6. 在這種狀況下若是節點 1 要和節點 3 進行一個合併,
    7. 這個合併操做就是就是讓節點 1 的指針指向節點 3 指向的這棵樹的根節點,
    8. 也就是讓節點 1 去指向節點 2。
    9. 若是又有一棵樹 節點 7 和節點 6 都指向節點 5,節點 5 是這棵樹的根節點,
    10. 可是若是要節點 7 要和節點 2 作一下合併,
    11. 其實就是就是讓節點 7 所在的這棵樹的根節點也就是節點 5 去指向節點 2,
    12. 或者你是想讓節點 7 和節點 3 進行一下合併,
    13. 那麼的獲得的結果依然是這樣的,由於實際的操做是讓
    14. 節點 7 所在的這棵樹的根節點去指向節點 3 所在的這棵樹的根節點,
    15. 依然是節點 5 去指向節點 2,因此依然獲得相同的結果,
    16. 這就是實際實現並查集相應的思路。
    // (5) (2)
    // / \ | \
    // / \ | \
    // (6) (7) (3) (1)
    複製代碼
  3. QuickUnion 的方式實現並查集很是的簡單

    1. 由於每個節點自己只有一個指針,只會指向另一個元素,
    2. 而且這個指針的存儲依然可使用數組的方式來存儲,
    3. 這個數組就叫作 parent,
    4. parent[i]就表示第 i 個元素所在的那個節點它指向了哪一個元素,
    5. 雖說是指針,可是實際存儲的時候依然使用一個 int 型的數組就夠了,
    6. 這樣一來在初始化的時候parent[i] = i
    7. 也就是初始化的時候每個節點都沒有和其它的節點進行合併,
    8. 因此在初始化的時候每個節點都指向了本身,
    9. 在這種狀況下至關於 以 10 個元素爲例,並查集總體就是下圖這樣子,
    10. 每個節點都是一個根節點,它們都指向本身。
    11. 嚴格的來講這個並查集不是一棵樹結構,而是一個森林,
    12. 所謂的森林就是說裏面有不少的樹,在初始的狀況下,
    13. 這個森林中就有 10 棵樹,每棵樹都只有一個節點,
    14. 若是進行union(4, 3)操做,
    15. 那麼直接讓節點 4 的的指針去指向節點 3 就行了,
    16. 這樣的一個操做在數組中表示出來就是parent[4] = 3
    17. 那麼節點 4 它指向了節點 3,若是在進行union(3, 8)操做,
    18. 那麼就讓節點 3 的指針指向的那個元素指向節點 8,
    19. 那麼在數組中parent[3] = 8,再進行union(6, 5)操做,
    20. 那麼就讓節點 6 的指針指向的那個元素指向節點 5,
    21. 也就是parent[6] = 5,再進行union(9, 4)操做,
    22. 那麼就讓節點 9 的指針指向指向節點 4 這棵樹的根節點,
    23. 那麼在這裏就有一個查詢操做了,
    24. 那麼就要看一下 4 這個節點所在的根節點是誰,
    25. 這個查詢過程就是 節點 4 指向了節點 3,節點 3 又指向了節點 8,
    26. 而節點 8 本身指向了節點 8 也就是指向了本身,說明 8 是一個根節點,
    27. 那麼下面要作的事情就是讓 9 這個節點指向節點 8 就行了
    28. 也就是parent[9] = 8,之因此不讓節點 9 指向節點 4,
    29. 由於那樣的話就會造成一個鏈表,那麼樹總體的優點就體現不出來,
    30. 當你的節點 9 指向節點 8,下次你查詢節點 9 的根節點只須要進行一步查詢,
    31. 因此才讓parent[9] = 8,再進行union(2, 1)操做,
    32. 直接讓節點 2 指向節點 1 就行了,parent[2] = 1
    33. 再進行union(5, 0)操做,直接讓節點 5 指向節點 0 就行了,
    34. parent[5] = 0,再進行union(7, 2)操做,
    35. 因爲節點 2 指向節點 1,那麼節點 7 就要指向節點 1,
    36. parent[7] = 1
    37. 接下來進行一個稍微複雜一點的操做,進行union(6, 2)操做,
    38. 因爲節點 6 指向節點 5,而節點 5 指向節點 0,2 指向節點 1,
    39. 那麼就是讓節點 0 指向節點 1 了,因此parent[0] = 1
    40. 這樣的一種實現就是並查集一般真正的實現方式。
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // parent 0 1 2 3 4 5 6 7 8 9
    //
    // Quick Union
    // (0) (1) (2) (3) (4) (5) (6) (7) (8) (9)
    //
    // 一通以下操做
    // union(4, 3); // 4->3
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 3 3 5 6 7 8 9
    //
    // union(3, 8); // 3->8
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 6 7 8 9
    //
    // union(6, 5); // 6->5
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 5 7 8 9
    //
    // union(9, 4); // 4->3 3->8 因此 9->8
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 5 7 8 8
    //
    // union(2, 1); // 2->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 5 5 7 8 8
    //
    // union(5, 0); // 5->0
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 0 5 7 8 8
    //
    // union(7, 2); // 2->1 因此 7->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 0 5 1 8 8
    //
    // union(6, 2); // 6->5 5->0,2->1 因此0->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 1 1 1 8 3 0 5 1 8 8
    複製代碼
  4. QuickUnion 的方式實現並查集中的 union 操做的時間複雜度是O(h)

    1. 這個 h 是當前 union 的這兩個元素它所在的樹相應的深度大小,
    2. 這個深度的大小在一般的狀況下都比元素的個數 n 要小,
    3. 因此 union 的這個過程相對以前要快一些,
    4. 不過相應的代價就是 查詢的過程相應的時間複雜度依然是樹的深度大小,
    5. 因此就稍微犧牲了一些查詢時相應的性能,
    6. 不過因爲在一般狀況下這棵樹的高度是遠遠小於數據總量 n 的,
    7. 因此要讓合併和查詢這兩個操做都是樹的高度這個時間複雜度,
    8. 相應的在大多數運用中這個性能是能夠接受的,
    9. 固然目前實現的並查集仍是有很大的優化空間的。
  5. 這個版本的並查集雖然是使用數組來進行存儲的

    1. 可是它其實是一種很是奇怪的樹,這種樹是由孩子指向父親的。

代碼示例

  1. MyUnionFindTwo

    // 自定義並查集 UnionFind 第二個版本 QuickUnion版
    // Union 操做變快了
    // 還能夠更快的
    class MyUnionFindTwo {
       constructor(size) {
          // 存儲當前節點所指向的父節點
          this.forest = new Array(size);
    
          // 在初始的時候每個節點都指向它本身
          // 也就是每個節點都是獨立的一棵樹
          const len = this.forest.length;
          for (var i = 0; i < len; i++) this.forest[i] = i;
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(h) h 爲樹的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 不管哪棵樹往那棵樹上進行合併 都同樣,他們都是樹
          // 這裏是主樹節點上往次樹節點進行合併
          this.forest[primaryRoot] = this.forest[secondarRoot];
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(h) h 爲樹的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所對應的集合編號
       find(id) {
          if (id < 0 || id >= this.ids.length)
             throw new Error('index is out of bound.');
    
          // 不斷的去查查找當前節點的根節點
          // 根節點的索引是指向本身,若是根節點爲 1 那麼對應的索引也爲 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.ids.length;
       }
    }
    複製代碼

並查集 Quick Union 基於 Size 的優化

  1. 兩版並查集的比較
    1. 第二版的 QuickUnion 方式的並查集和
    2. 初版 QuickFind 方式的並查集在思路上有很是大的不一樣,
    3. 初版的並查集實際上就是使用數組來模擬每一個數據所屬的集合是誰,
    4. 第二版的並查集雖然也是使用數組進行數據關係的存儲,
    5. 但總體思路上和初版的並查集是大相徑庭的,
    6. 由於讓數據造成了一棵比較奇怪的樹結構,更準確的說是森林結構,
    7. 在這個森林中每一棵樹相應的節點之間的關係都是孩子指向父親的,
    8. 這樣一來能夠經過任意的節點很是容易的查詢到這棵樹相應的根節點是誰,
    9. 那麼相應的就知道了對於每個節點來講它所屬的集合編號是誰。
  2. 兩個版本的並查集的性能
    1. 第一個版本的並查集 QuickFind,
    2. isConnected:判斷兩個集合是否鏈接 對應時間複雜度是O(1)級別的,
    3. union:將兩個集合進行合併 對應時間複雜度是O(n)級別的。
    4. 第二個版本的並查集 QuickUnion,
    5. isConnected:判斷兩個集合是否鏈接 對應時間複雜度是O(h)級別的,
    6. union:將兩個集合進行合併 對應時間複雜度是O(h)級別的。
  3. 在測試算法性能時候
    1. 不少時候實際測試的結果不只僅和算法有關,
    2. 也和你使用的語言具體執行的時候底層運行的機制相關,
    3. 第一個版本的並查集 總體就是使用的一個數組,
    4. 合併的操做就是對一片連續的空間進行一次循環的操做,
    5. 比方說這樣的操做在 一些強類型的 語言的底層會有很是好的優化,
    6. 因此運行速度會很是快。
    7. 而第二個版本的並查集 查詢的過程實際上是不斷索引的過程,
    8. 它不是順次的不斷訪問一片連續的空間,它要在不一樣的地址之間進行跳轉,
    9. 所以它的速度就會相對的慢一些,
    10. 並且在第二個版本的並查集中 find 的複雜度是O(h)級別的,
    11. 不管是 isConnected 仍是 union 都須要進行調用,
    12. 也就是說在第二個版本的並查集中的 isConnected 時間複雜度要比
    13. 第一個版本的並查集的 isConnected 時間複雜度要高的,
    14. 也就是更加的慢一些。
    15. 在第二個並查集中,
    16. 當你 union 的次數變得很大的時候,實際上就是將更多的元素組合在了一個集合中,
    17. 因此你獲得的那棵樹很是的大,可能仍是一個退化的超長鏈表,
    18. 那麼它相應的深度可能就會很是的高,
    19. 這就會使得 isConnected 的操做時的消耗也會很是的高,
    20. 因此可能會讓第二個版本的並查集明明是O(h)級別的複雜度還比
    21. 第一個版本的並查集的O(n)級別的複雜度還要慢一些,
    22. 因此第二個版本的並查集仍是有很大的優化空間的。
  4. 優化第二個版本的並查集
    1. 這個優化空間主要在於,在進行 union 操做的時候,
    2. 就直接將 q 這個元素的根節點直接去指向了 p 這個元素的根節點,
    3. 可是沒有充分的考慮 q 和 p 這兩個元素它所在的那兩棵樹的特色是怎樣的,
    4. 若是不對要合併的那兩個元素所在的樹的形狀不去作判斷,
    5. 不少時候這個合併的過程會不斷的增長樹的高度,
    6. 甚至在一些極端的狀況下獲得的這棵樹是一條鏈表的樣子。
  5. 簡單的解決方案:考慮 size
    1. 去考慮當前這棵樹它總體有多少個節點,
    2. 也就是讓節點少的那棵樹去指向節點多的那棵樹,
    3. 這樣就高几率的讓造成的那棵樹它的深度相對的會比較低,
    4. 這個優化的思路實際上是很是簡單的。
    5. 並且確定不會退化爲一個鏈表,
    6. 由於能夠保證最後造成的那棵樹相對是比較淺的,
    7. 對於O(h)的時間複雜度來講,h 越小它的時間複雜就會越小,
    8. 這樣的簡單優化讓性能有了巨大的提高。
    9. 可是還能夠繼續進行優化。

代碼示例

  1. (class: MyUnionFindOne, class: MyUnionFindTwo, class: MyUnionFindThree, class: PerformanceTest, class: Main)

  2. MyUnionFindOne

    // 自定義並查集 UnionFind 第一個版本 QuickFind版
    // isConnected 操做很快
    class MyUnionFindOne {
       constructor(size) {
          // 存儲數據所對應的集合的編號
          this.ids = new Array(size);
    
          // 模擬存入數據
          const len = this.ids.length;
          for (var i = 0; i < len; i++) this.ids[i] = i;
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(n)
       unionElements(q, p) {
          const qId = this.find(q);
          const pId = this.find(p);
    
          if (qId === pId) return;
    
          for (var i = 0; i < this.ids.length; i++)
             if (pId === this.ids[i]) this.ids[i] = qId;
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(1)
       isConnected(q, p) {
          return this.ids[q] === this.ids[p];
       }
    
       // 查找元素所對應的集合編號
       find(index) {
          if (index < 0 || index >= this.ids.length)
             throw new Error('index is out of bound.');
          return this.ids[index];
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.ids.length;
       }
    }
    複製代碼
  3. MyUnionFindTwo

    // 自定義並查集 UnionFind 第二個版本 QuickUnion版
    // Union 操做變快了
    // 還能夠更快的
    class MyUnionFindTwo {
       constructor(size) {
          // 存儲當前節點所指向的父節點
          this.forest = new Array(size);
    
          // 在初始的時候每個節點都指向它本身
          // 也就是每個節點都是獨立的一棵樹
          const len = this.forest.length;
          for (var i = 0; i < len; i++) this.forest[i] = i;
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(h) h 爲樹的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 不管哪棵樹往那棵樹上進行合併 都同樣,他們都是樹
          // 這裏是主樹節點上往次樹節點進行合併
          this.forest[primaryRoot] = this.forest[secondarRoot];
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(h) h 爲樹的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所對應的集合編號
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不斷的去查查找當前節點的根節點
          // 根節點的索引是指向本身,若是根節點爲 1 那麼對應的索引也爲 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.forest.length;
       }
    }
    複製代碼
  4. MyUnionFindThree

    // 自定義並查集 UnionFind 第三個版本 QuickUnion優化版
    // Union 操做變快了
    // 還能夠更快的
    // 解決方案:考慮size 也就是某一棵樹從根節點開始一共有多少個節點
    // 原理:節點少的向節點多的樹進行融合
    // 還能夠更快的
    class MyUnionFindThree {
       constructor(size) {
          // 存儲當前節點所指向的父節點
          this.forest = new Array(size);
          // 以以某個節點爲根的全部子節點的個數
          this.branch = new Array(size);
    
          // 在初始的時候每個節點都指向它本身
          // 也就是每個節點都是獨立的一棵樹
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.branch[i] = 1; // 默認節點個數爲1
          }
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(h) h 爲樹的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 節點少的 樹 往 節點多的樹 進行合併,在必定程度上減小最終樹的高度
          if (this.branch[primaryRoot] < this.branch[secondarRoot]) {
             // 主樹節點上往次樹節點進行合併
             this.forest[primaryRoot] = this.forest[secondarRoot];
             // 次樹的節點個數 += 主樹的節點個數
             this.branch[secondarRoot] += this.branch[primaryRoot];
          } else {
             // branch[primaryRoot] >= branch[secondarRoot]
             // 次樹節點上往主樹節點進行合併
             this.forest[secondarRoot] = this.forest[primaryRoot];
             // 主樹的節點個數 += 次樹的節點個數
             this.branch[primaryRoot] += this.branch[secondarRoot];
          }
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(h) h 爲樹的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所對應的集合編號
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不斷的去查查找當前節點的根節點
          // 根節點的索引是指向本身,若是根節點爲 1 那麼對應的索引也爲 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.forest.length;
       }
    }
    複製代碼
  5. PerformanceTest

    // 性能測試
    class PerformanceTest {
       constructor() {}
    
       // 對比隊列
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比棧
       testStack(stack, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             stack.push(random() * openCount);
          }
    
          while (!stack.isEmpty()) {
             stack.pop();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比集合
       testSet(set, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          let arr = [];
          let temp = null;
    
          // 第一遍測試
          for (var i = 0; i < openCount; i++) {
             temp = random();
             // 添加劇復元素,從而測試集合去重的能力
             set.add(temp * openCount);
             set.add(temp * openCount);
    
             arr.push(temp * openCount);
          }
    
          for (var i = 0; i < openCount; i++) {
             set.remove(arr[i]);
          }
    
          // 第二遍測試
          for (var i = 0; i < openCount; i++) {
             set.add(arr[i]);
             set.add(arr[i]);
          }
    
          while (!set.isEmpty()) {
             set.remove(arr[set.getSize() - 1]);
          }
    
          let endTime = Date.now();
    
          // 求出兩次測試的平均時間
          let avgTime = Math.ceil((endTime - startTime) / 2);
    
          return this.calcTime(avgTime);
       }
    
       // 對比映射
       testMap(map, openCount) {
          let startTime = Date.now();
    
          let array = new MyArray();
          let random = Math.random;
          let temp = null;
          let result = null;
          for (var i = 0; i < openCount; i++) {
             temp = random();
             result = openCount * temp;
             array.add(result);
             array.add(result);
             array.add(result);
             array.add(result);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             if (map.contains(result)) map.add(result, map.get(result) + 1);
             else map.add(result, 1);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             map.remove(result);
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比堆 主要對比 使用heapify 與 不使用heapify時的性能
       testHeap(heap, array, isHeapify) {
          const startTime = Date.now();
    
          // 是否支持 heapify
          if (isHeapify) heap.heapify(array);
          else {
             for (const element of array) heap.add(element);
          }
    
          console.log('heap size:' + heap.size() + '\r\n');
          document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';
    
          // 使用數組取值
          let arr = new Array(heap.size());
          for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();
    
          console.log(
             'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n'
          );
          document.body.innerHTML +=
             'Array size:' +
             arr.length +
             ',heap size:' +
             heap.size() +
             '<br /><br />';
    
          // 檢驗一下是否符合要求
          for (let i = 1; i < arr.length; i++)
             if (arr[i - 1] < arr[i]) throw new Error('error.');
    
          console.log('test heap completed.' + '\r\n');
          document.body.innerHTML += 'test heap completed.' + '<br /><br />';
    
          const endTime = Date.now();
          return this.calcTime(endTime - startTime);
       }
    
       // 對比並查集
       testUnionFind(unionFind, openCount, primaryArray, secondaryArray) {
          const size = unionFind.getSize();
          const random = Math.random;
    
          return this.testCustomFn(function() {
             // 合併操做
             for (var i = 0; i < openCount; i++) {
                let primaryId = primaryArray[i];
                let secondaryId = secondaryArray[i];
    
                unionFind.unionElements(primaryId, secondaryId);
             }
    
             // 查詢鏈接操做
             for (var i = 0; i < openCount; i++) {
                let primaryRandomId = Math.floor(random() * size);
                let secondaryRandomId = Math.floor(random() * size);
    
                unionFind.unionElements(primaryRandomId, secondaryRandomId);
             }
          });
       }
    
       // 計算運行的時間,轉換爲 天-小時-分鐘-秒-毫秒
       calcTime(result) {
          //獲取距離的天數
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //獲取距離的小時數
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //獲取距離的分鐘數
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //獲取距離的秒數
          var seconds = Math.floor((result / 1000) % 60);
    
          //獲取距離的毫秒數
          var milliSeconds = Math.floor(result % 1000);
    
          // 計算時間
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 輸出耗時字符串
          result =
             day +
             '天' +
             hours +
             '小時' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 總毫秒數:' +
             result;
    
          return result;
       }
    
       // 自定義對比
       testCustomFn(fn) {
          let startTime = Date.now();
    
          fn();
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    }
    複製代碼
  6. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('UnionFind Comparison Area');
          // 十萬級別
          const size = 100000; // 並查集維護節點數
          const openCount = 100000; // 操做數
    
          // 生成同一份測試數據的輔助代碼
          const random = Math.random;
          const primaryArray = new Array(openCount);
          const secondaryArray = new Array(openCount);
    
          // 生成同一份測試數據
          for (var i = 0; i < openCount; i++) {
             primaryArray[i] = Math.floor(random() * size);
             secondaryArray[i] = Math.floor(random() * size);
          }
    
          // 開始測試
          const myUnionFindOne = new MyUnionFindOne(size);
          const myUnionFindTwo = new MyUnionFindTwo(size);
          const myUnionFindThree = new MyUnionFindThree(size);
          const performanceTest = new PerformanceTest();
    
          // 測試後獲取測試信息
          const myUnionFindOneInfo = performanceTest.testUnionFind(
             myUnionFindOne,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindTwoInfo = performanceTest.testUnionFind(
             myUnionFindTwo,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
    
          // 總毫秒數:24143
          console.log(
             'MyUnionFindOne time:' + myUnionFindOneInfo,
             myUnionFindOne
          );
          this.show('MyUnionFindOne time:' + myUnionFindOneInfo);
          // 總毫秒數:32050
          console.log(
             'MyUnionFindTwo time:' + myUnionFindTwoInfo,
             myUnionFindTwo
          );
          this.show('MyUnionFindTwo time:' + myUnionFindTwoInfo);
          // 總毫秒數:69
          console.log(
             'MyUnionFindThree time:' + myUnionFindThreeInfo,
             myUnionFindThree
          );
          this.show('MyUnionFindThree time:' + myUnionFindThreeInfo);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

並查集 Quick Union 基於 Rank 的優化

  1. 這個 rank 就是指樹的高度或樹的深度
    1. 之因此不叫作 height 和 depth,
    2. 是由於進行路徑壓縮的時候並不會維護這個 rank 了,
    3. rank 只在 union 中進行維護,
    4. 這個 rank 準確的來講只是一個粗略的排名或者序而已,
    5. 並非很準確的存儲了樹的高度或深度。
  2. rank 的優化是基於 size 優化的基礎上進行的
    1. 最好的優化方式是記錄每個節點的根節點的最大深度是多少,
    2. 這樣纔可以在合併的時候,
    3. 讓深度比較低的那棵樹向深度比較高的那棵樹進行合併,
    4. 這樣總體更加的合理,這樣的一種優化方案就稱之爲 rank 的優化,
    5. 這個 rank 依然可使用一個數組來進行記錄,
    6. 其中rank[i]表示根節點爲 i 的樹的高度是多少。
  3. rank 的優化性能其實和 size 優化的性能差不了多少
    1. 可是當數據量達到千萬這個程度的時候,
    2. 就會有一點差距了,差距也不是有點大,就一兩秒左右。
    3. 因此仍是有優化空間的。

代碼示例

  1. (class: MyUnionFindThree, class: MyUnionFindFour, class: PerformanceTest, class: Main)

  2. MyUnionFindThree

    // 自定義並查集 UnionFind 第三個版本 QuickUnion優化版
    // Union 操做變快了
    // 還能夠更快的
    // 解決方案:考慮size 也就是某一棵樹從根節點開始一共有多少個節點
    // 原理:節點少的向節點多的樹進行融合
    // 還能夠更快的
    class MyUnionFindThree {
       constructor(size) {
          // 存儲當前節點所指向的父節點
          this.forest = new Array(size);
          // 以以某個節點爲根的全部子節點的個數
          this.branch = new Array(size);
    
          // 在初始的時候每個節點都指向它本身
          // 也就是每個節點都是獨立的一棵樹
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.branch[i] = 1; // 默認節點個數爲1
          }
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(h) h 爲樹的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 節點少的 樹 往 節點多的樹 進行合併,在必定程度上減小最終樹的高度
          if (this.branch[primaryRoot] < this.branch[secondarRoot]) {
             // 主樹節點上往次樹節點進行合併
             this.forest[primaryRoot] = this.forest[secondarRoot];
             // 次樹的節點個數 += 主樹的節點個數
             this.branch[secondarRoot] += this.branch[primaryRoot];
          } else {
             // branch[primaryRoot] >= branch[secondarRoot]
             // 次樹節點上往主樹節點進行合併
             this.forest[secondarRoot] = this.forest[primaryRoot];
             // 主樹的節點個數 += 次樹的節點個數
             this.branch[primaryRoot] += this.branch[secondarRoot];
          }
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(h) h 爲樹的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所對應的集合編號
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不斷的去查查找當前節點的根節點
          // 根節點的索引是指向本身,若是根節點爲 1 那麼對應的索引也爲 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.forest.length;
       }
    }
    複製代碼
  3. MyUnionFindFour

    // 自定義並查集 UnionFind 第四個版本 QuickUnion優化版
    // Union 操做變快了
    // 還能夠更快的
    // 解決方案:考慮rank 也就是某一棵樹從根節點開始計算最大深度是多少
    // 原理:讓深度比較低的那棵樹向深度比較高的那棵樹進行合併
    // 還能夠更快的
    class MyUnionFindFour {
       constructor(size) {
          // 存儲當前節點所指向的父節點
          this.forest = new Array(size);
          // 記錄某個節點爲根的樹的最大高度或深度
          this.rank = new Array(size);
    
          // 在初始的時候每個節點都指向它本身
          // 也就是每個節點都是獨立的一棵樹
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.rank[i] = 1; // 默認深度爲1
          }
       }
    
       // 功能:將元素q和元素p這兩個數據以及他們所在的集合進行合併
       // 時間複雜度:O(h) h 爲樹的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 根據兩個元素所在樹的rank不一樣判斷合併方向
          // 將rank低的集合合併到rank高的集合上
          if (this.rank[primaryRoot] < this.rank[secondarRoot]) {
             // 主樹節點上往次樹節點進行合併
             this.forest[primaryRoot] = this.forest[secondarRoot];
          } else if (this.rank[primaryRoot] > this.rank[secondarRoot]) {
             // 次樹節點上往主樹節點進行合併
             this.forest[secondarRoot] = this.forest[primaryRoot];
          } else {
             // rank[primaryRoot] == rank[secondarRoot]
             // 若是元素個數同樣的根節點,那誰指向誰都無所謂
             // 本質都是同樣的
    
             // primaryRoot合併到secondarRoot上了,qRoot的高度就會增長1
             this.forest[primaryRoot] = this.forest[secondarRoot];
             this.rank[secondarRoot] += 1;
          }
       }
    
       // 功能:查詢元素q和元素p這兩個數據是否在同一個集合中
       // 時間複雜度:O(h) h 爲樹的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所對應的集合編號
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不斷的去查查找當前節點的根節點
          // 根節點的索引是指向本身,若是根節點爲 1 那麼對應的索引也爲 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:當前並查集一共考慮多少個元素
       getSize() {
          return this.forest.length;
       }
    }
    複製代碼
  4. PerformanceTest

    // 性能測試
    class PerformanceTest {
       constructor() {}
    
       // 對比隊列
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比棧
       testStack(stack, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             stack.push(random() * openCount);
          }
    
          while (!stack.isEmpty()) {
             stack.pop();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比集合
       testSet(set, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          let arr = [];
          let temp = null;
    
          // 第一遍測試
          for (var i = 0; i < openCount; i++) {
             temp = random();
             // 添加劇復元素,從而測試集合去重的能力
             set.add(temp * openCount);
             set.add(temp * openCount);
    
             arr.push(temp * openCount);
          }
    
          for (var i = 0; i < openCount; i++) {
             set.remove(arr[i]);
          }
    
          // 第二遍測試
          for (var i = 0; i < openCount; i++) {
             set.add(arr[i]);
             set.add(arr[i]);
          }
    
          while (!set.isEmpty()) {
             set.remove(arr[set.getSize() - 1]);
          }
    
          let endTime = Date.now();
    
          // 求出兩次測試的平均時間
          let avgTime = Math.ceil((endTime - startTime) / 2);
    
          return this.calcTime(avgTime);
       }
    
       // 對比映射
       testMap(map, openCount) {
          let startTime = Date.now();
    
          let array = new MyArray();
          let random = Math.random;
          let temp = null;
          let result = null;
          for (var i = 0; i < openCount; i++) {
             temp = random();
             result = openCount * temp;
             array.add(result);
             array.add(result);
             array.add(result);
             array.add(result);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             if (map.contains(result)) map.add(result, map.get(result) + 1);
             else map.add(result, 1);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             map.remove(result);
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比堆 主要對比 使用heapify 與 不使用heapify時的性能
       testHeap(heap, array, isHeapify) {
          const startTime = Date.now();
    
          // 是否支持 heapify
          if (isHeapify) heap.heapify(array);
          else {
             for (const element of array) heap.add(element);
          }
    
          console.log('heap size:' + heap.size() + '\r\n');
          document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';
    
          // 使用數組取值
          let arr = new Array(heap.size());
          for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();
    
          console.log(
             'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n'
          );
          document.body.innerHTML +=
             'Array size:' +
             arr.length +
             ',heap size:' +
             heap.size() +
             '<br /><br />';
    
          // 檢驗一下是否符合要求
          for (let i = 1; i < arr.length; i++)
             if (arr[i - 1] < arr[i]) throw new Error('error.');
    
          console.log('test heap completed.' + '\r\n');
          document.body.innerHTML += 'test heap completed.' + '<br /><br />';
    
          const endTime = Date.now();
          return this.calcTime(endTime - startTime);
       }
    
       // 對比並查集
       testUnionFind(unionFind, openCount, primaryArray, secondaryArray) {
          const size = unionFind.getSize();
          const random = Math.random;
    
          return this.testCustomFn(function() {
             // 合併操做
             for (var i = 0; i < openCount; i++) {
                let primaryId = primaryArray[i];
                let secondaryId = secondaryArray[i];
    
                unionFind.unionElements(primaryId, secondaryId);
             }
    
             // 查詢鏈接操做
             for (var i = 0; i < openCount; i++) {
                let primaryRandomId = Math.floor(random() * size);
                let secondaryRandomId = Math.floor(random() * size);
    
                unionFind.unionElements(primaryRandomId, secondaryRandomId);
             }
          });
       }
    
       // 計算運行的時間,轉換爲 天-小時-分鐘-秒-毫秒
       calcTime(result) {
          //獲取距離的天數
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //獲取距離的小時數
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //獲取距離的分鐘數
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //獲取距離的秒數
          var seconds = Math.floor((result / 1000) % 60);
    
          //獲取距離的毫秒數
          var milliSeconds = Math.floor(result % 1000);
    
          // 計算時間
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 輸出耗時字符串
          result =
             day +
             '天' +
             hours +
             '小時' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 總毫秒數:' +
             result;
    
          return result;
       }
    
       // 自定義對比
       testCustomFn(fn) {
          let startTime = Date.now();
    
          fn();
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    }
    複製代碼
  5. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('UnionFind Comparison Area');
          // 千萬級別
          const size = 10000000; // 並查集維護節點數
          const openCount = 10000000; // 操做數
    
          // 生成同一份測試數據的輔助代碼
          const random = Math.random;
          const primaryArray = new Array(openCount);
          const secondaryArray = new Array(openCount);
    
          // 生成同一份測試數據
          for (var i = 0; i < openCount; i++) {
             primaryArray[i] = Math.floor(random() * size);
             secondaryArray[i] = Math.floor(random() * size);
          }
    
          // 開始測試
          const myUnionFindThree = new MyUnionFindThree(size);
          const myUnionFindFour = new MyUnionFindFour(size);
          const performanceTest = new PerformanceTest();
    
          // 測試後獲取測試信息
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFourInfo = performanceTest.testUnionFind(
             myUnionFindFour,
             openCount,
             primaryArray,
             secondaryArray
          );
    
          // 總毫秒數:8042
          console.log(
             'MyUnionFindThree time:' + myUnionFindThreeInfo,
             myUnionFindThree
          );
          this.show('MyUnionFindThree time:' + myUnionFindThreeInfo);
          // 總毫秒數:7463
          console.log(
             'MyUnionFindFour time:' + myUnionFindFourInfo,
             myUnionFindFour
          );
          this.show('MyUnionFindFour time:' + myUnionFindFourInfo);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼
相關文章
相關標籤/搜索