之前在公司組內(nèi)分享了紅黑樹的工作原理,今天把它整理下發(fā)出來,希望能對(duì)大家有所幫助,對(duì)自己也算是一個(gè)知識(shí)點(diǎn)的總結(jié)。
這篇文章算是我寫博客寫公眾號(hào)以來畫圖最多的一篇文章了,沒有之一,我希望盡可能多地用圖片來形象地描述紅黑樹的各種操作的前后變換原理,幫助大家來理解紅黑樹的工作原理,下面,多圖預(yù)警開始了。
在講紅黑樹之前,我們首先來了解下下面幾個(gè)概念:二叉樹,排序二叉樹以及平衡二叉樹。
二叉樹
二叉樹指的是每個(gè)節(jié)點(diǎn)最多只能有兩個(gè)字?jǐn)?shù)的有序樹。通常左邊的子樹稱為左子樹
,右邊的子樹稱為右子樹
。這里說的有序樹強(qiáng)調(diào)的是二叉樹的左子樹和右子樹的次序不能隨意顛倒。
二叉樹簡單的示意圖如下:
代碼定義:
class Node {
T data;
Node left;
Node right;
}
排序二叉樹
所謂排序二叉樹,顧名思義,排序二叉樹是有順序的,它是一種特殊結(jié)構(gòu)的二叉樹,我們可以對(duì)樹中所有節(jié)點(diǎn)進(jìn)行排序和檢索。
性質(zhì)
- 若它的左子樹不空,則左子樹上所有節(jié)點(diǎn)的值均小于它的根節(jié)點(diǎn)的值;
- 若她的右子樹不空,則右子樹上所有節(jié)點(diǎn)的值均大于它的根節(jié)點(diǎn)的值;
- 具有遞歸性,排序二叉樹的左子樹、右子樹也是排序二叉樹。
排序二叉樹簡單示意圖:
排序二叉樹退化成鏈表
排序二叉樹的左子樹上所有節(jié)點(diǎn)的值小于根節(jié)點(diǎn)的值,右子樹上所有節(jié)點(diǎn)的值大于根節(jié)點(diǎn)的值,當(dāng)我們插入一組元素正好是有序的時(shí)候,這時(shí)會(huì)讓排序二叉樹退化成鏈表。
正常情況下,排序二叉樹是如下圖這樣的:
但是,當(dāng)插入的一組元素正好是有序的時(shí)候,排序二叉樹就變成了下邊這樣了,就變成了普通的鏈表結(jié)構(gòu),如下圖所示:
正常情況下的排序二叉樹檢索效率類似于二分查找,二分查找的時(shí)間復(fù)雜度為 O(log n),但是如果排序二叉樹退化成鏈表結(jié)構(gòu),那么檢索效率就變成了線性的 O(n) 的,這樣相對(duì)于 O(log n) 來說,檢索效率肯定是要差不少的。
思考,二分查找和正常的排序二叉樹的時(shí)間復(fù)雜度都是 O(log n),那么為什么是O(log n) ?
關(guān)于 O(log n) 的分析下面這篇文章講解的非常好,感興趣的可以看下這篇文章 二分查找的時(shí)間復(fù)雜度,文章是拿二分查找來舉例的,二分查找和平衡二叉樹的時(shí)間復(fù)雜度是一樣的,理解了二分查找的時(shí)間復(fù)雜度,再來理解平衡二叉樹就不難了,這里就不贅述了。
繼續(xù)回到我們的主題上,為了解決排序二叉樹在特殊情況下會(huì)退化成鏈表的問題(鏈表的檢索效率是 O(n) 相對(duì)正常二叉樹來說要差不少),所以有人發(fā)明了平衡二叉樹
和紅黑樹
類似的平衡樹。
平衡二叉樹
平衡二叉數(shù)又被稱為 AVL 樹,AVL 樹的名字來源于它的發(fā)明作者 G.M. Adelson-Velsky 和 E.M. Landis,取自兩人名字的首字母。
官方定義:它或者是一顆空樹,或者具有以下性質(zhì)的排序二叉樹:它的左子樹和右子樹的深度之差(平衡因子)的絕對(duì)值不超過1,且它的左子樹和右子樹都是一顆平衡二叉樹。
兩個(gè)條件:
- 平衡二叉樹必須是排序二叉樹,也就是說平衡二叉樹他的左子樹所有節(jié)點(diǎn)的值必須小于根節(jié)點(diǎn)的值,它的右子樹上所有節(jié)點(diǎn)的值必須大于它的根節(jié)點(diǎn)的值。
- 左子樹和右子樹的深度之差的絕對(duì)值不超過1。
紅黑樹
講了這么多概念,接下來主角紅黑樹終于要上場了。
為什么有紅黑樹?
其實(shí)紅黑樹和上面的平衡二叉樹類似,本質(zhì)上都是為了解決排序二叉樹在極端情況下退化成鏈表導(dǎo)致檢索效率大大降低的問題,紅黑樹最早是由 Rudolf Bayer 于 1972 年發(fā)明的。
紅黑樹首先肯定是一個(gè)排序二叉樹,它在每個(gè)節(jié)點(diǎn)上增加了一個(gè)存儲(chǔ)位來表示節(jié)點(diǎn)的顏色,可以是 RED 或 BLACK 。
Java 中實(shí)現(xiàn)紅黑樹大概結(jié)構(gòu)圖如下所示:
紅黑樹的特性
- 性質(zhì)1:每個(gè)節(jié)點(diǎn)要么是紅色,要么是黑色。
- 性質(zhì)2:根節(jié)點(diǎn)永遠(yuǎn)是黑色的。
- 性質(zhì)3:所有的葉子節(jié)點(diǎn)都是空節(jié)點(diǎn)(即null),并且是黑色的。
- 性質(zhì)4:每個(gè)紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)都是黑色。(從每個(gè)葉子到根的路徑上不會(huì)有兩個(gè)連續(xù)的紅色節(jié)點(diǎn)。)
- 性質(zhì)5:從任一節(jié)點(diǎn)到其子樹中每個(gè)葉子節(jié)點(diǎn)的路徑都包含相同數(shù)量的黑色節(jié)點(diǎn)。
針對(duì)上面的 5 種性質(zhì),我們簡單理解下,對(duì)于性質(zhì) 1 和性質(zhì) 2 ,相當(dāng)于是對(duì)紅黑樹每個(gè)節(jié)點(diǎn)的約束,根節(jié)點(diǎn)是黑色,其他的節(jié)點(diǎn)要么是紅色,要么是黑色。
對(duì)于性質(zhì) 3 中指定紅黑樹的每個(gè)葉子節(jié)點(diǎn)都是空節(jié)點(diǎn),而且葉子節(jié)點(diǎn)都是黑色,但 Java 實(shí)現(xiàn)的紅黑樹會(huì)使用 null 來代表空節(jié)點(diǎn),因此我們在遍歷 Java里的紅黑樹的時(shí)候會(huì)看不到葉子節(jié)點(diǎn),而看到的是每個(gè)葉子節(jié)點(diǎn)都是紅色的,這一點(diǎn)需要注意。
對(duì)于性質(zhì) 5,這里我們需要注意的是,這里的描述是從任一節(jié)點(diǎn),從任一節(jié)點(diǎn)到它的子樹的每個(gè)葉子節(jié)點(diǎn)黑色節(jié)點(diǎn)的數(shù)量都是相同的,這個(gè)數(shù)量被稱為這個(gè)節(jié)點(diǎn)的黑高。
如果我們從根節(jié)點(diǎn)出發(fā)到每個(gè)葉子節(jié)點(diǎn)的路徑都包含相同數(shù)量的黑色節(jié)點(diǎn),這個(gè)黑色節(jié)點(diǎn)的數(shù)量被稱為樹的黑色高度。樹的黑色高度和節(jié)點(diǎn)的黑色高度是不一樣的,這里要注意區(qū)分。
其實(shí)到這里有人可能會(huì)問了,紅黑樹的性質(zhì)說了一大堆,那是不是說只要保證紅黑樹的節(jié)點(diǎn)是紅黑交替就能保證樹是平衡的呢?
其實(shí)不是這樣的,我們可以看來看下面這張圖:
左邊的子樹都是黑色節(jié)點(diǎn),但是這個(gè)紅黑樹依然是平衡的,5 條性質(zhì)它都滿足。
這個(gè)樹的黑色高度為 3,從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的最短路徑長度是 2,該路徑上全是黑色節(jié)點(diǎn),包括葉子節(jié)點(diǎn),從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)最長路徑為 4,每個(gè)黑色節(jié)點(diǎn)之間會(huì)插入紅色節(jié)點(diǎn)。
通過上面的性質(zhì) 4 和性質(zhì) 5,其實(shí)上保證了沒有任何一條路徑會(huì)比其他路徑長出兩倍,所以這樣的紅黑樹是平衡的。
其實(shí)這算是一個(gè)推論,紅黑樹在最差情況下,最長的路徑都不會(huì)比最短的路徑長出兩倍。其實(shí)紅黑樹并不是真正的平衡二叉樹,它只能保證大致是平衡的,因?yàn)榧t黑樹的高度不會(huì)無限增高,在實(shí)際應(yīng)用用,紅黑樹的統(tǒng)計(jì)性能要高于平衡二叉樹,但極端性能略差。
紅黑樹的插入
想要徹底理解紅黑樹,除了上面說到的理解紅黑樹的性質(zhì)以外,就是理解紅黑樹的插入操作了。
紅黑樹的插入和普通排序二叉樹的插入基本一致,排序二叉樹的要求是左子樹上的所有節(jié)點(diǎn)都要比根節(jié)點(diǎn)小,右子樹上的所有節(jié)點(diǎn)都要比跟節(jié)點(diǎn)大,當(dāng)插入一個(gè)新的節(jié)點(diǎn)的時(shí)候,首先要找到當(dāng)前要插入的節(jié)點(diǎn)適合放在排序二叉樹哪個(gè)位置,然后插入當(dāng)前節(jié)點(diǎn)即可。紅黑樹和排序二叉樹不同的是,紅黑樹需要在插入節(jié)點(diǎn)調(diào)整樹的結(jié)構(gòu)來讓樹保持平衡。
一般情況下,紅黑樹中新插入的節(jié)點(diǎn)都是紅色的,那么,為什么說新加入到紅黑樹中的節(jié)點(diǎn)要是紅色的呢?
這個(gè)問題可以這樣理解,我們從性質(zhì)5中知道,當(dāng)前紅黑樹中從根節(jié)點(diǎn)到每個(gè)葉子節(jié)點(diǎn)的黑色節(jié)點(diǎn)數(shù)量是一樣的,此時(shí)假如新的黑色節(jié)點(diǎn)的話,必然破壞規(guī)則,但加入紅色節(jié)點(diǎn)卻不一定,除非其父節(jié)點(diǎn)就是紅色節(jié)點(diǎn),因此加入紅色節(jié)點(diǎn),破壞規(guī)則的可能性小一些。
接下來我們重點(diǎn)來講紅黑樹插入新節(jié)點(diǎn)后是如何保持平衡的。
給定下面這樣一顆紅黑樹:
當(dāng)我們插入值為66的節(jié)點(diǎn)的時(shí)候,示意圖如下:
很明顯,這個(gè)時(shí)候結(jié)構(gòu)依然遵循著上述5大特性,無需啟動(dòng)自動(dòng)平衡機(jī)制調(diào)整節(jié)點(diǎn)平衡狀態(tài)。
如果再向里面插入值為51的節(jié)點(diǎn)呢,這個(gè)時(shí)候紅黑樹變成了這樣。
這樣的結(jié)構(gòu)實(shí)際上是不滿足性質(zhì)4的,紅色兩個(gè)子節(jié)點(diǎn)必須是黑色的,而這里49這個(gè)紅色節(jié)點(diǎn)現(xiàn)在有個(gè)51的紅色節(jié)點(diǎn)與其相連。
這個(gè)時(shí)候我們需要調(diào)整這個(gè)樹的結(jié)構(gòu)來保證紅黑樹的平衡。
首先嘗試將49這個(gè)節(jié)點(diǎn)設(shè)置為黑色,如下示意圖。
這個(gè)時(shí)候我們發(fā)現(xiàn)黑高是不對(duì)的,其中 60-56-45-49-51-null 這條路徑有 4 個(gè)黑節(jié)點(diǎn),其他路徑的黑色節(jié)點(diǎn)是 3 個(gè)。
接著調(diào)整紅黑樹,我們再次嘗試把45這個(gè)節(jié)點(diǎn)設(shè)置為紅色的,如下圖所示:
這個(gè)時(shí)候我們發(fā)現(xiàn)問題又來了,56-45-43 都是紅色節(jié)點(diǎn)的,出現(xiàn)了紅色節(jié)點(diǎn)相連的問題。
于是我們需要再把 56 和 43 設(shè)置為黑色的,如下圖所示。
于是我們把 68 這個(gè)紅色節(jié)點(diǎn)設(shè)置為黑色的。
對(duì)于這種紅黑樹插入節(jié)點(diǎn)的情況下,我們可以只需要通過變色就可以保持樹的平衡了。但是并不是每次都是這么幸運(yùn)的,當(dāng)變色行不通的時(shí)候,我們需要考慮另一個(gè)手段就是旋轉(zhuǎn)了。
例如下面這種情況,同樣還是拿這顆紅黑樹舉例。
現(xiàn)在這顆紅黑樹,我們現(xiàn)在插入節(jié)點(diǎn)65。
我們嘗試把 66 這個(gè)節(jié)點(diǎn)設(shè)置為黑色,如下圖所示。
這樣操作之后黑高又出現(xiàn)不一致的情況了,60-68-64-null 有 3 個(gè)黑色節(jié)點(diǎn),而60-68-64-66-null 這條路徑有 4 個(gè)黑色節(jié)點(diǎn),這樣的結(jié)構(gòu)是不平衡的。
或者我們把 68 設(shè)置為黑色,把 64 設(shè)置為紅色,如下圖所示:
但是,同樣的問題,上面這顆紅黑樹的黑色高度還是不一致,60-68-64-null 和 60-68-64-66-null 這兩條路徑黑色高度還是不一致。
這種情況如果只通過變色的情況是不能保持紅黑樹的平衡的。
紅黑樹的旋轉(zhuǎn)
接下來我們講講紅黑樹的旋轉(zhuǎn),旋轉(zhuǎn)分為左旋和右旋。
左旋
文字描述:逆時(shí)針旋轉(zhuǎn)兩個(gè)節(jié)點(diǎn),讓一個(gè)節(jié)點(diǎn)被其右子節(jié)點(diǎn)取代,而該節(jié)點(diǎn)成為右子節(jié)點(diǎn)的左子節(jié)點(diǎn)。
文字描述太抽象,接下來看下圖片展示。
首先斷開節(jié)點(diǎn)PL與右子節(jié)點(diǎn)G的關(guān)系,同時(shí)將其右子節(jié)點(diǎn)的引用指向節(jié)點(diǎn)C2;然后斷開節(jié)點(diǎn)G與左子節(jié)點(diǎn)C2的關(guān)系,同時(shí)將G的左子節(jié)點(diǎn)的應(yīng)用指向節(jié)點(diǎn)PL。
接下來再放下 gif 圖,希望能幫助大家更好地理解左旋,圖片來自網(wǎng)絡(luò)。
右旋
文字描述:順時(shí)針旋轉(zhuǎn)兩個(gè)節(jié)點(diǎn),讓一個(gè)節(jié)點(diǎn)被其左子節(jié)點(diǎn)取代,而該節(jié)點(diǎn)成為左子節(jié)點(diǎn)的右子節(jié)點(diǎn)。
右旋的圖片展示:
首先斷開節(jié)點(diǎn)G與左子節(jié)點(diǎn)PL的關(guān)系,同時(shí)將其左子節(jié)點(diǎn)的引用指向節(jié)點(diǎn)C2;然后斷開節(jié)點(diǎn)PL與右子節(jié)點(diǎn)C2的關(guān)系,同時(shí)將PL的右子節(jié)點(diǎn)的應(yīng)用指向節(jié)點(diǎn)G。
右旋的gif展示(圖片來自網(wǎng)絡(luò)):
介紹完了左旋和右旋基本操作,我們來詳細(xì)介紹下紅黑樹的幾種旋轉(zhuǎn)場景。
左左節(jié)點(diǎn)旋轉(zhuǎn)(插入節(jié)點(diǎn)的父節(jié)點(diǎn)是左節(jié)點(diǎn),插入節(jié)點(diǎn)也是左節(jié)點(diǎn))
如下圖所示的紅黑樹,我們插入節(jié)點(diǎn)是65。
操作步驟如下可以圍繞祖父節(jié)點(diǎn) 69 右旋,再結(jié)合變色,步驟如下所示:
左右節(jié)點(diǎn)旋轉(zhuǎn)(插入節(jié)點(diǎn)的父節(jié)點(diǎn)是左節(jié)點(diǎn),插入節(jié)點(diǎn)是右節(jié)點(diǎn))
還是上面這顆紅黑樹,我們再插入節(jié)點(diǎn) 67。
這種情況我們可以這樣操作,先圍繞父節(jié)點(diǎn) 66 左旋,然后再圍繞祖父節(jié)點(diǎn) 69 右旋,最后再將 67 設(shè)置為黑色,把 69 設(shè)置為紅色,如下圖所示。
右左節(jié)點(diǎn)旋轉(zhuǎn)(插入節(jié)點(diǎn)的父節(jié)點(diǎn)是右節(jié)點(diǎn),插入節(jié)點(diǎn)左節(jié)點(diǎn))
如下圖這種情況,我們要插入節(jié)點(diǎn)68。
這種情況,我們可以先圍繞父節(jié)點(diǎn) 69 右旋,接著再圍繞祖父節(jié)點(diǎn) 66 左旋,最后把 68 節(jié)點(diǎn)設(shè)置為黑色,把 66 設(shè)置為紅色,我們的具體操作步驟如下所示。
右右節(jié)點(diǎn)旋轉(zhuǎn)(插入節(jié)點(diǎn)的父節(jié)點(diǎn)是右節(jié)點(diǎn),插入節(jié)點(diǎn)也是右節(jié)點(diǎn))
還是來上面的圖來舉例,我們在這顆紅黑樹上插入節(jié)點(diǎn) 70 。
我們可以這樣操作圍繞祖父節(jié)點(diǎn) 66 左旋,再把旋轉(zhuǎn)后的根節(jié)點(diǎn) 69 設(shè)置為黑色,把 66 這個(gè)節(jié)點(diǎn)設(shè)置為紅色。具體可以參看下圖:
紅黑樹在 Java 中的實(shí)現(xiàn)
Java 中的紅黑樹實(shí)現(xiàn)類是 TreeMap ,接下來我們嘗試從源碼角度來逐行解釋 TreeMap 這一套機(jī)制是如何運(yùn)作的。
// TreeMap中使用Entry來描述每個(gè)節(jié)點(diǎn)
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
...
}
TreeMap 的put方法。
public V put(K key, V value) {
//先以t保存鏈表的root節(jié)點(diǎn)
Entry<K,V> t = root;
//如果t=null,表明是一個(gè)空鏈表,即該TreeMap里沒有任何Entry作為root
if (t == null) {
compare(key, key); // type (and possibly null) check
//將新的key-value創(chuàng)建一個(gè)Entry,并將該Entry作為root
root = new Entry<>(key, value, null);
size = 1;
//記錄修改次數(shù)加1
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//如果比較器cpr不為null,即表明采用定制排序
if (cpr != null) {
do {
//使用parent上次循環(huán)后的t所引用的Entry
parent = t;
//將新插入的key和t的key進(jìn)行比較
cmp = cpr.compare(key, t.key);
//如果新插入的key小于t的key,t等于t的左邊節(jié)點(diǎn)
if (cmp < 0)
t = t.left;
//如果新插入的key大于t的key,t等于t的右邊節(jié)點(diǎn)
else if (cmp > 0)
t = t.right;
else
//如果兩個(gè)key相等,新value覆蓋原有的value,并返回原有的value
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//將新插入的節(jié)點(diǎn)作為parent節(jié)點(diǎn)的子節(jié)點(diǎn)
Entry<K,V> e = new Entry<>(key, value, parent);
//如果新插入key小于parent的key,則e作為parent的左子節(jié)點(diǎn)
if (cmp < 0)
parent.left = e;
//如果新插入key小于parent的key,則e作為parent的右子節(jié)點(diǎn)
else
parent.right = e;
//修復(fù)紅黑樹
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
//插入節(jié)點(diǎn)后修復(fù)紅黑樹
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
//直到x節(jié)點(diǎn)的父節(jié)點(diǎn)不是根,且x的父節(jié)點(diǎn)是紅色
while (x != null && x != root && x.parent.color == RED) {
//如果x的父節(jié)點(diǎn)是其父節(jié)點(diǎn)的左子節(jié)點(diǎn)
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//獲取x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//如果x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是紅色
if (colorOf(y) == RED) {
//將x的父節(jié)點(diǎn)設(shè)置為黑色
setColor(parentOf(x), BLACK);
//將x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)設(shè)置為黑色
setColor(y, BLACK);
//將x的父節(jié)點(diǎn)的父節(jié)點(diǎn)設(shè)為紅色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
}
//如果x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是黑色
else {
//TODO 對(duì)應(yīng)情況第二種,左右節(jié)點(diǎn)旋轉(zhuǎn)
//如果x是其父節(jié)點(diǎn)的右子節(jié)點(diǎn)
if (x == rightOf(parentOf(x))) {
//將x的父節(jié)點(diǎn)設(shè)為x
x = parentOf(x);
//右旋轉(zhuǎn)
rotateLeft(x);
}
//把x的父節(jié)點(diǎn)設(shè)置為黑色
setColor(parentOf(x), BLACK);
//把x的父節(jié)點(diǎn)父節(jié)點(diǎn)設(shè)為紅色
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
}
//如果x的父節(jié)點(diǎn)是其父節(jié)點(diǎn)的右子節(jié)點(diǎn)
else {
//獲取x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//只著色的情況對(duì)應(yīng)的是最開始例子,沒有旋轉(zhuǎn)操作,但是要對(duì)應(yīng)多次變換
//如果x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是紅色
if (colorOf(y) == RED) {
//將x的父節(jié)點(diǎn)設(shè)置為黑色
setColor(parentOf(x), BLACK);
//將x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)設(shè)為黑色
setColor(y, BLACK);
//將X的父節(jié)點(diǎn)的父節(jié)點(diǎn)(G)設(shè)置紅色
setColor(parentOf(parentOf(x)), RED);
//將x設(shè)為x的父節(jié)點(diǎn)的節(jié)點(diǎn)
x = parentOf(parentOf(x));
}
//如果x的父節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是黑色
else {
//如果x是其父節(jié)點(diǎn)的左子節(jié)點(diǎn)
if (x == leftOf(parentOf(x))) {
//將x的父節(jié)點(diǎn)設(shè)為x
x = parentOf(x);
//右旋轉(zhuǎn)
rotateRight(x);
}
//將x的父節(jié)點(diǎn)設(shè)為黑色
setColor(parentOf(x), BLACK);
//把x的父節(jié)點(diǎn)的父節(jié)點(diǎn)設(shè)為紅色
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//將根節(jié)點(diǎn)強(qiáng)制設(shè)置為黑色
root.color = BLACK;
}
TreeMap的插入節(jié)點(diǎn)和普通的排序二叉樹沒啥區(qū)別,唯一不同的是,在TreeMap 插入節(jié)點(diǎn)后會(huì)調(diào)用方法fixAfterInsertion(e)來重新調(diào)整紅黑樹的結(jié)構(gòu)來讓紅黑樹保持平衡。
我們重點(diǎn)關(guān)注下紅黑樹的fixAfterInsertion(e)方法,接下來我們來分別介紹兩種場景來演示fixAfterInsertion(e)方法的執(zhí)行流程。
第一種場景:只需變色即可平衡
同樣是拿這顆紅黑樹舉例,現(xiàn)在我們插入節(jié)點(diǎn) 51。
當(dāng)我們需要插入節(jié)點(diǎn)51的時(shí)候,這個(gè)時(shí)候TreeMap 的 put 方法執(zhí)行后會(huì)得到下面這張圖。
接著調(diào)用fixAfterInsertion(e)方法,如下代碼流程所示。
當(dāng)?shù)谝淮芜M(jìn)入循環(huán)后,執(zhí)行后會(huì)得到下面的紅黑樹結(jié)構(gòu)。
在把 x 重新賦值后,重新進(jìn)入 while 循環(huán),此時(shí)的 x 節(jié)點(diǎn)為 45 。
執(zhí)行上述流程后,得到下面所示的紅黑樹結(jié)構(gòu)。
這個(gè)時(shí)候x被重新賦值為60,因?yàn)?0是根節(jié)點(diǎn),所以會(huì)退出 while 循環(huán)。在退出循序后,會(huì)再次把根節(jié)點(diǎn)設(shè)置為黑色,得到最終的結(jié)構(gòu)如下圖所示。
最后經(jīng)過兩次執(zhí)行while循環(huán)后,我們的紅黑樹會(huì)調(diào)整成現(xiàn)在這樣的結(jié)構(gòu),這樣的紅黑樹結(jié)構(gòu)是平衡的,所以路徑的黑高一致,并且沒有紅色節(jié)點(diǎn)相連的情況。
第二種場景 旋轉(zhuǎn)搭配變色來保持平衡
接下來我們再來演示第二種場景,需要結(jié)合變色和旋轉(zhuǎn)一起來保持平衡。
給定下面這樣一顆紅黑樹:
現(xiàn)在我們插入節(jié)點(diǎn)66,得到如下樹結(jié)構(gòu)。
同樣地,我們進(jìn)入fixAfterInsertion(e)方法。
最終我們得到的紅黑樹結(jié)構(gòu)如下圖所示:
調(diào)整成這樣的結(jié)構(gòu)我們的紅黑樹又再次保持平衡了。
演示 TreeMap 的流程就拿這兩種場景舉例了,其他的就不一一舉例了。
紅黑樹的刪除
因?yàn)橹暗姆窒碇徽砹思t黑樹的插入部分,本來想著紅黑樹的刪除就不整理了,有人跟我反饋說紅黑樹的刪除相對(duì)更復(fù)雜,于是索性還是把紅黑樹的刪除再整理下。
刪除相對(duì)插入來說,的確是要復(fù)雜一點(diǎn),但是復(fù)雜的地方是因?yàn)樵趧h除節(jié)點(diǎn)的這個(gè)操作情況有很多種,但是插入不一樣,插入節(jié)點(diǎn)的時(shí)候?qū)嶋H上這個(gè)節(jié)點(diǎn)的位置是確定的,在節(jié)點(diǎn)插入成功后只需要調(diào)整紅黑樹的平衡就可以了。
但是刪除不一樣的是,刪除節(jié)點(diǎn)的時(shí)候我們不能簡單地把這個(gè)節(jié)點(diǎn)設(shè)置為null,因?yàn)槿绻@個(gè)節(jié)點(diǎn)有子節(jié)點(diǎn)的情況下,不能簡單地把當(dāng)前刪除的節(jié)點(diǎn)設(shè)置為null,這個(gè)被刪除的節(jié)點(diǎn)的位置需要有新的節(jié)點(diǎn)來填補(bǔ)。這樣一來,需要分多種情況來處理了。
刪除節(jié)點(diǎn)是根節(jié)點(diǎn)
直接刪除根節(jié)點(diǎn)即可。
刪掉節(jié)點(diǎn)的左子節(jié)點(diǎn)和右子節(jié)點(diǎn)都是為空
直接刪除當(dāng)前節(jié)點(diǎn)即可。
刪除節(jié)點(diǎn)有一個(gè)子節(jié)點(diǎn)不為空
這個(gè)時(shí)候需要使用子節(jié)點(diǎn)來代替當(dāng)前需要?jiǎng)h除的節(jié)點(diǎn),然后再把子節(jié)點(diǎn)刪除即可。
給定下面這棵樹,當(dāng)我們需要?jiǎng)h除節(jié)點(diǎn)69的時(shí)候。
首先用子節(jié)點(diǎn)代替當(dāng)前待刪除節(jié)點(diǎn),然后再把子節(jié)點(diǎn)刪除。
最終的紅黑樹結(jié)構(gòu)如下面所示,這個(gè)結(jié)構(gòu)的紅黑樹我們是不需要通過變色+旋轉(zhuǎn)來保持紅黑樹的平衡了,因?yàn)閷⒆庸?jié)點(diǎn)刪除后樹已經(jīng)是平衡的了。
還有一種場景是當(dāng)我們待刪除節(jié)點(diǎn)是黑色的,黑色的節(jié)點(diǎn)被刪除后,樹的黑高就會(huì)出現(xiàn)不一致的情況,這個(gè)時(shí)候就需要重新調(diào)整結(jié)構(gòu)。
還是拿上面這顆刪除節(jié)點(diǎn)后的紅黑樹舉例,我們現(xiàn)在需要?jiǎng)h除節(jié)點(diǎn)67。
因?yàn)?7 這個(gè)節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)都是null,所以直接刪除,得到如下圖所示結(jié)構(gòu):
這個(gè)時(shí)候我們樹的黑高是不一致的,左邊黑高是3,右邊是2,所以我們需要把64節(jié)點(diǎn)設(shè)置為紅色來保持平衡。
刪除節(jié)點(diǎn)兩個(gè)子節(jié)點(diǎn)都不為空
刪除節(jié)點(diǎn)兩個(gè)子節(jié)點(diǎn)都不為空的情況下,跟上面有一個(gè)節(jié)點(diǎn)不為空的情況下也是有點(diǎn)類似,同樣是需要找能替代當(dāng)前節(jié)點(diǎn)的節(jié)點(diǎn),找到后,把能替代刪除節(jié)點(diǎn)值復(fù)制過來,然后再把替代節(jié)點(diǎn)刪除掉。
- 先找到替代節(jié)點(diǎn),也就是前驅(qū)節(jié)點(diǎn)或者后繼節(jié)點(diǎn)
- 然后把前驅(qū)節(jié)點(diǎn)或者后繼節(jié)點(diǎn)復(fù)制到當(dāng)前待刪除節(jié)點(diǎn)的位置,然后在刪除前驅(qū)節(jié)點(diǎn)或者后繼節(jié)點(diǎn)。
那么什么叫做前驅(qū),什么叫做后繼呢?
前驅(qū)是左子樹中最大的節(jié)點(diǎn),后繼則是右子樹中最小的節(jié)點(diǎn)。
前驅(qū)或者后繼都是最接近當(dāng)前節(jié)點(diǎn)的節(jié)點(diǎn),當(dāng)我們需要?jiǎng)h除當(dāng)前節(jié)點(diǎn)的時(shí)候,也就是找到能替代當(dāng)前節(jié)點(diǎn)的節(jié)點(diǎn),能夠替代當(dāng)前節(jié)點(diǎn)肯定是最接近當(dāng)前節(jié)點(diǎn)。
在當(dāng)前刪除節(jié)點(diǎn)兩個(gè)子節(jié)點(diǎn)不為空的場景下,我們需要再進(jìn)行細(xì)分,主要分為以下三種情況。
第一種,前驅(qū)節(jié)點(diǎn)為黑色節(jié)點(diǎn),同時(shí)有一個(gè)非空節(jié)點(diǎn)
如下面這樣一棵樹,我們需要?jiǎng)h除節(jié)點(diǎn)64:
首先找到前驅(qū)節(jié)點(diǎn),把前驅(qū)節(jié)點(diǎn)復(fù)制到當(dāng)前節(jié)點(diǎn):
接著刪除前驅(qū)節(jié)點(diǎn)。
這個(gè)時(shí)候63和60這個(gè)節(jié)點(diǎn)都是紅色的,我們嘗試把60這個(gè)節(jié)點(diǎn)設(shè)置為紅色即可使整個(gè)紅黑樹達(dá)到平衡。
第二種,前驅(qū)節(jié)點(diǎn)為黑色節(jié)點(diǎn),同時(shí)子節(jié)點(diǎn)都為空
前驅(qū)節(jié)點(diǎn)是黑色的,子節(jié)點(diǎn)都為空,這個(gè)時(shí)候操作步驟與上面基本類似。
如下操作步驟:
因?yàn)橐獎(jiǎng)h除節(jié)點(diǎn)64,接著找到前驅(qū)節(jié)點(diǎn)63,把63節(jié)點(diǎn)復(fù)制到當(dāng)前位置,然后將前驅(qū)節(jié)點(diǎn)63刪除掉,變色后出現(xiàn)黑高不一致的情況下,最后把63節(jié)點(diǎn)設(shè)置為黑色,把65節(jié)點(diǎn)設(shè)置為紅色,這樣就能保證紅黑樹的平衡。
第三種,前驅(qū)節(jié)點(diǎn)為紅色節(jié)點(diǎn),同時(shí)子節(jié)點(diǎn)都為空
給定下面這顆紅黑樹,我們需要?jiǎng)h除節(jié)點(diǎn)64的時(shí)候。
同樣地,我們找到64的前驅(qū)節(jié)點(diǎn)63,接著把63賦值到64這個(gè)位置。
然后刪除前驅(qū)節(jié)點(diǎn)。
刪除節(jié)點(diǎn)后不需要變色也不需要旋轉(zhuǎn)即可保持樹的平衡。
終于把紅黑樹的基本原理部分寫完了,用了很多示意圖,這篇文章是在之前分享的 ppt 上再整理出來,我覺得自己應(yīng)該算是把基本操作講明白了,整理這篇文章前前后后用了近一周左右,因?yàn)槠綍r(shí)上班,基本上只有周末有時(shí)間才有時(shí)間整理,如有問題請(qǐng)留言討論。
如果您覺得寫得還可以,請(qǐng)您幫忙點(diǎn)個(gè)贊,您的點(diǎn)贊真的是對(duì)我最大的支持,也是我能繼續(xù)寫下去的動(dòng)力,感謝。
文章中很多參考了下面文章的一些示意圖,非常感謝以下文章。