红黑树与平衡二叉树_百图详解红黑树,想不理解都难
之前在公司組內(nèi)分享了紅黑樹(shù)的工作原理,今天把它整理下發(fā)出來(lái),希望能對(duì)大家有所幫助,對(duì)自己也算是一個(gè)知識(shí)點(diǎn)的總結(jié)。
這篇文章算是我寫(xiě)博客寫(xiě)公眾號(hào)以來(lái)畫(huà)圖最多的一篇文章了,沒(méi)有之一,我希望盡可能多地用圖片來(lái)形象地描述紅黑樹(shù)的各種操作的前后變換原理,幫助大家來(lái)理解紅黑樹(shù)的工作原理,下面,多圖預(yù)警開(kāi)始了。
在講紅黑樹(shù)之前,我們首先來(lái)了解下下面幾個(gè)概念:二叉樹(shù),排序二叉樹(shù)以及平衡二叉樹(shù)。
二叉樹(shù)
二叉樹(shù)指的是每個(gè)節(jié)點(diǎn)最多只能有兩個(gè)字?jǐn)?shù)的有序樹(shù)。通常左邊的子樹(shù)稱為左子樹(shù) ,右邊的子樹(shù)稱為右子樹(shù) 。這里說(shuō)的有序樹(shù)強(qiáng)調(diào)的是二叉樹(shù)的左子樹(shù)和右子樹(shù)的次序不能隨意顛倒。
二叉樹(shù)簡(jiǎn)單的示意圖如下:
代碼定義:
class Node {T data;Node left;Node right; }排序二叉樹(shù)
所謂排序二叉樹(shù),顧名思義,排序二叉樹(shù)是有順序的,它是一種特殊結(jié)構(gòu)的二叉樹(shù),我們可以對(duì)樹(shù)中所有節(jié)點(diǎn)進(jìn)行排序和檢索。
性質(zhì)- 若它的左子樹(shù)不空,則左子樹(shù)上所有節(jié)點(diǎn)的值均小于它的根節(jié)點(diǎn)的值;
- 若她的右子樹(shù)不空,則右子樹(shù)上所有節(jié)點(diǎn)的值均大于它的根節(jié)點(diǎn)的值;
- 具有遞歸性,排序二叉樹(shù)的左子樹(shù)、右子樹(shù)也是排序二叉樹(shù)。
排序二叉樹(shù)簡(jiǎn)單示意圖:
排序二叉樹(shù)退化成鏈表
排序二叉樹(shù)的左子樹(shù)上所有節(jié)點(diǎn)的值小于根節(jié)點(diǎn)的值,右子樹(shù)上所有節(jié)點(diǎn)的值大于根節(jié)點(diǎn)的值,當(dāng)我們插入一組元素正好是有序的時(shí)候,這時(shí)會(huì)讓排序二叉樹(shù)退化成鏈表。
正常情況下,排序二叉樹(shù)是如下圖這樣的:
但是,當(dāng)插入的一組元素正好是有序的時(shí)候,排序二叉樹(shù)就變成了下邊這樣了,就變成了普通的鏈表結(jié)構(gòu),如下圖所示:
正常情況下的排序二叉樹(shù)檢索效率類(lèi)似于二分查找,二分查找的時(shí)間復(fù)雜度為 O(log n),但是如果排序二叉樹(shù)退化成鏈表結(jié)構(gòu),那么檢索效率就變成了線性的 O(n) 的,這樣相對(duì)于 O(log n) 來(lái)說(shuō),檢索效率肯定是要差不少的。
思考,二分查找和正常的排序二叉樹(shù)的時(shí)間復(fù)雜度都是 O(log n),那么為什么是O(log n) ?關(guān)于 O(log n) 的分析下面這篇文章講解的非常好,感興趣的可以看下這篇文章 二分查找的時(shí)間復(fù)雜度.md),文章是拿二分查找來(lái)舉例的,二分查找和平衡二叉樹(shù)的時(shí)間復(fù)雜度是一樣的,理解了二分查找的時(shí)間復(fù)雜度,再來(lái)理解平衡二叉樹(shù)就不難了,這里就不贅述了。
繼續(xù)回到我們的主題上,為了解決排序二叉樹(shù)在特殊情況下會(huì)退化成鏈表的問(wèn)題(鏈表的檢索效率是 O(n) 相對(duì)正常二叉樹(shù)來(lái)說(shuō)要差不少),所以有人發(fā)明了平衡二叉樹(shù)和紅黑樹(shù)類(lèi)似的平衡樹(shù)。
平衡二叉樹(shù)
平衡二叉數(shù)又被稱為 AVL 樹(shù),AVL 樹(shù)的名字來(lái)源于它的發(fā)明作者 G.M. Adelson-Velsky 和 E.M. Landis,取自兩人名字的首字母。
官方定義:它或者是一顆空樹(shù),或者具有以下性質(zhì)的排序二叉樹(shù):它的左子樹(shù)和右子樹(shù)的深度之差(平衡因子)的絕對(duì)值不超過(guò)1,且它的左子樹(shù)和右子樹(shù)都是一顆平衡二叉樹(shù)。
兩個(gè)條件:
- 平衡二叉樹(shù)必須是排序二叉樹(shù),也就是說(shuō)平衡二叉樹(shù)他的左子樹(shù)所有節(jié)點(diǎn)的值必須小于根節(jié)點(diǎn)的值,它的右子樹(shù)上所有節(jié)點(diǎn)的值必須大于它的根節(jié)點(diǎn)的值。
- 左子樹(shù)和右子樹(shù)的深度之差的絕對(duì)值不超過(guò)1。
紅黑樹(shù)
講了這么多概念,接下來(lái)主角紅黑樹(shù)終于要上場(chǎng)了。
為什么有紅黑樹(shù)?其實(shí)紅黑樹(shù)和上面的平衡二叉樹(shù)類(lèi)似,本質(zhì)上都是為了解決排序二叉樹(shù)在極端情況下退化成鏈表導(dǎo)致檢索效率大大降低的問(wèn)題,紅黑樹(shù)最早是由 Rudolf Bayer 于 1972 年發(fā)明的。
紅黑樹(shù)首先肯定是一個(gè)排序二叉樹(shù),它在每個(gè)節(jié)點(diǎn)上增加了一個(gè)存儲(chǔ)位來(lái)表示節(jié)點(diǎn)的顏色,可以是 RED 或 BLACK 。
Java 中實(shí)現(xiàn)紅黑樹(shù)大概結(jié)構(gòu)圖如下所示:
紅黑樹(shù)的特性
- 性質(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)到其子樹(shù)中每個(gè)葉子節(jié)點(diǎn)的路徑都包含相同數(shù)量的黑色節(jié)點(diǎn)。
針對(duì)上面的 5 種性質(zhì),我們簡(jiǎn)單理解下,對(duì)于性質(zhì) 1 和性質(zhì) 2 ,相當(dāng)于是對(duì)紅黑樹(shù)每個(gè)節(jié)點(diǎn)的約束,根節(jié)點(diǎn)是黑色,其他的節(jié)點(diǎn)要么是紅色,要么是黑色。
對(duì)于性質(zhì) 3 中指定紅黑樹(shù)的每個(gè)葉子節(jié)點(diǎn)都是空節(jié)點(diǎn),而且葉子節(jié)點(diǎn)都是黑色,但 Java 實(shí)現(xiàn)的紅黑樹(shù)會(huì)使用 null 來(lái)代表空節(jié)點(diǎn),因此我們?cè)诒闅v Java里的紅黑樹(shù)的時(shí)候會(huì)看不到葉子節(jié)點(diǎn),而看到的是每個(gè)葉子節(jié)點(diǎn)都是紅色的,這一點(diǎn)需要注意。
對(duì)于性質(zhì) 5,這里我們需要注意的是,這里的描述是從任一節(jié)點(diǎn),從任一節(jié)點(diǎn)到它的子樹(shù)的每個(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ù)量被稱為樹(shù)的黑色高度。樹(shù)的黑色高度和節(jié)點(diǎn)的黑色高度是不一樣的,這里要注意區(qū)分。
其實(shí)到這里有人可能會(huì)問(wèn)了,紅黑樹(shù)的性質(zhì)說(shuō)了一大堆,那是不是說(shuō)只要保證紅黑樹(shù)的節(jié)點(diǎn)是紅黑交替就能保證樹(shù)是平衡的呢?
其實(shí)不是這樣的,我們可以看來(lái)看下面這張圖:
左邊的子樹(shù)都是黑色節(jié)點(diǎn),但是這個(gè)紅黑樹(shù)依然是平衡的,5 條性質(zhì)它都滿足。
這個(gè)樹(shù)的黑色高度為 3,從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的最短路徑長(zhǎng)度是 2,該路徑上全是黑色節(jié)點(diǎn),包括葉子節(jié)點(diǎn),從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)最長(zhǎng)路徑為 4,每個(gè)黑色節(jié)點(diǎn)之間會(huì)插入紅色節(jié)點(diǎn)。
通過(guò)上面的性質(zhì) 4 和性質(zhì) 5,其實(shí)上保證了沒(méi)有任何一條路徑會(huì)比其他路徑長(zhǎng)出兩倍,所以這樣的紅黑樹(shù)是平衡的。
其實(shí)這算是一個(gè)推論,紅黑樹(shù)在最差情況下,最長(zhǎng)的路徑都不會(huì)比最短的路徑長(zhǎng)出兩倍。其實(shí)紅黑樹(shù)并不是真正的平衡二叉樹(shù),它只能保證大致是平衡的,因?yàn)榧t黑樹(shù)的高度不會(huì)無(wú)限增高,在實(shí)際應(yīng)用用,紅黑樹(shù)的統(tǒng)計(jì)性能要高于平衡二叉樹(shù),但極端性能略差。
紅黑樹(shù)的插入
想要徹底理解紅黑樹(shù),除了上面說(shuō)到的理解紅黑樹(shù)的性質(zhì)以外,就是理解紅黑樹(shù)的插入操作了。
紅黑樹(shù)的插入和普通排序二叉樹(shù)的插入基本一致,排序二叉樹(shù)的要求是左子樹(shù)上的所有節(jié)點(diǎn)都要比根節(jié)點(diǎn)小,右子樹(shù)上的所有節(jié)點(diǎn)都要比跟節(jié)點(diǎn)大,當(dāng)插入一個(gè)新的節(jié)點(diǎn)的時(shí)候,首先要找到當(dāng)前要插入的節(jié)點(diǎn)適合放在排序二叉樹(shù)哪個(gè)位置,然后插入當(dāng)前節(jié)點(diǎn)即可。紅黑樹(shù)和排序二叉樹(shù)不同的是,紅黑樹(shù)需要在插入節(jié)點(diǎn)調(diào)整樹(shù)的結(jié)構(gòu)來(lái)讓樹(shù)保持平衡。
一般情況下,紅黑樹(shù)中新插入的節(jié)點(diǎn)都是紅色的,那么,為什么說(shuō)新加入到紅黑樹(shù)中的節(jié)點(diǎn)要是紅色的呢?
這個(gè)問(wèn)題可以這樣理解,我們從性質(zhì)5中知道,當(dāng)前紅黑樹(shù)中從根節(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ī)則的可能性小一些。
接下來(lái)我們重點(diǎn)來(lái)講紅黑樹(shù)插入新節(jié)點(diǎn)后是如何保持平衡的。
給定下面這樣一顆紅黑樹(shù):
當(dāng)我們插入值為66的節(jié)點(diǎn)的時(shí)候,示意圖如下:
很明顯,這個(gè)時(shí)候結(jié)構(gòu)依然遵循著上述5大特性,無(wú)需啟動(dòng)自動(dòng)平衡機(jī)制調(diào)整節(jié)點(diǎn)平衡狀態(tài)。
如果再向里面插入值為51的節(jié)點(diǎn)呢,這個(gè)時(shí)候紅黑樹(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è)樹(shù)的結(jié)構(gòu)來(lái)保證紅黑樹(shù)的平衡。
首先嘗試將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)整紅黑樹(shù),我們?cè)俅螄L試把45這個(gè)節(jié)點(diǎn)設(shè)置為紅色的,如下圖所示:
這個(gè)時(shí)候我們發(fā)現(xiàn)問(wèn)題又來(lái)了,56-45-43 都是紅色節(jié)點(diǎn)的,出現(xiàn)了紅色節(jié)點(diǎn)相連的問(wèn)題。
于是我們需要再把 56 和 43 設(shè)置為黑色的,如下圖所示。
于是我們把 68 這個(gè)紅色節(jié)點(diǎn)設(shè)置為黑色的。
對(duì)于這種紅黑樹(shù)插入節(jié)點(diǎn)的情況下,我們可以只需要通過(guò)變色就可以保持樹(shù)的平衡了。但是并不是每次都是這么幸運(yùn)的,當(dāng)變色行不通的時(shí)候,我們需要考慮另一個(gè)手段就是旋轉(zhuǎn)了。
例如下面這種情況,同樣還是拿這顆紅黑樹(shù)舉例。
現(xiàn)在這顆紅黑樹(shù),我們現(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è)置為紅色,如下圖所示:
但是,同樣的問(wèn)題,上面這顆紅黑樹(shù)的黑色高度還是不一致,60-68-64-null 和 60-68-64-66-null 這兩條路徑黑色高度還是不一致。
這種情況如果只通過(guò)變色的情況是不能保持紅黑樹(shù)的平衡的。
紅黑樹(shù)的旋轉(zhuǎn)
接下來(lái)我們講講紅黑樹(shù)的旋轉(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)。
文字描述太抽象,接下來(lái)看下圖片展示。
首先斷開(kāi)節(jié)點(diǎn)PL與右子節(jié)點(diǎn)G的關(guān)系,同時(shí)將其右子節(jié)點(diǎn)的引用指向節(jié)點(diǎn)C2;然后斷開(kāi)節(jié)點(diǎn)G與左子節(jié)點(diǎn)C2的關(guān)系,同時(shí)將G的左子節(jié)點(diǎn)的應(yīng)用指向節(jié)點(diǎn)PL。
接下來(lái)再放下 gif 圖,希望能幫助大家更好地理解左旋,圖片來(lái)自網(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)。
右旋的圖片展示:
首先斷開(kāi)節(jié)點(diǎn)G與左子節(jié)點(diǎn)PL的關(guān)系,同時(shí)將其左子節(jié)點(diǎn)的引用指向節(jié)點(diǎn)C2;然后斷開(kāi)節(jié)點(diǎn)PL與右子節(jié)點(diǎn)C2的關(guān)系,同時(shí)將PL的右子節(jié)點(diǎn)的應(yīng)用指向節(jié)點(diǎn)G。
右旋的gif展示(圖片來(lái)自網(wǎng)絡(luò)):
介紹完了左旋和右旋基本操作,我們來(lái)詳細(xì)介紹下紅黑樹(shù)的幾種旋轉(zhuǎn)場(chǎng)景。
左左節(jié)點(diǎn)旋轉(zhuǎn)(插入節(jié)點(diǎn)的父節(jié)點(diǎn)是左節(jié)點(diǎn),插入節(jié)點(diǎn)也是左節(jié)點(diǎn))
如下圖所示的紅黑樹(shù),我們插入節(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))
還是上面這顆紅黑樹(shù),我們?cè)俨迦牍?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))
還是來(lái)上面的圖來(lái)舉例,我們?cè)谶@顆紅黑樹(shù)上插入節(jié)點(diǎn) 70 。
我們可以這樣操作圍繞祖父節(jié)點(diǎn) 66 左旋,再把旋轉(zhuǎn)后的根節(jié)點(diǎn) 69 設(shè)置為黑色,把 66 這個(gè)節(jié)點(diǎn)設(shè)置為紅色。具體可以參看下圖:
紅黑樹(shù)在 Java 中的實(shí)現(xiàn)
Java 中的紅黑樹(shù)實(shí)現(xiàn)類(lèi)是 TreeMap ,接下來(lái)我們嘗試從源碼角度來(lái)逐行解釋 TreeMap 這一套機(jī)制是如何運(yùn)作的。
// TreeMap中使用Entry來(lái)描述每個(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;...} 復(fù)制代碼TreeMap 的put方法。
public V put(K key, V value) {//先以t保存鏈表的root節(jié)點(diǎn)Entry<K,V> t = root;//如果t=null,表明是一個(gè)空鏈表,即該TreeMap里沒(méi)有任何Entry作為rootif (t == null) {compare(key, key); // type (and possibly null) check//將新的key-value創(chuàng)建一個(gè)Entry,并將該Entry作為rootroot = new Entry<>(key, value, null);size = 1;//記錄修改次數(shù)加1modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;//如果比較器cpr不為null,即表明采用定制排序if (cpr != null) {do {//使用parent上次循環(huán)后的t所引用的Entryparent = 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,并返回原有的valuereturn 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;elsereturn 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)elseparent.right = e;//修復(fù)紅黑樹(shù)fixAfterInsertion(e);size++;modCount++;return null;} 復(fù)制代碼 //插入節(jié)點(diǎn)后修復(fù)紅黑樹(shù) 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è)為xx = 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)的是最開(kāi)始例子,沒(méi)有旋轉(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è)為xx = 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; } 復(fù)制代碼TreeMap的插入節(jié)點(diǎn)和普通的排序二叉樹(shù)沒(méi)啥區(qū)別,唯一不同的是,在TreeMap 插入節(jié)點(diǎn)后會(huì)調(diào)用方法fixAfterInsertion(e)來(lái)重新調(diào)整紅黑樹(shù)的結(jié)構(gòu)來(lái)讓紅黑樹(shù)保持平衡。
我們重點(diǎn)關(guān)注下紅黑樹(shù)的fixAfterInsertion(e)方法,接下來(lái)我們來(lái)分別介紹兩種場(chǎng)景來(lái)演示fixAfterInsertion(e)方法的執(zhí)行流程。
第一種場(chǎng)景:只需變色即可平衡
同樣是拿這顆紅黑樹(shù)舉例,現(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ì)得到下面的紅黑樹(shù)結(jié)構(gòu)。
在把 x 重新賦值后,重新進(jìn)入 while 循環(huán),此時(shí)的 x 節(jié)點(diǎn)為 45 。
執(zhí)行上述流程后,得到下面所示的紅黑樹(shù)結(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)過(guò)兩次執(zhí)行while循環(huán)后,我們的紅黑樹(shù)會(huì)調(diào)整成現(xiàn)在這樣的結(jié)構(gòu),這樣的紅黑樹(shù)結(jié)構(gòu)是平衡的,所以路徑的黑高一致,并且沒(méi)有紅色節(jié)點(diǎn)相連的情況。
第二種場(chǎng)景 旋轉(zhuǎn)搭配變色來(lái)保持平衡
接下來(lái)我們?cè)賮?lái)演示第二種場(chǎng)景,需要結(jié)合變色和旋轉(zhuǎn)一起來(lái)保持平衡。
給定下面這樣一顆紅黑樹(shù):
現(xiàn)在我們插入節(jié)點(diǎn)66,得到如下樹(shù)結(jié)構(gòu)。
同樣地,我們進(jìn)入fixAfterInsertion(e)方法。
最終我們得到的紅黑樹(shù)結(jié)構(gòu)如下圖所示:
調(diào)整成這樣的結(jié)構(gòu)我們的紅黑樹(shù)又再次保持平衡了。
演示 TreeMap 的流程就拿這兩種場(chǎng)景舉例了,其他的就不一一舉例了。
紅黑樹(shù)的刪除
因?yàn)橹暗姆窒碇徽砹思t黑樹(shù)的插入部分,本來(lái)想著紅黑樹(shù)的刪除就不整理了,有人跟我反饋說(shuō)紅黑樹(shù)的刪除相對(duì)更復(fù)雜,于是索性還是把紅黑樹(shù)的刪除再整理下。
刪除相對(duì)插入來(lái)說(shuō),的確是要復(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)整紅黑樹(shù)的平衡就可以了。
但是刪除不一樣的是,刪除節(jié)點(diǎn)的時(shí)候我們不能簡(jiǎn)單地把這個(gè)節(jié)點(diǎn)設(shè)置為null,因?yàn)槿绻@個(gè)節(jié)點(diǎn)有子節(jié)點(diǎn)的情況下,不能簡(jiǎn)單地把當(dāng)前刪除的節(jié)點(diǎn)設(shè)置為null,這個(gè)被刪除的節(jié)點(diǎn)的位置需要有新的節(jié)點(diǎn)來(lái)填補(bǔ)。這樣一來(lái),需要分多種情況來(lái)處理了。
刪除節(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)來(lái)代替當(dāng)前需要?jiǎng)h除的節(jié)點(diǎn),然后再把子節(jié)點(diǎn)刪除即可。
給定下面這棵樹(shù),當(dāng)我們需要?jiǎng)h除節(jié)點(diǎn)69的時(shí)候。
首先用子節(jié)點(diǎn)代替當(dāng)前待刪除節(jié)點(diǎn),然后再把子節(jié)點(diǎn)刪除。
最終的紅黑樹(shù)結(jié)構(gòu)如下面所示,這個(gè)結(jié)構(gòu)的紅黑樹(shù)我們是不需要通過(guò)變色+旋轉(zhuǎn)來(lái)保持紅黑樹(shù)的平衡了,因?yàn)閷⒆庸?jié)點(diǎn)刪除后樹(shù)已經(jīng)是平衡的了。
還有一種場(chǎng)景是當(dāng)我們待刪除節(jié)點(diǎn)是黑色的,黑色的節(jié)點(diǎn)被刪除后,樹(shù)的黑高就會(huì)出現(xiàn)不一致的情況,這個(gè)時(shí)候就需要重新調(diào)整結(jié)構(gòu)。
還是拿上面這顆刪除節(jié)點(diǎn)后的紅黑樹(shù)舉例,我們現(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í)候我們樹(shù)的黑高是不一致的,左邊黑高是3,右邊是2,所以我們需要把64節(jié)點(diǎn)設(shè)置為紅色來(lái)保持平衡。
刪除節(jié)點(diǎn)兩個(gè)子節(jié)點(diǎn)都不為空
刪除節(jié)點(diǎn)兩個(gè)子節(jié)點(diǎn)都不為空的情況下,跟上面有一個(gè)節(jié)點(diǎn)不為空的情況下也是有點(diǎn)類(lèi)似,同樣是需要找能替代當(dāng)前節(jié)點(diǎn)的節(jié)點(diǎn),找到后,把能替代刪除節(jié)點(diǎn)值復(fù)制過(guò)來(lái),然后再把替代節(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ū)是左子樹(shù)中最大的節(jié)點(diǎn),后繼則是右子樹(shù)中最小的節(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)不為空的場(chǎng)景下,我們需要再進(jìn)行細(xì)分,主要分為以下三種情況。
第一種,前驅(qū)節(jié)點(diǎn)為黑色節(jié)點(diǎn),同時(shí)有一個(gè)非空節(jié)點(diǎn)
如下面這樣一棵樹(shù),我們需要?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è)紅黑樹(shù)達(dá)到平衡。
第二種,前驅(qū)節(jié)點(diǎn)為黑色節(jié)點(diǎn),同時(shí)子節(jié)點(diǎn)都為空
前驅(qū)節(jié)點(diǎn)是黑色的,子節(jié)點(diǎn)都為空,這個(gè)時(shí)候操作步驟與上面基本類(lèi)似。
如下操作步驟:
因?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è)置為紅色,這樣就能保證紅黑樹(shù)的平衡。
第三種,前驅(qū)節(jié)點(diǎn)為紅色節(jié)點(diǎn),同時(shí)子節(jié)點(diǎn)都為空
給定下面這顆紅黑樹(shù),我們需要?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)即可保持樹(shù)的平衡。
終于把紅黑樹(shù)的基本原理部分寫(xiě)完了,用了很多示意圖,這篇文章是在之前分享的 ppt 上再整理出來(lái),我覺(jué)得自己應(yīng)該算是把基本操作講明白了,整理這篇文章前前后后用了近一周左右,因?yàn)槠綍r(shí)上班,基本上只有周末有時(shí)間才有時(shí)間整理,如有問(wèn)題請(qǐng)留言討論。
如果您覺(jué)得寫(xiě)得還可以,請(qǐng)您幫忙點(diǎn)個(gè)贊,您的點(diǎn)贊真的是對(duì)我最大的支持,也是我能繼續(xù)寫(xiě)下去的動(dòng)力,感謝。
轉(zhuǎn)自:https://juejin.im/post/5df4aa...
怒求一波贊
能堅(jiān)持看到這兒的都是努力學(xué)習(xí)的人,我們相信,努力奮斗終將會(huì)使我們過(guò)上自己想要的生活。
我會(huì)努力更新原創(chuàng)干貨,也會(huì)收集一些精品文章,供大家日常學(xué)習(xí)。不論如何,如果大家覺(jué)得在我這兒能學(xué)到點(diǎn)東西,在這兒厚著臉皮的向大家求個(gè)贊,求個(gè)關(guān)注,求個(gè)分享。我一定不會(huì)辜負(fù)大家,為大家的學(xué)習(xí)之路添加更多精彩的文章。
創(chuàng)作不易,堅(jiān)持不易,大家的支持是我最大的動(dòng)力,再次謝謝大家。
下面這篇文章,是我收集的5000G的精品VIP視頻的部分目錄,都是會(huì)免費(fèi)分享給大家的,大家可以點(diǎn)進(jìn)去看看是否有自己需要的,如果沒(méi)有,大家也可以通過(guò)公眾號(hào)或者微信私聊我,我也會(huì)盡力去收集。
java架構(gòu)師:作為Java開(kāi)發(fā),我是如何在一年之內(nèi),讓自己的月薪爆炸式提升!!?zhuanlan.zhihu.com總結(jié)
以上是生活随笔為你收集整理的红黑树与平衡二叉树_百图详解红黑树,想不理解都难的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: oracle中where中使用函数,Or
- 下一篇: c4d流体插件_(图文+视频)C4D野教