徹底搞懂“紅黑樹”......

來自:掘金,作者:FiTeen
鏈接:https://juejin.im/post/5e509b27f265da57455b3f33
紅黑樹(Red Black Tree)是一種自平衡的二叉搜索樹(Self-balancing Binary Search Tree)。以前也叫做平衡二叉 B 樹(Symmetric Binary B-tree)。
預(yù)備知識

平衡二叉搜索樹
平衡二叉搜索樹(Balanced Binary Search Tree),英文簡稱 BBST。經(jīng)典常見的平衡二叉搜索樹是 AVL 樹和紅黑樹。
①二叉搜索樹
二叉搜索樹(Binary Search Tree)是二叉樹的一種,英文簡稱 BST。又稱為二叉查找樹、二叉排序樹。
它的特點是任何一個結(jié)點的值都大于其左子樹的所有結(jié)點的值,任何一個結(jié)點的值都小于其右子樹的所有結(jié)點的值。
②平衡

一棵二叉搜索樹平均時間復(fù)雜度可以認(rèn)為是樹的高度 O(h)。像左邊這棵,結(jié)點的左右子樹的高度接近,屬于一棵平衡二叉搜索樹,O(h) = O(logn);而右邊這棵,高度達到了最大,已經(jīng)退化成了鏈表,O(h)=O(n)。
③改進二叉搜索樹
當(dāng)二叉樹退化成鏈表時,性能是很低的,所以我們需要在結(jié)點的插入、刪除操作之后,想辦法讓二叉搜索樹恢復(fù)平衡(減小樹的高度)。
但是如果為了追求最理想的平衡,而增加了時間復(fù)雜度也不是很有必要,因此比較合理的方案就是:用盡量少的調(diào)整次數(shù)達到適度平衡。
由此引申出 AVL 樹的概念。
AVL 樹
AVL 樹是最早發(fā)明的自平衡二叉搜索樹之一,它取名自兩位發(fā)明家的名字:G.M.Adelson-Velsky 和 E.M.Landis。
平衡因子(Balance Factor):某結(jié)點的左右子樹的高度差。

舉例:8 的左子樹高度為 2,右子樹高度為 1,因此它的平衡因子為 1;5 的左子樹高度為 0,右子樹高度為 3,因此它的平衡因子為 -3;4 的左子樹高度為 2,右子樹高度為 4,因此它的平衡因子為 -2;

可以看到 AVL 樹具有以下特點:
每個結(jié)點的平衡因子只可能是 -1、0、1(如果絕對值超過 1,則認(rèn)為是失衡)
每個結(jié)點的左右子樹高度差不超過 1
搜索、插入、刪除的時間復(fù)雜度是 O(logn)
B 樹
這是一個簡單的 3 階 B 樹:

特點如下:
1 個結(jié)點可以存儲超過 2 個元素,可以擁有超過 2 個子結(jié)點
擁有二叉搜索樹的一些性質(zhì)
平衡,每個結(jié)點的所有子樹高度一致
比較矮
①m 階 B 樹的性質(zhì)(m ≥ 2)
根結(jié)點:1 ≤ x ≤ m - 1
非根結(jié)點:┌ m / 2 ┐ - 1 ≤ x ≤ m - 1
根結(jié)點:2 ≤ y ≤ m
非根結(jié)點:┌ m / 2 ┐ ≤ y ≤ m
②B 樹 VS 二叉搜索樹

B 樹和二叉搜索樹,在邏輯上是等價的
多代結(jié)點合并,可以獲得一個超級結(jié)點,且 n 代合并的超級結(jié)點,最多擁有 (2^n) 個子結(jié)點 (至少是 (2^n) 階 B 樹)
紅黑樹定義和性質(zhì)
每個結(jié)點是要么是紅色或黑色
根結(jié)點必須是黑色
葉結(jié)點(外部結(jié)點、空結(jié)點)是黑色
紅色結(jié)點不能連續(xù)(也就是,紅色結(jié)點的孩子和父親都是黑色)
對于每個結(jié)點,從該點至 nil(樹尾端,Java 中為 null 的結(jié)點)的任何路徑都包含所相同個數(shù)的黑色結(jié)點
紅黑樹與 B 樹的等價變換

根據(jù)上面的性質(zhì),可以畫出這樣一棵紅黑樹。接下來對紅黑樹做等價變換,即將所有的紅色結(jié)點上升一層與它的父結(jié)點放在同一行,這就很像一棵 4 階 B 樹,轉(zhuǎn)換效果如下圖所示。

紅黑樹與 4 階 B 樹(2-3-4樹)具有等價性
黑色結(jié)點與紅色子結(jié)點融合在一起,形成 1 個 B 樹結(jié)點
紅黑樹的黑色結(jié)點個數(shù)與 4 階 B 樹的結(jié)點總個數(shù)相等
紅黑樹的基本操作
旋轉(zhuǎn)操作是局部的。當(dāng)一側(cè)子樹的結(jié)點少了,向另一側(cè)“借”一些結(jié)點;當(dāng)一側(cè)子樹的結(jié)點多了,則“租”一些結(jié)點給另一側(cè)。

N-node:當(dāng)前結(jié)點
P-parent:父結(jié)點
S-sibling:兄弟結(jié)點
U-uncle:叔父結(jié)點(P 的兄弟結(jié)點)
G-grand:祖父結(jié)點(P 的父結(jié)點)
左旋
左旋指的是以某個結(jié)點作為支點(旋轉(zhuǎn)結(jié)點),其右子結(jié)點變?yōu)樾D(zhuǎn)結(jié)點的父結(jié)點,右子結(jié)點的左子結(jié)點變?yōu)樾D(zhuǎn)結(jié)點的右子結(jié)點,左子結(jié)點保持不變。

右旋
右旋指的是以某個結(jié)點作為支點(旋轉(zhuǎn)結(jié)點),其左子結(jié)點變?yōu)樾D(zhuǎn)結(jié)點的父結(jié)點,左子結(jié)點的右子結(jié)點變?yōu)樾D(zhuǎn)結(jié)點的左子結(jié)點,右子結(jié)點保持不變。

變色
變換規(guī)則
把父結(jié)點和叔父結(jié)點變?yōu)楹谏?/span>
把祖父結(jié)點變?yōu)榧t色
把指針定義到祖父結(jié)點
把父結(jié)點變?yōu)楹谏?/span>
把祖父結(jié)點變?yōu)榧t色
對祖父結(jié)點右旋
紅黑樹搜索
由于紅黑樹本來就是平衡二叉搜索樹,并且搜索也不會破壞樹的平衡,所以搜索算法也與平衡二叉搜索樹一致:

從根結(jié)點開始檢索,把根結(jié)點設(shè)置為當(dāng)前結(jié)點。
若當(dāng)前結(jié)點為空,返回 nil。
若當(dāng)前結(jié)點不為空,比較當(dāng)前結(jié)點 key 與搜索 key 的大小。
若當(dāng)前結(jié)點 key 等于搜索 key,那么該 key 就是搜索目標(biāo),返回當(dāng)前結(jié)點。
若當(dāng)前結(jié)點 key 大于搜索 key,把當(dāng)前結(jié)點的左子結(jié)點設(shè)置為當(dāng)前結(jié)點,重復(fù)步驟 2。
若當(dāng)前結(jié)點 key 小于搜索 key,把當(dāng)前結(jié)點的右子結(jié)點設(shè)置為當(dāng)前結(jié)點,重復(fù)步驟 2。
紅黑樹插入
定位插入的位置

從根結(jié)點開始檢索。
若根結(jié)點為空,那么插入結(jié)點設(shè)為根結(jié)點,結(jié)束。
若根結(jié)點不為空,那么把根結(jié)點設(shè)為當(dāng)前結(jié)點。
若當(dāng)前結(jié)點為 nil,返回當(dāng)前結(jié)點的父結(jié)點,結(jié)束。
若當(dāng)前結(jié)點 key 等于搜索 key,那么該 key 所在結(jié)點就是插入結(jié)點,更新結(jié)點的值,結(jié)束。
若當(dāng)前結(jié)點 key 大于搜索 key,把當(dāng)前結(jié)點的左子結(jié)點設(shè)置為當(dāng)前結(jié)點,重復(fù)步驟 4。
若當(dāng)前結(jié)點 key 小于搜索 key,把當(dāng)前結(jié)點的右子結(jié)點設(shè)置為當(dāng)前結(jié)點,重復(fù)步驟 4。
插入后實現(xiàn)自平衡
總結(jié)一下紅黑樹插入可能出現(xiàn)的所有場景:

將插入結(jié)點設(shè)為將要替換結(jié)點的顏色
更新當(dāng)前結(jié)點的值為插入結(jié)點的值
由紅黑樹性質(zhì) 4 可知:紅色結(jié)點不能連續(xù)。那么此時該插入子樹的紅黑層數(shù)的情況是:黑-紅-紅。顯然最簡單的處理方式就是將其改為:紅-黑-紅。

將父結(jié)點和叔父結(jié)點變?yōu)楹谏?/span>
將祖父結(jié)點變?yōu)榧t色
將祖父結(jié)點設(shè)置為當(dāng)前插入結(jié)點
場景 4.2.1:插入結(jié)點是左子樹

將父結(jié)點變?yōu)楹谏?/span>
將祖父結(jié)點變?yōu)榧t色
將祖父結(jié)點右旋
這種場景顯然可以轉(zhuǎn)換為 4.2.1。

將父結(jié)點進行左旋
將父結(jié)點設(shè)為插入結(jié)點,得到場景 4.2.1
進行場景 4.2.1 的處理
場景 4.3.1:插入結(jié)點是左子樹

將父結(jié)點變?yōu)楹谏?/span>
將祖父結(jié)點變?yōu)榧t色
對祖父結(jié)點進行左旋
場景 4.3.2:插入結(jié)點是右子樹

將父結(jié)點進行右旋
將父結(jié)點設(shè)置為插入結(jié)點,得到場景 4.3.1
進行場景 4.3.1 的處理
下面舉個例子,往一棵紅黑樹中插入元素,整棵樹的變換如下圖所示:

紅黑樹刪除
定位刪除的位置
刪除后實現(xiàn)自平衡
若刪除結(jié)點無子結(jié)點,直接刪除即可。
若刪除結(jié)點只有一個子結(jié)點,用子結(jié)點替換刪除結(jié)點。
若刪除結(jié)點有兩個子結(jié)點,用**后繼結(jié)點(大于刪除結(jié)點的最小結(jié)點)**替換刪除結(jié)點。
具體應(yīng)用,可以借助這張圖理解:

在場景三情況下:刪除結(jié)點用后繼結(jié)點,如果后繼結(jié)點有右子結(jié)點,那么相當(dāng)于轉(zhuǎn)換為場景二,否則轉(zhuǎn)為場景一。


R:替換結(jié)點
P:替換結(jié)點的父結(jié)點
S:替換結(jié)點的兄弟結(jié)點
SL:兄弟結(jié)點的左子結(jié)點
SR:兄弟結(jié)點的右子結(jié)點
灰色:結(jié)點顏色可能是紅色,也可能是黑色

若兄弟結(jié)點是紅結(jié)點,那么根據(jù)紅黑樹性質(zhì) 4,兄弟結(jié)點的父結(jié)點和子結(jié)點肯定為黑色,按照下圖方式處理,得到刪除場景 2.1.2.3。

將兄弟結(jié)點變?yōu)楹谏?/span>
將父結(jié)點變?yōu)榧t色
對父結(jié)點進行左旋,得到場景 2.1.2.3
進行場景 2.1.2.3 的處理
如圖所示:

將兄弟結(jié)點的顏色變?yōu)楦附Y(jié)點的顏色
將父結(jié)點變?yōu)楹谏?/span>
將兄弟結(jié)點的右子結(jié)點變?yōu)楹谏?/span>
對父結(jié)點進行左旋
如圖所示:

將兄弟結(jié)點變?yōu)榧t色
將兄弟結(jié)點的左子結(jié)點變?yōu)楹谏?/span>
對兄弟結(jié)點進行右旋,得到場景 2.1.2.1
進行場景 2.1.2.1 的處理
兄弟子樹沒有紅結(jié)點可以“借”了,再向父結(jié)點“借”。如果父結(jié)點是黑色,為了讓父結(jié)點在所在的子樹中保證平衡(替換結(jié)點即將刪除,少了一個黑色結(jié)點,子樹也需要少一個)先把兄弟結(jié)點變?yōu)榧t色,再讓父結(jié)點成為新的替換結(jié)點。

如果父結(jié)點為黑色:將兄弟結(jié)點變?yōu)榧t色;將父結(jié)點作為新的替換結(jié)點;重新進行刪除結(jié)點的場景處理。
如果父結(jié)點為紅色:替換結(jié)點的父結(jié)點和替換結(jié)點的兄弟結(jié)點顏色交換;刪除結(jié)點和替換結(jié)點的值交換后,刪除替換結(jié)點。
場景 2.2.1:替換結(jié)點的兄弟結(jié)點為紅色

將兄弟結(jié)點變?yōu)楹谏?/span>
將父結(jié)點變?yōu)榧t色
對父結(jié)點進行右旋,得到場景 2.2.2.3
進行場景 2.2.2.3 的處理

將兄弟結(jié)點的顏色變?yōu)楦附Y(jié)點的顏色
將父結(jié)點變?yōu)楹谏?/span>
將兄弟結(jié)點的左子結(jié)點變?yōu)楹谏?/span>
對父結(jié)點進行右旋
場景 2.2.2.2:替換結(jié)點的兄弟結(jié)點的左子結(jié)點為黑色,右子結(jié)點為紅色

將兄弟結(jié)點變?yōu)榧t色
將兄弟結(jié)點的右子結(jié)點設(shè)為黑色
對兄弟結(jié)點進行左旋,得到場景 2.2.2.1
進行場景 2.2.2.1 的處理
場景 2.2.2.3:替換結(jié)點的兄弟結(jié)點的子結(jié)點都為黑色

如果父結(jié)點為黑色:將兄弟結(jié)點變?yōu)榧t色;將父結(jié)點作為新的替換結(jié)點;重新進行刪除結(jié)點的場景處理。
如果父結(jié)點為紅色:替換結(jié)點的父結(jié)點和替換結(jié)點的兄弟結(jié)點顏色交換;刪除結(jié)點和替換結(jié)點的值交換后,刪除替換結(jié)點。
推薦閱讀:
完全整理 | 365篇高質(zhì)技術(shù)文章目錄整理
專注服務(wù)器后臺技術(shù)棧知識總結(jié)分享
歡迎關(guān)注交流共同進步
