【從蛋殼到滿天飛】JS 資料結構解析和演算法實現-紅黑樹(一)

哎喲迪奧發表於2019-03-21

思維導圖

前言

【從蛋殼到滿天飛】JS 資料結構解析和演算法實現,全部文章大概的內容如下: Arrays(陣列)、Stacks(棧)、Queues(佇列)、LinkedList(連結串列)、Recursion(遞迴思想)、BinarySearchTree(二分搜尋樹)、Set(集合)、Map(對映)、Heap(堆)、PriorityQueue(優先佇列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(雜湊表)

原始碼有三個:ES6(單個單個的 class 型別的 js 檔案) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)

全部原始碼已上傳 github,點選我吧,光看文章能夠掌握兩成,動手敲程式碼、動腦思考、畫圖才可以掌握八成。

本文章適合 對資料結構想了解並且感興趣的人群,文章風格一如既往如此,就覺得手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加原始碼,時間跨度也算將近半年時間了,希望對想學習資料結構的人或者正在學習資料結構的人群有幫助。

紅黑樹

  1. 紅黑樹是歷史上最有名的一種平衡的二叉樹
    1. 紅黑樹就是與紅色和黑色有關,
    2. 事實上在紅黑樹中對於每一個節點都附著了一個顏色,
    3. 這個顏色或者是紅色或者是黑色,
    4. 對於不同顏色的節點的意思
  2. 演算法導論中的紅黑樹定義
    1. 紅黑樹一定是一棵二分搜尋樹,
    2. 紅黑樹在二分搜尋樹的基礎上和 AVL 樹一樣新增了一些其它的性質
    3. 來保證它不會退化成為連結串列,
    4. 也就是來保證自己在某種程度上是一顆平衡的二叉樹。
    5. 這些性質分別是
    6. 每個節點或者是紅色的或者是黑色的;
    7. 根節點一定是黑色的;
    8. 每一個葉子節點(最後的空節點)是黑色的,這裡並不是左右子樹都為空的那個節點,
    9. 而是再向下遞迴一層的那個最後的空節點才管它叫做葉子節點,
    10. 也就是說每一個空節點如果也要給它上一種顏色的話,它是黑色的;
    11. 如果一個節點是紅色的,那麼它的孩子節點都是黑色的;
    12. 從任意一個節點到葉子節點,經過的黑色節點是一樣的。
    13. 以上五點性質就是演算法導論中對紅黑樹的一個定義,
    14. 通常一上來就告訴你這五個定義,然後說滿足這五個定義就叫做紅黑樹,
    15. 然後根據這些性質開始推倒出紅黑樹的更多的性質或者結論,
    16. 以至於最終的實現其實是很難理解到底什麼是紅黑樹,
    17. 這樣的一個介紹方法最大的問題是,
    18. 沒有介紹清楚對於紅黑樹這種資料結構來說,它到底是從哪兒來的,
    19. 到底為什麼要把哪個節點定義成有的是紅色有的是黑色,
    20. 而是直接給出了一個特別生硬的定義,告訴別人紅黑樹就是這個樣子。
  3. 大名鼎鼎的演算法 4 是紅黑樹的發明人寫的
    1. 這個教材中對紅黑樹的定義是最好的紅黑樹的介紹,
    2. 演算法 4 這本教材它的作者是一位老爺爺,叫做 Robert Sedgewick,
    3. 它正是紅黑樹的發明人,事實上紅黑樹的發明人不能完全歸功於這位老爺爺,
    4. 還有一位共同作家和他一起在當年發表了非常轟動的論文提出了紅黑樹這樣的資料結構,
    5. 這位老爺爺其實是大有來頭的,
    6. 在計算機領域有一位可以稱之為現代電腦科學之父的牛人叫做 Donald Knuth,
    7. 這位紅黑樹的發明人老爺爺正是這位牛人的弟子,也就是它的學生,
    8. 這位牛人高納德先生是現代電腦科學的前驅,
    9. 近乎沒有這位高納德先生就沒有現在學習的和演算法分析相關的各種複雜性理論相關的內容,
    10. 如果沒有這些內容,很有可能現在還不能非常客觀的去評價演算法的好壞,
    11. 如果不能客觀的評價演算法的好壞,其實也很難去進一步的優化自己的演算法,
    12. 而這位高納德先生他的一生做過了很多了不起的壯舉,
    13. 離現在時間點最近的他做過的一件非常重要的事情,就是在編纂一套書,
    14. 中文叫做計算機程式設計的藝術,這套書其實還沒有出版完,但是已經舉世矚目,
    15. 在微軟最輝煌的時候,比爾蓋茲就曾經說過,對於這套書,當時這套書只是出版了兩本,
    16. 比爾蓋茲就聲稱如果對於這兩本書你曾經讀過並且讀懂了的話,
    17. 那麼就可以直接把簡歷投給比爾蓋茲,可見這套書的價值以及它的分量,
    18. 這套書現在也已經有了中文的譯本,如果有興趣也可以找來挑戰一下,
    19. 是電腦科學領域尤其是演算法這個領域的一套非常重要的著作。
  4. 大名鼎鼎的演算法 4 這本教材中的介紹
    1. 這個教材中對紅黑樹的定義是最好的紅黑樹的介紹,
    2. 在演算法 4 這本書中對紅黑樹的介紹
    3. 直接繞開這些演算法導論中所說的一上來就擺出紅黑樹五個基本性質,
    4. 而是首先探索了另外一種平衡的樹,這種平衡的樹叫做 2-3 樹,
    5. 事實上紅黑樹與 2-3 樹本身是等價的,
    6. 如果理解了 2-3 樹與紅黑樹之間的這種等價關係以後就會發現,
    7. 其實紅黑樹並不難,不僅如此,之前在演算法導論中對於紅黑樹的五個基本性質,
    8. 你再去看時就會發現其實它們是非常自然的,所以要首先介紹 2-3 樹,
    9. 如果真正的能夠掌握 2-3 樹這種資料結構,不僅對理解紅黑樹有巨大的幫助,
    10. 同時對於理解在資料結構中另外一類非常重要的資料結構,
    11. 也就是通常用於磁碟儲存或者檔案系統資料庫相應的這種資料儲存的資料結構 B 類樹,
    12. 也是有巨大的幫助的。

2-3 樹

  1. 2-3 樹這種資料結構與之前大多數資料結構有一些不同的地方
    1. 之前介紹的大多數資料結構每一個節點相應的它的結構是一致的,
    2. 而 2-3 樹有所不同,首先它依然是滿足二分搜尋樹的基本性質,
    3. 但是在滿足這個基本性質的基礎上它並不是一種二叉樹,
    4. 事實上 2-3 樹有兩種節點,有一種節點可以存放一個元素,
    5. 還有一種節點可以存放兩個元素,如下圖,
    6. 一種節點和二分搜尋樹的節點一樣,另外一種節點是一個節點裡存放了兩個元素,
    7. 相應的它有三個孩子,這三個孩子分別在第一個元素的左側,兩個元素的中間,
    8. 第二個元素的右側,相應的就可以想到和二分搜尋樹一樣,對於二分搜尋樹來說,
    9. 這個節點左孩子值小於這個節點的值,右孩子的值大於這個節點的值,
    10. 那麼在二三樹中,對於這個可以存放兩個元素的節點它也滿足二分搜尋樹的性質,
    11. 左孩子的是小於這個節點中第一個元素的值,
    12. 中間的這個孩子的值是在第一個元素和第二個元素之間的,
    13. 相應的右孩子的值是比第二個元素還要大的,
    14. 這就是所謂的滿足二分搜尋樹的基本性質意義。
    // (a)     (b  c)
    // / \     / |  \
    複製程式碼
  2. 二三樹的特徵
    1. 二三樹的這兩種節點來說,每一個節點或者有兩個孩子或者有三個孩子,
    2. 這也就是 2-3 樹這個名稱的由來,
    3. 通常管這種存放一個元素又有兩個孩子的節點,在二三樹中叫做二節點,
    4. 相應的這種存放兩個元素又有三個孩子的節點,在二三樹中叫做三節點,
    5. 對於一棵二三樹來說,相應的有可能由這兩種節點組成,如下圖,
    6. 都滿足二分搜尋樹的性質,根據二三樹相應的性質可以推匯出,
    7. 如何在一棵二三樹中如何進行搜尋查詢,其實是非常簡單的,
    8. 和二分搜尋樹的基本思路也是一樣的,只不過搜尋的過程來到了一個三節點,
    9. 就要比較一下,如果小於三節點的左值就到三節點的左子樹中進行尋找,
    10. 大於三節點的右值那麼就到三節點的右子樹中進行尋找,
    11. 但是如果如果要尋找的值在三節點中間的話,
    12. 就到這個三節點的中間這棵子樹中繼續去尋找,
    13. 這是非常簡單的,對於二三樹來說它有一個非常重要的性質,
    14. 實際上這個性質是和二三樹本身插入元素的時候構建的方法是相關的,
    15. 這個性質就是二三樹是一棵絕對平衡的樹,
    16. 實際上二三樹是這個課程中到現在為止所學習過的唯一一棵絕對平衡的樹,
    17. 絕對平衡就是從根節點到任意一個葉子節點所經過的節點數量一定是相同的,
    18. 不平衡的二分搜尋樹,二分搜尋樹它可能會退化成連結串列、對於堆來說它雖然是完全二叉樹,
    19. 但是由於最後一層葉子節點有可能沒有填滿,所以不能叫做絕對平衡,線段樹是同理的,
    20. 它們的葉子節點分佈在最後一層的倒數第二層,所以也不能叫做絕對平衡,
    21. 關於 Trie 或者是並查集更不用說了,他們肯定不是平衡的樹結構,
    22. AVL 樹雖然叫做平衡二叉樹,
    23. 但是這個平衡二叉樹的定義是對於任意一個節點左右子樹的高度差是不超過一的,
    24. 所以是比這種絕對平衡的條件要寬鬆的,而對於二三樹來說,
    25. 它滿足對於任意一個節點來說,左右子樹的高度一定是相等的,
    26. 二三樹維持這種絕對的平衡是在新增節點的時候使用了一種機制來維護絕對平衡的,
    27. 理解新增節點的時候二三樹這種維護絕對平衡的機制
    28. 對理解紅黑樹它的運作機制是非常重要的。
    //              (   42   )
    //              /       \
    //        ( 17 33 )     (50)
    //       /    |   \     /  \
    //    (6 2)  (18) (37)(48)(66 88)
    複製程式碼

樹的絕對平衡性

  1. 二三樹是一種既有二節點又有三節點的滿足二分搜尋樹基本性質的這樣的一種資料結構
    1. 二三樹是一種可以保持絕對平衡的這樣的一種樹結構,
    2. 可以通過新增節點來檢視二三樹究竟是如何維持這種絕對的平衡,
    3. 理解二三樹維持這種絕對平衡之後就可以看到紅黑樹其實和二三樹是等價的。
  2. 二三樹的新增操作
    1. 首先你在一棵空樹新增一個新節點 42,那麼新節點將作為這棵樹的根節點 42,
    2. 這個根節點就是平衡的,然後你再新增一個新節點 37,
    3. 雖然根節點 42 左子樹都為空,但是這個新節點不會新增到這個空的位置,
    4. 而是會融合到這個根節點中,根節點本來是一個二節點,
    5. 但是通過融合操作,根節點變成了一個三節點,
    6. 此時這棵二三樹依然是平衡的,它依然只有一個節點,
    7. 只不過這個節點從二節點變成了三節點,同時裡面有兩個元素,
    8. 如果向這棵二三樹再新增一個節點 12,
    9. 按道理講因該新增進根節點(37, 42)的左子樹中去,
    10. 但是根節點(37, 42)的左子樹為空,而在二三樹中新增節點,
    11. 新的節點永遠不會去那個空的位置,只會和最後找到的葉子節點做融合,
    12. 那麼在這裡最後找到的葉子節點是根節點(37, 42)這樣的一個三節點,
    13. 在此時依然先進行一下融合,暫時形成一個四節點,
    14. 也就是容納了三個元素的節點,相應的它可以有四個孩子,
    15. 但是對於二三樹來說它不可以有四節點,它最多隻能有三節點,
    16. 也就是一個節點中容納兩個元素,有三個孩子,
    17. 對於這種四節點可以非常容易的直接將它分裂成一棵子樹,
    18. 也就是將一個四節點轉而變成了一個由三個二節點組成的一棵平衡的樹,
    19. 這樣一來就很像是一棵正常的二分搜尋樹,也可以把它理解成是一棵二三樹,
    20. 只不過每一個節點都是二節點,同時這棵樹依然保持著絕對的平衡,
    21. 從一個空樹開始新增節點,新增一個節點新增兩個節點新增三個節點
    22. 都能保持一個絕對的平衡,如果在這棵樹的基礎上再來新增一個節點 18,
    23. 對於 18 這個元素來說根節點是 37,所以需要將 18 新增到根節點 37 的左子樹中,
    24. 這是和二分搜尋樹是一致的,那麼對於它的左子樹 12 來說,
    25. 節點 18 是比節點 12 要大的,那麼就應該把節點 18 新增到節點 12 的右子樹中去,
    26. 此時節點 12 的右子樹已經是空了,在這種情況下,對於二三樹的新增來說,
    27. 它並不會像二分搜尋樹那樣新增到一個空位置上去,
    28. 而是和它最後找到的那個位置的葉子節點做一個融合,
    29. 這個葉子節點是節點 12,它是一個二節點,所以它還有空間融合成一個三節點,
    30. 這樣就不會破壞這個二三樹的性質,此時這棵二三樹依然保持著絕對的平衡,
    31. 如果再來新增一個新的節點 6,節點 6 比根節點 37 要小,
    32. 所以它需要新增到根節點 37 的左子樹中去,
    33. 對於這個左子樹是一個三節點(12, 18),
    34. 節點 6 比節點 12 還要小,所以它要新增到這個三節點的左子樹中去,
    35. 不過對於三節點的左子樹為空,由於對於二三樹新增節點來說,
    36. 不會把一個新的節點新增到一個空節點上去,
    37. 而是找到最後它新增的那個位置的葉子節點,和這個葉子節點做融合,
    38. 如果現在這個葉子節點是三節點的話,那麼就會暫時形成一個四節點,
    39. 之後對這個四節點再進行一個拆解,之前是對根節點是一個四節點進行拆解,
    40. 是拆解成一棵包含有三個二節點的子樹,但是現在對於這個葉子節點進行拆解,
    41. 拆解成一棵包含有三個二節點的子樹,那麼這棵二三樹就不是一棵絕對平衡的樹,
    42. 對於二三樹來說如果一個葉子節點它本身已經是一個三節點了,
    43. 新增了一個新的節點變成四節點的話,
    44. 那麼對於這個新的四節點拆解成三個二節點的形式之後,
    45. 這棵子樹它有一個新的根節點 12,這個節點 12 要向上去和上面的父親節點融合去,
    46. 對於節點 12 的父親節點是節點 37,是一個二節點,那麼就非常容易了,
    47. 節點 12 和節點 37 可以直接的融合成一個三節點,近而原來節點 12 的這個左右節點 6 和 18,
    48. 就可以變成這個新的三節點對應的左孩子和中間的這個孩子,
    49. 那麼這個二三樹經過剛才的操作,依然保持了絕對的平衡,
    50. 如果再新增一個新的元素 11,對於根節點(12, 37)來說比 12 要小,
    51. 所以要插入根節點的左子樹中去,根節點的左子樹是節點 6,節點 11 比節點 6 要大,
    52. 所以要插入到節點 12 的右子樹中去,不過節點 6 的右子樹已經為空了,
    53. 所以節點 11 和節點 6 直接做一個融合,變成了一個包含了 6 和 11 的三節點,
    54. 如果再新增一個新節點 5,對於節點 5 這個元素,從根節點(12, 37)開始,
    55. 節點 5 比根節點要小,所以還是要插入到根節點的左子樹中來,
    56. 對於左子樹的這個根節點(6, 11)它也是一個三節點,那麼節點 5 比節點 6 還要小,
    57. 所以節點 5 要插入到節點(6, 11)這個節點的左子樹中去,
    58. 不過節點(6, 11)這個三節點的左子樹已經為空了,所以對於二三樹的新增來說,
    59. 節點 5 和最後找到的這個葉子節點(6, 11)做融合,
    60. 不過這個葉子節點本身是一個三節點,所以首先暫時形成一個四節點,
    61. 對於這樣的一個四節點把他變成三個二節點的子樹,對於這樣的一棵子樹,
    62. 它的新的根節點,也就是節點 6,相應的融合到父親節點中去,
    63. 不過它的父親節點又是一個三節點,不過沒有關係,但是照樣做融合,
    64. 形成一個暫時的四節點,原來節點 6 的兩個子樹就可以掛接到融合後的四節點上,
    65. 成為這個四節點相應的兩棵子樹,依然沒有打破二分搜尋樹的性質,
    66. 不過對於這個四節點,由於在二三樹中最多隻能是三節點,
    67. 所以這個四節點還要繼續進行分裂,它的分裂方式和之前依然是一樣的,
    68. 把這一個四節點的三個元素化成是三個二節點,
    69. 由於之前的那個四節點本身也是根節點,
    70. 化成七個二節點的樹形狀之後,也就不需要繼續向上去融合新的父親節點了,
    71. 因為根節點已經到頭了,至此這次新增操作也完成了,
    72. 現在二三樹變成了所有的節點都是二節點的樣子,它依然滿足二三樹的性質,
    73. 於此同時它依然保持著絕對的平衡,這整個過程是一個很極端的情況,
    74. 新增的第一個節點其實是 42,之後新增了節點 37,之後新增了節點 18,
    75. 依此類推,第一次新增的節點是整棵樹中儲存元素中最大的那個元素,
    76. 然後後續新增的元素都比這個最大的元素要小,
    77. 換句話說其實一直向著最大的那個元素的左側去新增元素,
    78. 這樣的一種新增方式,如果你使用的是一種二分搜尋樹的話,
    79. 那麼早就已經非常偏斜了,不過在這個過程中,
    80. 二三樹整體的非常神奇的維護了整棵樹的絕對平衡的性質,
    81. 不管怎麼新增元素,二三樹整體都保持著平衡,
    82. 這就是二三樹可以維持一種絕對平衡的。

總結二三樹的新增操作

  1. 二三樹新增元素的過程整體上在二三樹中新增一個新元素的過程,

    1. 不會像二分搜尋樹那樣新增到一個空的節點的位置,
    2. 它一定是新增到最後搜尋到的那個葉子節點的位置,然後和它進行融合操作,
    3. 如果融合的葉子節點本身是一個二節點,融合之後就形成了一個三節點,
    4. 非常的容易,但是如果待融合的葉子節點它本身就是一個三節點,
    5. 其實也並不難,本來是節點(6, 12)這樣一個三節點,插入新的元素 2,
    6. 那麼就暫時臨時的形成這樣的一個四節點(2, 6, 12),它有三個元素四個孩子,
    7. 那麼對於這個四節點可以進行一下變形,變形之後形成了一個三個二節點的子樹,
    8. 對於這個子樹來說它有三個二節點,如果融合的這個三節點它本身就是一個根節點,
    9. 這樣做那就直接結束了,非常的容易,可是關鍵是通常融合的這個三節點,
    10. 它可能不是一個根節點而是一個葉子節點,在這種情況下,還需要進行處理,
    11. 其實這個處理過程也非常的簡單,整體來講分成兩種情況,
    12. 如果插入的這個三節點它是一個葉子節點,
    13. 同時這個樣子節點它的父親節點還是二節點的話,
    14. 首先暫時將這個元素插入到葉子節點中形成一個臨時的四節點,
    15. 那麼對這個臨時的四節點,依然是把它拆分成由三個二節點組成的這樣的一個子樹,
    16. 只不過在這種時候,
    17. 把它拆分三個二節點的子樹的時候會打破現在的這個二三樹的絕對的平衡,
    18. 那麼此時這個四節點變成的三個二節點組成的這個子樹的根節點
    19. 就需要向上進行一個融合,和它的父親節點進行一個融合,
    20. 如果它的父親節點是一個二節點,那麼這個融合就非常的簡單,
    21. 相當於就是讓它的父親節點變成一個新的三節點就好了,
    22. 融合後依然保持的絕對的平衡,
    23. 同時原來這個節點左右兩個孩子也可以正確的放到根節點的子樹中,
    24. 因為根節點是一個三節點了,三節點可以放三個孩子;
    25. 但是向上融合的父親節點是一個三節點的話,情況就會稍微複雜一些,
    26. 但是也非常的簡單,那麼在這種情況下,對於這個二三樹也是一樣,
    27. 先將一個四節點拆分成三個二節點的子樹,
    28. 對應這個子樹它的根節點依然進行向上的融合,
    29. 它向上融合以後,由於它的父親節點是一個三節點,
    30. 所以向上融合後它的父親節點變成了一個臨時的四節點,
    31. 原來這個節點的兩個子樹也成為了這個臨時四節點的孩子節點,
    32. 由於它的父親節點變成了一個臨時的四節點就需要進行拆分,
    33. 所以處理方式和之前一樣,
    34. 依然是把這個四節點拆分成由三個二節點組成的這樣的一個子樹,
    35. 拆成這樣的一個子樹之後,這棵子樹又有一個新的根節點,
    36. 這個節點繼續向上融合,
    37. 如果這個節點繼續向上融合它的父親節點是一個二節點,那麼非常容易,
    38. 融合成一個三節點就可以結束了,如果它的父親節點還是一個三節點,
    39. 那麼又形成了一個新的臨時的四節點,對這個新的臨時的四節點做同樣的操作,
    40. 一直向上推,直到最終到達了根節點的時候,就不需要向上進行融合了,
    41. 因為已經到頂了,那麼這一輪新增操作就此結束,
    42. 使用這樣的規則就可以保證二三樹這樣的一種樹結構可以維持絕對的平衡。
    //   新增元素4
    
    //   ( 6,  8 )           ( 6,    8 )           ( 4, 6, 8 )
    //   /   |   \    --->   /     |   \    --->    /  |  |  \
    // (2,5) (7) (12)      (2,4,5)(7) (12)        (2) (5)(7) (12)
    
    //               (6)
    //              /   \
    // --->       (4)   (8)
    //           /  \   /  \
    //         (2) (5) (7) (12)
    //
    複製程式碼
  2. 學習二三樹的這種資料結構的理解,

    1. 不僅可以幫助理解紅黑樹這種資料結構,
    2. 也可以對學習 B 類樹這種資料結構有巨大的幫助。

學習演算法的方式

  1. 學習抽象的演算法和資料結構的時候
    1. 有一個非常重要的學習方法,
    2. 其實就是用比較小的資料集對自己所設想的演算法
    3. 或者資料結構或者已經有的演算法或資料結構的程式碼進行模擬,
    4. 在這個模擬的過程中可以更深刻的理解這個邏輯整體的運轉過程,
    5. 很多時候做這樣的一個事情是比只是生對著程式碼去看去想要有效的多。

紅黑樹和 2-3 樹的等價性

  1. 紅黑樹這種資料結構本質上是和二三樹等價的

    1. 對於二三樹來說就是包含兩種節點的樹結構,
    2. 分別管他們叫做二節點和三節點,二節點中儲存這樣的一個元素,
    3. 三節點中儲存兩個元素,相應的二節點就有兩個孩子,
    4. 三節點就有三個孩子。
    5. 在之前所學習的所有的樹結構每一個節點中只能儲存一個元素,
    6. 那麼對於紅黑樹來講依然是這個樣子,
    7. 這是因為每一個節點中如果只保持含有一個元素的話,
    8. 那麼對這個節點的操作,包括對整個樹的操作在具體的程式碼編寫上會簡單很多,
    9. 基於這樣的一種方式也可以實現出和二三樹一樣的邏輯,
    10. 實際上這樣的一種資料結構就是紅黑樹。
    11. 對於二三樹中的二節點非常簡單,因為二節點本身這個節點中就存有一個元素,
    12. 這和之前所實現的二分搜尋樹中的節點是一致的,
    13. 在紅黑樹中相應的也是相應的這樣的一個節點,這個節點只存一個元素,
    14. 它有左右兩個孩子,這就表示一個二節點,非常的簡單,
    15. 但是複雜的是三節點,三節點是二三樹中特有的一種節點,
    16. 那麼對於三節點來說相應的它包含有兩個元素,可是現在想實現的這種樹結構中,
    17. 每一個節點只能存一個元素,那麼非常的簡單,由於這個三節點中有兩個元素,
    18. 只好使用兩個節點來表示這樣的一種三節點,
    19. 相應的表示的方法就是也和三節點差不多,也是將兩個節點平行的連線,
    20. 它本質上和二三樹中的三節點是一致的,相應的兩個元素分別存在一個節點中,
    21. 只是這兩個節點並行的連線在一起了,於此同時,
    22. 由於在二三樹中這個三節點是有大小關係的,節點中左邊的元素小於右邊的元素,
    23. 相應的在紅黑樹中並行連線的兩個節點,
    24. 那麼左邊的節點就應該是右邊節點的左孩子,
    25. 如下圖中的對比圖,在二分搜尋樹中就是這個樣子,
    26. 在二三樹中的一個三節點就等價成在這個二分搜尋樹中的樣子,
    27. 其中節點 b 是節點 c 的左孩子,因為 b 比 c 小,
    28. 為了表示 b 和 c 在原來的二三樹中是一個並列的關係,
    29. 是在一起存放在一個三節點中,那麼就在下圖的紅黑樹中以這樣的虛線邊來連線,
    30. 之前所實現的二分搜尋樹其實對邊這樣的一個物件是並沒有相應的類來表示的,
    31. 同樣在紅黑樹中也沒有必要對於每兩個節點它們之間所連線的這個邊
    32. 實現一個特殊的類來表示,可是這個虛線的邊應該是紅色的,
    33. 怎麼來表示這個特殊顏色的邊,由於每一個節點它只有一個父親,
    34. 換句話說每一個節點和他父親節點所相連線的那個邊的只有一根邊,
    35. 可以把這個邊的資訊存放在節點上,換句話說把節點 b 做一個特殊的標識,
    36. 比如讓它變成是紅顏色,
    37. 在這種情況下其實就表示節點 b 和父親節點相連線的那個邊是紅色的,
    38. 它是一個特殊的邊,實際上它的意思就是節點 b 和他的父親節點 c
    39. 在原來的二三樹中是一個並列的關係,是一起存放在一個三節點中的,
    40. 這樣一來就巧妙的把特殊的邊的資訊存放在了節點上,
    41. 也可以表示同樣的屬性或者說是同樣的邏輯,
    42. 而不需要新增特殊的程式碼來維護節點之間的這個邊相應的資訊,
    43. 到這裡就可以理解了,這個紅黑樹和二三樹是怎樣等價的,
    44. 實際上是進行了一個特殊的定義,
    45. 在二分搜尋樹上用這樣的兩種方式來表示出了對於二三樹來說
    46. 二節點和三節點這兩種節點,在這裡特殊的地方引入了一種叫做紅色的節點,
    47. 對於紅色的節點它的意思就是和他的父親節點一起表示
    48. 原來在二三樹中的三節點,現在這個二分搜尋樹相當於就有兩種節點了,
    49. 一種節點是黑節點,其實就是普通的節點,另外一種是紅色的節點,
    50. 也就是定義好的一種特殊節點,所以這種樹就叫做紅黑樹,
    51. 與此同時,通過這個定義就可以看到在紅黑樹中,
    52. 所有的紅色節點一定都是向左傾斜的,這個結論其實是定義出來的,
    53. 並不是推匯出來的,這是因為對於二三樹中的三節點來說,
    54. 在紅黑樹中選擇這樣的一種方式來進行表徵,
    55. 在其中會將三節點它左邊的那個元素當作右邊那個元素的左孩子來看待,
    56. 與此同時左邊的這個元素所在的節點是一個紅色的節點,
    57. 所以紅色的節點一定是向左傾斜的。
    // // 二三樹
    // (a)                        (b, c)
    // / \                        /    \
    
    // // 紅黑樹
    // [a]                       [b]---[c]
    // / \                       / \     \
    
    // 二分搜尋樹
    // {a}                          {c}
    // / \                          / \
    //                            {b}
    //                            / \
    複製程式碼
  2. 如果有興趣的話可以自己編寫一個二三樹

    1. 對於二三樹來說每一個節點中既可以存一個元素也可以存兩個元素,
    2. 如果真正深入的理解了二三樹所對應的邏輯,
    3. 是可以編寫出這樣的資料結構的,
    4. 雖然可能程式碼會複雜一些,但是應該是一個很好的鍛鍊的過程。
  3. 看圖理解紅黑樹與二三樹是等價的原因

    1. 在圖中,二三樹中有三個三節點,紅黑樹中有三個紅節點,
    2. 因為對於這三個三節點每一個三節點相應的在紅黑樹中一定會產生一個紅色節點,
    3. 如二三樹中節點(17,33)是一個三節點,在二三樹中就有一個紅節點{17},
    4. 其中這個紅節點{17}是黑節點[33]的左孩子,
    5. 這個紅節點代表的是與它父親相連線的邊是一條紅色的邊是一個特殊的邊,
    6. 這是因為紅節點{17}與黑節點[33]本身在二三樹中是合在一起的一個三節點,
    7. 由於對這些節點進行了一個紅色的標記,所以把它等價的看成是一棵二三樹,
    8. 如下圖中將紅黑樹繪製成類似二三樹這樣,
    9. 就可以很明顯的看出來紅色節點和它的父親節點對應了二三樹中的三節點,
    10. 通過這樣的例子就更深刻的理解了紅黑樹和二三樹是這樣一個等價的關係,
    11. 這是因為對於任意的一棵二三樹都可以使用這樣的規則把它轉化成一棵紅黑樹,
    12. 而且這個轉化的過程其實是非常簡單的。
    //       // 二三樹中的定義  小括號中一個元素為二節點、
    //                         小括號中兩個元素為三節點。
    //           (    42    )
    //           /          \
    //     (  17, 33  )     ( 50 )
    //     /     |   \      /    \
    // (6, 12)  (18) (37) (48)  (66, 88)
    //
    //      // 紅黑樹中的定義   中括號中為黑節點、
    //                         大括號中衛紅節點。
    //            [     42    ]
    //            /           \
    //        [ 33 ]         [  50 ]
    //        /    \          /    \
    //      {17}   [37]     [48]   [88]
    //      /  \                    /
    //    [12] [18]               {66}
    //    /
    //  {6}
    
    // 將紅黑樹 繪製 成類似二三樹的樣子
    //                      [     42    ]
    //                      /           \
    //           {17} —— [ 33 ]        [     50     ]
    //           /  \      \           /            \
    // {6} —— [12]  [18]  [37]      [48]   {66} —— [88]
    //
    複製程式碼
  4. 實現紅黑樹只需要基於二分搜尋樹對映來進行修改即可

    1. 這和實現 AVL 樹也是基於二分搜尋樹來進行修改是一樣的。
  5. 紅黑樹中的顏色

    1. 紅黑樹的節點需要增加一個 bool 型的變數來確定這個節點是紅色還是黑色,
    2. 可以直接給這兩個顏色設定為兩個常量的變數,初始化的時候直接使用這兩個變數即可,
    3. 要麼是 RED 要麼是 BLACK,這樣就很方便的了,每一個節點預設就是紅色的,
    4. 之所以是預設的,是因為你新增的這個節點永遠是和一個葉子節點進行一個融合,
    5. 在紅黑樹中紅色的節點就是代表著它和它的父親節點本身在二三樹中是在一起的
    6. 是融合在一塊兒的,所以在新建立一個節點的時候,也就是新新增了一個節點的時候,
    7. 由於新增的這個節點總是要和某一個節點進行融合,只不過融合之後還會做別的事情,
    8. 但不管怎樣,它都是先進行一個融合,
    9. 融合以後或者形成一個三節點或者形成一個臨時的四節點,
    10. 所以對應的在紅黑樹中新建立一個節點,這個節點的顏色總先將它設定成紅顏色,
    11. 代表它要在這棵紅黑樹中和所對應的那個等價的二三樹中對應的某一個節點進行融合,
    12. 這也是紅黑樹與二三樹之間的那種等價關係的體現。

程式碼示例

  1. MyRedBlackTree

    // 自定義紅黑樹節點 RedBalckTreeNode
    class MyRedBalckTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK;
       }
    
       // @Override toString 2018-11-25-jwl
       toString() {
          return (
             this.key.toString() +
             '--->' +
             this.value.toString() +
             '--->' +
             (this.color ? '紅色節點' : '綠色節點')
          );
       }
    }
    
    // 自定義紅黑樹 RedBlackTree
    class MyRedBlackTree {
       constructor() {
          MyRedBlackTree.RED = true;
          MyRedBlackTree.BLACK = false;
    
          this.root = null;
          this.size = 0;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (node === null) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 1、 -1 .'
                );
                break;
          }
       }
    
       // 新增操作 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 新增操作 遞迴演算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyRedBalckTreeNode(key, value);
          }
    
          // 將複雜的問題規模逐漸變小,
          // 從而求出小問題的解,從而構建出原問題的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else node.value = value;
    
          return node;
       }
    
       // 刪除操作 返回被刪除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 刪除操作 遞迴演算法 +
       recursiveRemove(node, key) {
          // 解決最基本的問題
          if (node === null) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 當前節點的key 與 待刪除的key的那個節點相同
             // 有三種情況
             // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 3. 當前節點左右子樹都有, 那麼又分兩種情況,使用前驅刪除法或者後繼刪除法
             //      1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點
             //      2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 開始嫁接 當前節點的左右子樹
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 將當前節點從根節點剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接後的新節點
                return predecessor;
             }
          }
       }
    
       // 刪除操作的兩個輔助函式
       // 獲取最大值、刪除最大值
       // 以前驅的方式 來輔助刪除操作的函式
    
       // 獲取最大值
       maximum(node) {
          // 再也不能往右了,說明當前節點已經是最大的了
          if (node.right === null) return node;
    
          // 將複雜的問題漸漸減小規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案
          return this.maximum(node.right);
       }
    
       // 刪除最大值
       removeMax(node) {
          // 解決最基本的問題
          if (node.right === null) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 開始化歸
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查詢操作 返回查詢到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操作 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含該key的元素的判斷值  +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回對映中實際的元素個數 +
       getSize() {
          return this.size;
       }
    
       // 返回對映中是否為空的判斷值  +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${
             this.size
          }, data = [ <br/><br/>`;
    
          // 以非遞迴的前序遍歷 輸出字串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    複製程式碼

紅黑樹的基本性質和複雜度分析

  1. 紅黑樹與二三樹是等價的

    1. 二三樹中的二節點和三節點這樣的兩種節點型別,
    2. 二節點在紅黑樹中都可以使用一個節點中儲存一個元素
    3. 它有兩個孩子的這樣的一個節點來表示,
    4. 只不過對於三節點來說只需要有兩個這樣的節點來表示,
    5. 其中還將一個節點標註成了紅色來表示它和它的父親節點作為兩個元素合在一起,
    6. 從而用來表示二三樹中的一個三節點,這樣一來就解釋了紅黑樹這個名字的由來,
    7. 包含了紅色的節點和黑色的節點這樣的兩種節點,理解了紅黑樹與二三樹本身是等價的時候,
    8. 就可以回過頭來,再來看一下演算法導論中的紅黑樹。
  2. 演算法導論中對紅黑樹的定義

    1. 每個節點或者是紅色的或者是黑色的;
    2. 根節點一定是黑色的;
    3. 每一個葉子節點(最後的空節點)是黑色的,這裡並不是左右子樹都為空的那個節點,
    4. 而是再向下遞迴一層的那個最後的空節點才管它叫做葉子節點,
    5. 也就是說每一個空節點如果也要給它上一種顏色的話,它是黑色的;
    6. 如果一個節點是紅色的,那麼它的孩子節點都是黑色的;
    7. 從任意一個節點到葉子節點,經過的黑色節點是一樣的。
  3. 看圖理解演算法導論中對紅黑樹的定義

    1. 第一條,紅黑樹中的確每個節點不是紅就是黑;
    2. 第二條,在二三樹中根節點要麼是二節點要麼是三節點,
    3. 但是在紅黑樹中,三節點是通過一個紅色節點和一個黑色節點以父子連線的方式來表示的,
    4. 如果根節點是二節點,對應的在紅黑樹中相應的這個根節點就是黑色節點,
    5. 如果在二三樹中根節點是三節點,包含有兩個元素,那麼相應的對應於紅黑樹來說,
    6. 和它等價的這個根節點就變成了紅色節點是黑色節點的左子樹這樣的情況,
    7. 也就是紅色節點變成了黑色節點的左孩子,在這種情況下紅黑樹中根節點還是黑色節點,
    8. 所以不管是在二三樹中,對應的根節點是二節點還是三節點,
    9. 對應到紅黑樹中一定是一個黑色的節點,
    10. 如果理解了在二三樹中這兩種節點所對應的紅黑樹中的表現形式,
    11. 這一條性質也是非常好理解的;
    12. 第三條,每一個葉子節點(最後的空節點)是黑色的,
    13. 這裡並不是左右子樹都為空的那個節點,與其說這是一條性質不如說它是一條定義,
    14. 它相當於是在說在紅黑樹中定義 空這樣的一個節點本身它是黑色的,
    15. 對於這樣的一個定義,可以寫一個函式,傳入一個節點,判斷這個節點是否為空,
    16. 如果為空的話就返回它是黑色的,本身也是這一條性質相應的一個邏輯體現,
    17. 與此同時這一條性質是與上一條性質相吻合的,
    18. 對於上一條的性質在紅黑樹中根節點一定是黑色的,
    19. 之前舉的例子都是紅黑樹它的根節點是存在的,
    20. 這個根節點是二三樹中的二節點形態或者是三節點形態,
    21. 對應到紅黑樹中都是一個黑色節點,不過還存在一種情況,
    22. 就是一棵空樹本身它本身也是一棵紅黑樹,對於一棵空樹來說,它本身是空,
    23. 相應的它的根節點也是空,第二條性質上說跟節點一定是黑色的,
    24. 在這裡 空 這個根節點也是黑色的,這就和第三條性質其實連在了一起,
    25. 在極端的情況下,整棵樹都是空的時候,這個空既是葉子節點又是根節點,
    26. 在這種情況下就定義它是一個黑色的節點;
    27. 第四條,如果如果在紅黑樹中一個節點本身是紅色的,
    28. 那麼這個紅色節點對應的它的孩子節點一定是一個黑色的節點,
    29. 在紅黑樹中只在它表示的是原來二三樹中的三節點時,
    30. 對應的三節點左側的這個元素所在的節點在紅黑樹中就是一個紅色的節點,
    31. 這個紅色的節點它的孩子節點對應的就是原先在二三樹中對應的左孩子或者是中間孩子,
    32. 不管它對應的是左孩子還是中間的孩子,
    33. 在原來的二三樹中相應的所連線的這個節點要麼是一個二節點要麼是一個三節點,
    34. 如果它連線的孩子節點是一個二節點那麼很顯然對應的就是紅黑樹中的黑色節點,
    35. 此時這個紅色的節點它的孩子節點一定是黑色的節點,
    36. 如果它連線的孩子節點是一個三節點的話,
    37. 那麼其實和之前看根節點是黑色的節點是一樣的,
    38. 它連線的雖然是一個三節點,但是所連線的這個三節點對應的紅黑樹的表現形式是
    39. 黑色節點為父紅色節點為左孩子,所以它就需要先連線上這個黑色節點,
    40. 再讓這個黑色節點連線上左側的紅色孩子節點,所以對於紅色節點的兩個孩子,
    41. 不管誰是三節點,首先接的一定是一個黑色的節點,
    42. 只不過這個黑色的節點它的左孩子又是一個紅色的節點,所以在這種情況下,
    43. 這個紅節點的孩子依然是一個黑色的節點,那麼整體上就有了第四條的性質,
    44. 如果一個節點是紅色的,那麼他的孩子節點都是黑色的,
    45. 這個結論對黑色的節點不成立,黑色的節點的右孩子一定是黑色的,
    46. 它的左孩子有可能是紅色的,它的原因和紅色的節點它的孩子節點是黑色的原因一致;
    47. 第五條,也就是最後一條性質,這條性質近乎是紅黑樹的核心,
    48. 也就是在一棵紅黑樹中從任意一個節點出發到葉子節點,經過的黑色節點一定是一樣多的,
    49. 這裡強調的是黑色節點是一樣多的,但是經過的紅色節點不一定是一樣多的,
    50. 核心還是因為紅黑樹和二三樹之間是一個等價的關係,二三樹是一顆絕對平衡的樹,
    51. 一棵絕對平衡的樹意味著從二三樹中的任意一個節點出發到葉子節點
    52. 所經過的節點數是一樣多的,這是因為二三樹是絕對平衡的,
    53. 所以所有的葉子節點都在同一層上,他們的深度是一致的,那麼從任意一個節點出發,
    54. 那麼任意一個節點它是有一個固定的深度的,
    55. 從這個節點向下到達它任意一個可以達到的葉子節點,相應的向下走的深度就是一樣的,
    56. 也就意味著它經過的節點數量是一樣的,在二三樹中有這樣的一個性質,
    57. 對應到紅黑樹中,其實就對應著它走過的黑色的節點是一樣多的,
    58. 這是因為在二三樹中無論是二節點還是三節點,
    59. 相應的轉換成紅黑樹中的節點表示的時候都會有一個黑色的節點,
    60. 所以從紅黑樹中任意一個節點出發,每經過一個黑色的節點,
    61. 其實就等於是一定經過了原來的二三樹中的某一個節點,
    62. 區別只是在於經過的這個黑色的節點如果它的左孩子是紅色的節點的話,
    63. 那麼相應的其實就是經過原來二三樹中的一個三節點,
    64. 那麼此時走到的這個黑節點就是走到了這個二三樹中的三節點的一半兒,
    65. 雖然說是走到了一半兒,但是也是經過了這個三節點,
    66. 所以由於二三樹中不管是二節點還是三節點,在紅黑樹中都一定有一個黑色的節點,
    67. 而在二三樹中,從任何一個節點到葉子節點經過的節點個數是一樣的,
    68. 相應的在紅黑樹中就變成了從任意一個節點到葉子節點,經過的黑色節點數量是一樣的,
    69. 可以在下圖中進行一下實驗,從任意一個節點出發,一直到一個葉子節點,
    70. 看看經過的葉子節點它的數目是一樣的,
    71. 可以結合在紅黑樹中從任意一個節點到葉子節點這個路徑是什麼樣子的,
    72. 然後對應到二三樹中相應的是什麼樣子的,
    73. 更進一步的深刻理解紅黑樹和二三樹之間的這個等價關係,
    74. 與此同時理解這條性質在紅黑樹中,
    75. 從任意一個節點到葉子節點經過的黑色節點是一樣的,
    76. 根節點肯定是任意節點中的一個節點,也就是說在紅黑樹中從根節點出發,
    77. 到任意一個葉子節點經過的黑色節點是一樣的,這本身就是紅黑樹的一個重要的性質。
    //      // 紅黑樹中的定義   中括號中為黑節點、
    //                         大括號中衛紅節點。
    // 將紅黑樹 繪製 成類似二三樹的樣子
    //                      [     42    ]
    //                      /           \
    //           {17} —— [ 33 ]        [     50     ]
    //           /  \      \           /            \
    // {6} —— [12]  [18]  [37]      [48]   {66} —— [88]
    
    //       // 二三樹中的定義  小括號中一個元素為二節點、
    //                         小括號中兩個元素為三節點。
    //           (    42    )
    //           /          \
    //     (  17, 33  )     ( 50 )
    //     /     |   \      /    \
    // (6, 12)  (18) (37) (48)  (66, 88)
    複製程式碼
  4. 紅黑樹是一個保持“黑平衡”的二叉樹

    1. 這個黑平衡是指對於從根節點開始搜尋,
    2. 一直搜尋到葉子節點所經歷的黑色節點的個數是一樣多的,
    3. 是黑色的一種絕對平衡的這樣的一種二叉樹,
    4. 這種黑平衡的二叉樹,嚴格意義上來講,不是平衡的二叉樹,
    5. 在 AVL 樹中對平衡二叉樹進行了嚴格的定義,
    6. 是指左右子樹的高度差不能夠超過一,
    7. 而對於紅黑樹來說是有可能打破平衡二叉樹的定義的,
    8. 換句話說,在紅黑樹中一個節點的左右子樹的高度差是有可能大於一的,
    9. 但是紅黑樹保持了一個看起來非常奇怪的性質,
    10. 就是它的左右子樹的黑色節點的高度差保持的絕對的平衡
    11. 它的本質其實是在於二三樹本身是一棵保持著絕對平衡的樹結構。
  5. 紅黑樹的複雜度分析

    1. 對於紅黑樹來說如果它的節點個數為 n 的話相應的它的最大的高度並不是 logn,
    2. 而是 2logn,這是因為在最次的情況下從根節點出發,一直到最深的那個葉子節點,
    3. 可能經過了 logn 這個級別的黑色節點,
    4. 同時每一個黑色節點它的左子樹又都是一個紅色的節點,
    5. 換句話說這條路徑上所對應的二三樹都是三節點,那麼這樣一來就有 logn 個紅色的節點,
    6. 所以它的最大高度是 2 倍的 logn,但是 2 這個數是一個常數,
    7. 所以放在複雜度分析的領域來講對於紅黑樹來說它的高度依然是 logn,
    8. 所以對應的時間複雜度就是O(logn)這個級別,
    9. 換句話說在一個紅黑樹中查詢一個元素從根節點出發,
    10. 依然是使用二分搜尋樹的方式去查詢這個元素,
    11. 相應的時間複雜度是O(logn)這個級別的,
    12. 雖然遍歷經歷的節點個數最多可能是 2 倍的 logn,
    13. 修改一個元素首先要查詢到這個元素再修改它,它的時間複雜度是O(logn)這個級別的,
    14. 新增一個元素和刪除一個元素也是在這棵紅黑樹上
    15. 從根節點出發向下在一條路徑上進行遍歷,它們的時間複雜度都是O(logn)級別的,
    16. 所以這就是紅黑樹不會像二分搜尋樹那樣退化成一個連結串列的具體原因,
    17. 對於紅黑樹增刪改查的操作相應的時間複雜度都是O(logn)這個級別的。
  6. 紅黑樹對比 AVL 樹的優缺點

    1. 由於紅黑樹的最大高度是 2 倍的 logn,這個高度其實會比 AVL 樹的最大高度要高,
    2. 所以其實在紅黑樹上進行元素的查詢相比 AVL 樹來說會慢一點,
    3. 雖然這二者都是O(logn)這個級別的,
    4. 但是這不影響紅黑樹成為一個非常重要的資料結構,
    5. 甚至比 AVL 樹還要重要還要常用,這背後的原因其實是在於對於紅黑樹來說,
    6. 新增元素和刪除元素這兩個操作相比於 AVL 樹來說要快速一些,
    7. 對於資料結構來說如果儲存的資料經常要發生這種新增或者刪除的變動,
    8. 相應的使用紅黑樹就是一個更好的選擇,但是如果在資料結構中,
    9. 儲存的這個資料近乎是不會動的話,只是建立好這個資料結構以後,
    10. 之後的主要操作只在於查詢的話其實 AVL 樹效能會高一點,
    11. 雖然這二者查詢的時間複雜度都是 O(logn)級別的,
    12. 對於紅黑樹和 AVL 樹這二者之間相應的這些效能比較還會具體的做實驗,
    13. 只有這樣才能夠直觀的看到這二者的差別。

紅黑樹新增新元素

  1. 在一般的面試中瞭解以上基本概念之後就可以應付大多數的面試問題

    1. 很少有真正的面試讓你從底層去實現一個紅黑樹,
    2. 大多數情況只需要瞭解什麼是紅黑樹,以及他的優缺點到底在哪裡,
    3. 它內部工作的原理到底是怎樣的,主要是這些概念性的問題,
    4. 如果在面試中面試官真的讓你白板程式設計一個紅黑樹,
    5. 可能面試官是稍微有一點在刁難你了,
    6. 像紅黑樹中新增一個元素這整個的過程其實相對是比較複雜的,
    7. 程式碼量也是比較大的,在面試這樣的一個環節中,短時間完成這樣一個複雜的邏輯,
    8. 更關鍵的是這個複雜的邏輯背後其實並不能特別的考察
    9. 你的演算法設計能力或者對資料結構的深入程度,
    10. 很多時候可能只是有沒有準備這部分的內容而已,
    11. 所以通常情況下認為要面試者去白板程式設計紅黑樹中某一個具體的操作
    12. 並不是一個明顯的面試問題,為了能夠更深入的理解紅黑樹,
    13. 所以還是要從底層對紅黑樹的一些基本操作進行一下程式設計。
  2. 紅黑樹與二三樹是等價的

    1. 在二三樹中新增新的元素,先查詢新新增的這個元素的位置,
    2. 在二三樹新增新的元素永遠不會在一個空的位置,
    3. 而會是找到的最後一個葉子節點進行融合,
    4. 如果你找到的最後一個葉子節點是是一個二節點的話,
    5. 那麼這個新的元素就會直接新增進這個新的節點,從而形成一個三節點,
    6. 這種情況非常的容易,如果找到的最後一個葉子節點是一個三節點,
    7. 那麼新新增的這個元素也是先融合進這個三節點,暫時形成一個四節點,
    8. 然後再對這個四節點進行分裂處理,就是分裂成三個二節點,也就是變成一顆子樹,
    9. 作為根節點的那個二節點會再向上與父節點進行融合,
    10. 如果父節點是一個二節點,那麼就會融合成一個三節點,
    11. 這樣一來整棵樹的高度還是沒有變,還是一棵絕對平衡的樹,
    12. 如果父節點是一個三節點,那麼就會融合成一個暫時的四節點,
    13. 那麼會對這個四節點再進行分裂處理,這時會再分裂成一棵子樹,
    14. 然後作為根節點的那個二節點會繼續向上進行融合,迴圈往復,
    15. 直到到達了這棵二三樹最頂層的根節點為止,因為無法再進行融合操作了,
    16. 整棵樹一定會是一棵絕對平衡的樹。
  3. 在紅黑樹中新增新節點

    1. 在二三樹中新增一個新的元素,首先都是把這個新的元素融合進二三樹已有的節點中,
    2. 之前有講過在紅黑樹中紅色的節點其實就是表示的是在二三樹中的
    3. 三節點裡兩個元素中最左側的那個元素,所以把它設計成紅色,
    4. 它代表的是這個節點和他的父親節點這兩個節點本身應該合在一起,
    5. 等價於二三樹中的一個三節點,正是因為這個原因,
    6. 在紅黑樹中新增新的元素的時候,這個新的元素所在的節點永遠讓它是一個紅色的節點,
    7. 這代表的是 等價於在二三樹中新增一個新的元素的時候,
    8. 這個新的元素永遠是首先要融合進一個已有的節點中,
    9. 在紅黑樹中新增一個紅色的節點之後有可能會破壞紅黑樹的基本性質,
    10. 之後再做相應的一些調整工作,讓它繼續維持紅黑樹的基本性質就好了,
    11. 所以對於紅黑樹中的節點新增了一個 color 屬性值,
    12. 那麼相應的紅黑樹中的節點的建構函式中預設讓這個 color 是等於 RED 的等於紅色的,
    13. 就是這個原因,在紅黑樹中,每當 new 一個新的節點的時候這個節點都是一個紅色的節點。
  4. 在紅黑樹中新增一個元素最初始的情況

    1. 最初始的情況就是整棵紅黑樹為空,新增一個節點 42,新增的這個節點預設是紅色,
    2. 這個節點會作為根節點,但是在紅黑樹中有一個非常重要的性質,
    3. 根節點必須是黑色的,那麼就需要做一件事情,就是讓根節點變成黑色的。
  5. 保持根節點為黑色的節點

    1. 新增重新給根節點賦值之後,就可以給根節點進行染色操作,直接將 color 設定為黑色。
  6. 新增操作的情況

    1. 已經有一個根節點 42,插入一個新節點 37,那麼這個節點是紅色的,
    2. 按照二分搜尋樹的新增原則,直接新增為根節點的左孩子,
    3. 此時依然滿足紅黑樹的定義,所以還是一棵紅黑樹。
    4. 假設根節點是 37,但是如果插入一個新節點 42,根據二分搜樹的新增原則,
    5. 節點 42 比節點 37 大,就會被新增為根節點的右孩子,
    6. 此時不滿足紅黑樹的定義了,因為在紅黑樹中定義了紅色節點只能放在左子樹的位置,
    7. 所以破壞了紅黑樹的性質,需要進行左旋轉。
  7. 左旋轉

    1. 此時的做法和在 AVL 樹中的操作是一樣的,需要做一次左旋轉,
    2. 通過左旋轉將新節點 42 變成根節點,節點 37 變成紅色節點,
    3. 然後新增為根節點的左孩子,操作過程如下圖,
    4. 讓節點 x 與其左子樹 T2 斷開連線,再讓節點 node 與右子樹 X 斷開連線,
    5. 讓節點 X 的左子樹與節點 node 進行連線,讓節點 node 的右子樹與 T2 連線,
    6. 有一個逆時針的旋轉過程,還有染色過程,如果原來 node 是黑色,那麼 x 也就是黑色,
    7. 如果原來 node 是紅色,那麼 x 也就是紅色,但是 node 成為了 x 的左孩子,
    8. 並且和 x 形成了一個三節點,所以 node 需要變成紅顏色的節點,
    9. 也許問題來了,如果原來 node 是紅色,x 後來也變成了紅色,
    10. 然後 node 還是紅色,node 與其父節點 x 一起組成了一個三節點,
    11. 在紅黑樹中三節點兩個元素都是紅色,那麼就違背了紅黑樹的定義,
    12. 可是左旋轉只是一個子過程,雖然在左旋轉之後有可能產生連續的兩個紅色節點,
    13. 但是左旋轉之後會將新的根節點 x 傳回去之後,在新增邏輯裡,會進行更多的後續處理,
    14. 這些後續處理會讓最終的二叉樹不會破壞紅黑樹的性質,
    15. 所以在左旋轉的過程中並不會去維護紅黑樹的性質,
    16. 左旋轉的作用只是讓這兩個節點對應成二三樹中的三節點。
    // 原來是這樣的 ,
    // 中括號為黑色節點,大括號為紅色節點,
    // 小括號只是參與演示,並不真實存在
    //         [37] node
    //         /  \
    //       (T1) {42} X
    //            /  \
    //         (T2)  (T3)
    
    //        // 進行左旋轉後
    //         [42] x
    //         /  \
    // node {37}  (T3)
    //      /  \
    //   (T1)  (T2)
    
    //   // 程式碼如此。
    //   node.right = x.left;
    //   x.left = node;
    //   // x.color = BLACK;
    //   x.color = node.color;
    //   node.color = RED;
    複製程式碼

程式碼示例

  1. MyRedBlackTree

    // 自定義紅黑樹節點 RedBalckTreeNode
    class MyRedBalckTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK;
       }
    
       // @Override toString 2018-11-25-jwl
       toString() {
          return (
             this.key.toString() +
             '--->' +
             this.value.toString() +
             '--->' +
             (this.color ? '紅色節點' : '綠色節點')
          );
       }
    }
    
    // 自定義紅黑樹 RedBlackTree
    class MyRedBlackTree {
       constructor() {
          MyRedBlackTree.RED = true;
          MyRedBlackTree.BLACK = false;
    
          this.root = null;
          this.size = 0;
       }
    
       // 判斷節點node的顏色
       isRed(node) {
          // 定義:空節點顏色為黑色
          if (!node) return MyRedBlackTree.BLACK;
    
          return node.color;
       }
    
       //   node                     x
       //  /   \     左旋轉         /  \
       // T1   x   --------->   node   T3
       //     / \              /   \
       //    T2 T3            T1   T2
       leftRotate(node) {
          const x = node.right;
    
          // 左旋轉過程
          node.right = x.left;
          x.left = node;
    
          // 染色過程
          x.color = node.color;
          node.color = MyRedBlackTree.RED;
    
          // 返回這個 x
          return x;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (node === null) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 1、 -1 .'
                );
                break;
          }
       }
    
       // 新增操作 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
          this.root.color = MyRedBlackTree.BLACK;
       }
    
       // 新增操作 遞迴演算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyRedBalckTreeNode(key, value);
          }
    
          // 將複雜的問題規模逐漸變小,
          // 從而求出小問題的解,從而構建出原問題的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else node.value = value;
    
          return node;
       }
    
       // 刪除操作 返回被刪除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 刪除操作 遞迴演算法 +
       recursiveRemove(node, key) {
          // 解決最基本的問題
          if (node === null) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 當前節點的key 與 待刪除的key的那個節點相同
             // 有三種情況
             // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 3. 當前節點左右子樹都有, 那麼又分兩種情況,使用前驅刪除法或者後繼刪除法
             //      1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點
             //      2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 開始嫁接 當前節點的左右子樹
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 將當前節點從根節點剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接後的新節點
                return predecessor;
             }
          }
       }
    
       // 刪除操作的兩個輔助函式
       // 獲取最大值、刪除最大值
       // 以前驅的方式 來輔助刪除操作的函式
    
       // 獲取最大值
       maximum(node) {
          // 再也不能往右了,說明當前節點已經是最大的了
          if (node.right === null) return node;
    
          // 將複雜的問題漸漸減小規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案
          return this.maximum(node.right);
       }
    
       // 刪除最大值
       removeMax(node) {
          // 解決最基本的問題
          if (node.right === null) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 開始化歸
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查詢操作 返回查詢到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操作 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含該key的元素的判斷值  +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回對映中實際的元素個數 +
       getSize() {
          return this.size;
       }
    
       // 返回對映中是否為空的判斷值  +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${
             this.size
          }, data = [ <br/><br/>`;
    
          // 以非遞迴的前序遍歷 輸出字串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    複製程式碼

相關文章