【從蛋殼到滿天飛】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 (一個一個的工程)node

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

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。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 .'
                );
                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 .'
                );
                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;
       }
    }
    複製代碼
相關文章
相關標籤/搜索