當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > Linux閱碼場(chǎng)
[導(dǎo)讀]大家都聽說(shuō)過(guò)紅黑樹,也都知道紅黑樹很厲害,是計(jì)算機(jī)里面評(píng)價(jià)非常高的數(shù)據(jù)結(jié)構(gòu)。但是每當(dāng)想學(xué)習(xí)紅黑樹的時(shí)候,卻總是找不到通俗易懂很好理解的學(xué)習(xí)資料。很多書上上來(lái)就是紅黑樹的定義,然后就是紅黑樹的實(shí)現(xiàn),直接就把人給整暈了。光看紅黑樹的定義就有5條,為什么要有5條定義,為什么要這么定義,這么定義是什么意思,光定義都讓人懵了,更別說(shuō)實(shí)現(xiàn)了。我看最近抖音上有很多人在講底層邏輯,只要你掌握了底層邏輯,其它的問(wèn)題都不在話下,今天我們也來(lái)講一講紅黑樹的底層邏輯。在講之前我們先介紹一下紅黑樹的誕生,紅黑樹是Rudolf Bayer在1972年首先提出來(lái)的,不過(guò)當(dāng)時(shí)并不叫紅黑樹,而是叫對(duì)稱二叉 B 樹(symmetric binary B-trees)。后來(lái)在1978年Leo J. Guibas 和 Robert Sedgewick 對(duì)此數(shù)據(jù)結(jié)構(gòu)進(jìn)行了修改和完善,并重新命名為紅黑樹。為什么叫紅黑樹呢?有兩種說(shuō)法,因?yàn)榧t黑樹中要對(duì)節(jié)點(diǎn)連接做兩種顏色的區(qū)分,一說(shuō)是因?yàn)楫?dāng)時(shí)的書寫筆只有紅色和黑色兩種顏色,另一說(shuō)是當(dāng)時(shí)的打印機(jī)只有紅和黑兩種顏色。

作者簡(jiǎn)介:

程磊,某手機(jī)大廠系統(tǒng)開發(fā)工程師,閱碼場(chǎng)榮譽(yù)總編輯,最大的愛(ài)好是鉆研Linux內(nèi)核基本原理。


目錄:

一、紅黑樹的底層邏輯

1.1 紅黑樹的本質(zhì)

1.2 B樹簡(jiǎn)介

二、2-3 B樹的操作邏輯

2.1 2-3 B樹的插入操作

2.2 2-3 B樹的刪除操作

三、紅黑樹的操作邏輯

3.1 紅黑樹的定義

3.2 紅黑樹的旋轉(zhuǎn)與翻色

3.3 紅黑樹的插入操作

3.4 紅黑樹的刪除操作

四、紅黑樹的實(shí)現(xiàn)

4.1 結(jié)構(gòu)體定義

4.2 翻色與旋轉(zhuǎn)代碼

4.3 插入操作的代碼

4.4 刪除操作的代碼

五、總結(jié)回顧



一、紅黑樹的底層邏輯


大家都聽說(shuō)過(guò)紅黑樹,也都知道紅黑樹很厲害,是計(jì)算機(jī)里面評(píng)價(jià)非常高的數(shù)據(jù)結(jié)構(gòu)。但是每當(dāng)想學(xué)習(xí)紅黑樹的時(shí)候,卻總是找不到通俗易懂很好理解的學(xué)習(xí)資料。很多書上上來(lái)就是紅黑樹的定義,然后就是紅黑樹的實(shí)現(xiàn),直接就把人給整暈了。光看紅黑樹的定義就有5條,為什么要有5條定義,為什么要這么定義,這么定義是什么意思,光定義都讓人懵了,更別說(shuō)實(shí)現(xiàn)了。我看最近抖音上有很多人在講底層邏輯,只要你掌握了底層邏輯,其它的問(wèn)題都不在話下,今天我們也來(lái)講一講紅黑樹的底層邏輯。在講之前我們先介紹一下紅黑樹的誕生,紅黑樹是Rudolf Bayer在1972年首先提出來(lái)的,不過(guò)當(dāng)時(shí)并不叫紅黑樹,而是叫對(duì)稱二叉 B 樹(symmetric binary B-trees)。后來(lái)在1978年Leo J. Guibas 和 Robert Sedgewick 對(duì)此數(shù)據(jù)結(jié)構(gòu)進(jìn)行了修改和完善,并重新命名為紅黑樹。為什么叫紅黑樹呢?有兩種說(shuō)法,因?yàn)榧t黑樹中要對(duì)節(jié)點(diǎn)連接做兩種顏色的區(qū)分,一說(shuō)是因?yàn)楫?dāng)時(shí)的書寫筆只有紅色和黑色兩種顏色,另一說(shuō)是當(dāng)時(shí)的打印機(jī)只有紅和黑兩種顏色。


1.1 紅黑樹的本質(zhì)


什么是紅黑樹,紅黑樹的本質(zhì)是什么?一句話就可以說(shuō)清楚,紅黑樹是二叉樹的身體、2-3 B樹的靈魂,用計(jì)算機(jī)的語(yǔ)言來(lái)說(shuō)就是,紅黑樹是二叉樹的存儲(chǔ)結(jié)構(gòu)、2-3 B樹的操作邏輯。那么紅黑樹為什么是這樣的呢?這就和遺傳學(xué)中的道理是一樣的了,是為了取得雜交優(yōu)勢(shì),既繼承父本的優(yōu)勢(shì)又繼承母本的優(yōu)勢(shì),同時(shí)又拋棄了父本和母本的劣勢(shì)。我們把二叉樹看成是母本,2-3 B樹看成是父本,母本的優(yōu)勢(shì)就是存儲(chǔ)結(jié)構(gòu)簡(jiǎn)單,還有比二叉樹更簡(jiǎn)單的樹形結(jié)構(gòu)嗎,沒(méi)有了。父本的優(yōu)勢(shì)就是B樹是絕對(duì)平衡樹,任何時(shí)候都是絕對(duì)平衡的。但是父本的劣勢(shì)也是因于此,為了實(shí)現(xiàn)絕對(duì)平衡,B樹的存儲(chǔ)結(jié)構(gòu)比較復(fù)雜,當(dāng)然操作邏輯也比較復(fù)雜。而二叉樹雖然存儲(chǔ)結(jié)構(gòu)簡(jiǎn)單,操作也簡(jiǎn)單,但是它最大的缺點(diǎn)就是不一定平衡,一棵樹如果不平衡,它的操作復(fù)雜度就會(huì)從O(logn)退化為O(n),這是一個(gè)很嚴(yán)重的問(wèn)題。為了實(shí)現(xiàn)一個(gè)既平衡又相對(duì)簡(jiǎn)單的樹形結(jié)構(gòu),于是有人就想到了把二叉樹和2-3 B樹給結(jié)合起來(lái),取二叉樹的存儲(chǔ)結(jié)構(gòu)和2-3 B樹的操作邏輯,用二叉樹來(lái)模擬2-3 B樹,于是紅黑樹就誕生了,這樣紅黑樹就既實(shí)現(xiàn)了存儲(chǔ)結(jié)構(gòu)簡(jiǎn)單又實(shí)現(xiàn)了平衡的效果。紅黑樹的定義也就比較好理解了,就是為了保證紅黑樹在邏輯上是一顆2-3 B 樹。我們這里暫時(shí)先不講紅黑樹的定義,我們先來(lái)講一講2-3 B樹,當(dāng)你把B樹的邏輯理解透了,那么掌握紅黑樹的定義和實(shí)現(xiàn)就易如反掌。這里說(shuō)的二叉樹具體來(lái)說(shuō)是二叉搜索樹,下文也會(huì)簡(jiǎn)單地講一下。


1.2 B樹簡(jiǎn)介


樹形結(jié)構(gòu)首先可以分為等叉樹和不等叉樹,等叉樹是每個(gè)節(jié)點(diǎn)的鍵值個(gè)數(shù)都相同、子節(jié)點(diǎn)個(gè)數(shù)也都相同,不等叉樹是每個(gè)節(jié)點(diǎn)的鍵值個(gè)數(shù)不一定相同、子節(jié)點(diǎn)個(gè)數(shù)也不一定相同。


最簡(jiǎn)單的等叉樹是二叉樹,直接二叉樹的作用并不大,我們一般會(huì)要求二叉樹所有的節(jié)點(diǎn)按照一定的順序排列,這樣我們進(jìn)行插入、刪除、查找時(shí)效率就會(huì)非常高,我們把這樣的樹叫做二叉搜索樹或者二叉查找樹。它的具體定義是這樣的,二叉搜索樹,要么是個(gè)空樹,要么符合以下幾個(gè)條件,1.左子樹如果存在的話,左子樹所有節(jié)點(diǎn)的鍵值都要小于根節(jié)點(diǎn)的鍵值,2.右子樹如果存在的話,右子樹所有節(jié)點(diǎn)的鍵值都要大于根節(jié)點(diǎn)的鍵值,3.它的所有子樹也都要符合前面的兩個(gè)條件(前面的小于同時(shí)換成大于也成立)。經(jīng)過(guò)這樣定義之后,二叉樹就變成了二叉搜索樹,它的插入、刪除、查找效率一般情況下都是O(logn)。等叉樹還有三叉樹、四叉樹、五叉樹等,但是它們和二叉樹相比,除了更復(fù)雜以外,好像也沒(méi)有啥優(yōu)點(diǎn),所以很少聽到有人用過(guò)。


不等叉樹和等叉樹相比除了更省空間以外,好像也沒(méi)啥特別的用處,但是如果我們對(duì)不等叉樹的節(jié)點(diǎn)鍵值數(shù)和插入、刪除邏輯添加一些特殊的要求,使其能達(dá)到絕對(duì)平衡的效果,我們就把這種樹叫做B樹。B樹全稱Balance Tree,是一種自平衡樹。它和等叉樹最大的不同首先表現(xiàn)在存儲(chǔ)結(jié)構(gòu)上,等叉樹上每個(gè)節(jié)點(diǎn)的鍵值數(shù)和分叉數(shù)都是相同的,而B樹則不是。如果某個(gè)B樹上所有節(jié)點(diǎn)的分叉數(shù)最大值是m,則把這個(gè)B數(shù)叫做m階B樹。下面我們來(lái)看一下B樹的具體定義:


  1. 所有節(jié)點(diǎn)最多有m個(gè)子節(jié)點(diǎn)。

  2. 非根非葉子節(jié)點(diǎn)至少有m/2(向上取整)個(gè)子節(jié)點(diǎn)。

  3. 根節(jié)點(diǎn)至少有兩個(gè)子節(jié)點(diǎn)(除非總結(jié)點(diǎn)數(shù)都不夠3個(gè))。

  4. 所有葉子節(jié)點(diǎn)都在同一層。

  5. 任意節(jié)點(diǎn)如果有k個(gè)鍵值,則有k+1個(gè)子節(jié)點(diǎn)指針,鍵值要按照從小到大排列,子節(jié)點(diǎn)樹上所有的鍵值都要在對(duì)應(yīng)的兩個(gè)鍵值之間。


B樹的定義好像很復(fù)雜,但是仔細(xì)分析一下也不復(fù)雜,可以分為3類。一是對(duì)子節(jié)點(diǎn)的個(gè)數(shù)進(jìn)行限制,包括1、2、3三條,1是限制最大子節(jié)點(diǎn)數(shù)的,這也是m階B樹的m的由來(lái),2是限制非根非葉子節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù),葉子節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù)是0,所以才叫葉子,沒(méi)啥限制的,根比較特殊,下一條說(shuō),普通節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù)至少要是m/2(向上取整)個(gè),這么要求是為了提高樹的緊湊性,避免樹變得過(guò)于瘦高,3是限制根節(jié)點(diǎn)的子節(jié)點(diǎn)個(gè)數(shù),要求根節(jié)點(diǎn)至少有2個(gè)子節(jié)點(diǎn)。二是要求整個(gè)樹是要有序的,是5,第5條雖然不好用語(yǔ)言描述,但是它的意思是很簡(jiǎn)單明確的,就是鍵值要從小到大排序,鍵值之間的子節(jié)點(diǎn)的值也要在兩個(gè)鍵值之間,如果是兩端的,則要小于最小鍵值,或者大于最大鍵值。三是對(duì)樹高的要求,是4,第4條說(shuō)的是所有葉子都在同一層,也就是說(shuō)每一個(gè)葉子的深度都是相同的,這也是整個(gè)樹的高度,這一條直接規(guī)定了B樹是一個(gè)絕對(duì)平衡樹,不會(huì)出現(xiàn)退化成線性結(jié)構(gòu)的可能,所以B樹的效率一直是O(logn)的,沒(méi)有例外情況。

B樹的5條定義,其它的都好實(shí)現(xiàn),就第4條比較難,而它也是B樹保持絕對(duì)平衡的關(guān)鍵。那么第4條如何實(shí)現(xiàn)呢?這就要對(duì)它的插入和刪除做特殊規(guī)定了。插入的時(shí)候只能在葉子節(jié)點(diǎn)進(jìn)行插入,如果插入后葉子節(jié)點(diǎn)滿了,則會(huì)對(duì)這個(gè)葉子節(jié)點(diǎn)進(jìn)行分裂操作,選取中間鍵值把這個(gè)葉子一分為三,小于這個(gè)鍵值的重新組成一個(gè)節(jié)點(diǎn),大于這個(gè)鍵值的重新組成一個(gè)節(jié)點(diǎn),然后把這個(gè)中間鍵值送給父節(jié)點(diǎn)。如果父節(jié)點(diǎn)沒(méi)有滿,則插入操作結(jié)束。如果父節(jié)點(diǎn)也滿了,則遞歸此操作,直至根節(jié)點(diǎn)。如果根節(jié)點(diǎn)也滿了,則對(duì)根節(jié)點(diǎn)進(jìn)行分裂,生成新的根節(jié)點(diǎn),這時(shí)樹的高度就會(huì)增加1,由于是在根部增長(zhǎng),所以所有節(jié)點(diǎn)的高度都增加了,整個(gè)樹還是平衡的??偨Y(jié)起來(lái),B樹插入的特點(diǎn)就是底部插入、根部增長(zhǎng),而二叉樹是底部插入、底部增長(zhǎng),時(shí)間長(zhǎng)了容易生長(zhǎng)不平衡,B樹則沒(méi)有這種煩惱,它一直都是平衡的。雖然B樹的插入一直是平衡的,但是如果刪除操作是直接執(zhí)行的,也會(huì)導(dǎo)致B樹被刪的不平衡了,所以B樹的刪除也要特殊操作才行。B樹的刪除更加復(fù)雜,我們首先考慮如果被刪除的鍵值不在葉子節(jié)點(diǎn)上,我們找到它的后繼,它的后繼一定是在葉子節(jié)點(diǎn)上,然后用后繼覆蓋它,再去刪除這個(gè)后繼,然后就是葉子節(jié)點(diǎn)的刪除了。如果葉子節(jié)點(diǎn)里面的鍵值個(gè)數(shù)足夠,刪除一個(gè)也滿足B樹要求的個(gè)數(shù),則直接刪除,沒(méi)有問(wèn)題。如果自己的鍵值數(shù)不夠了,則要考慮向臨近的兄弟節(jié)點(diǎn)借,借的時(shí)候要經(jīng)過(guò)父節(jié)點(diǎn)轉(zhuǎn)手一次,并不是直接借人家的節(jié)點(diǎn)值,而是兄弟節(jié)點(diǎn)給父節(jié)點(diǎn)一個(gè)合適的鍵值,父節(jié)點(diǎn)再拿一個(gè)合適的鍵值給自己。如果兄弟節(jié)點(diǎn)也沒(méi)有多余的鍵值可以借了,那就要向父節(jié)點(diǎn)借一個(gè)元素并和兄弟節(jié)點(diǎn)進(jìn)行合并處理,自己和左兄弟節(jié)點(diǎn)或者右兄弟節(jié)點(diǎn)再加上父節(jié)點(diǎn)的一個(gè)鍵值,合并成一個(gè)新的節(jié)點(diǎn)。如果父節(jié)點(diǎn)被借走了一個(gè)鍵值之后,鍵值數(shù)也小于要求了,則繼續(xù)此過(guò)程,直至最后向根節(jié)點(diǎn)借,如果根節(jié)點(diǎn)也被借沒(méi)了,則新合并的節(jié)點(diǎn)成為根節(jié)點(diǎn),B樹的高度減1。


我們對(duì)B樹有了基本的了解,對(duì)B樹的插入刪除操作也有了大概的認(rèn)識(shí),但是可能還不是很清晰,下一章我們以2-3 B樹為例,詳細(xì)地講一講B樹的操作邏輯。



二、2-3 B樹的操作邏輯


2-3 B樹是最簡(jiǎn)單的B樹,它就是3階B樹,由于它的子節(jié)點(diǎn)個(gè)數(shù)是2或者3,所以大家都叫它2-3 B樹。同理,4階B樹也被大家廣泛的叫做2-3-4 B樹,2-3-4 B樹和2-3 B樹差別并不大,因?yàn)橐粋€(gè)4節(jié)點(diǎn)可以很輕松的轉(zhuǎn)化為2個(gè)2節(jié)點(diǎn),所以本文中就用2-3 B樹的邏輯來(lái)實(shí)現(xiàn)紅黑樹,不再討論2-3-4 B樹的情況了。
2-3 B樹就是3階B樹,我們把m階B樹的定義,把m=3代入進(jìn)來(lái)看一下:


  1. 所有節(jié)點(diǎn)最多有3個(gè)子節(jié)點(diǎn)。

  2. 非根非葉子節(jié)點(diǎn)至少有3/2(向上取整)= 2個(gè)子節(jié)點(diǎn)。

  3. 根節(jié)點(diǎn)至少有兩個(gè)子節(jié)點(diǎn)(除非總結(jié)點(diǎn)數(shù)都不夠3個(gè))。

  4. 所有葉子節(jié)點(diǎn)都在同一層。

  5. 任意節(jié)點(diǎn)有k(1或者2)個(gè)鍵值,則有k+1個(gè)子節(jié)點(diǎn)指針,鍵值要按照從小到大排列,子節(jié)點(diǎn)樹上所有的鍵值都要在對(duì)應(yīng)的兩個(gè)鍵值之間。


這個(gè)定義和B樹的定義差別并不大,也沒(méi)有多少簡(jiǎn)化,只不過(guò)是值具體化了而已。從這個(gè)定義中我們可以看出,2-3 B樹中一共有兩個(gè)類型的節(jié)點(diǎn),2節(jié)點(diǎn)(單元素節(jié)點(diǎn)),3節(jié)點(diǎn)(雙元素節(jié)點(diǎn)),后面為了敘述方便我們可能會(huì)混用這兩對(duì)術(shù)語(yǔ),后文也會(huì)混用元素和鍵值這對(duì)詞。我們先來(lái)看一個(gè)具體的2-3 B樹,先有個(gè)大概的認(rèn)識(shí)。
這是一個(gè)由0-9依次插入而形成的2-3 B樹,圖中的節(jié)點(diǎn)都是2節(jié)點(diǎn)和3節(jié)點(diǎn),所有葉子節(jié)點(diǎn)都在最底層,高度是一樣的,3的左子樹都小于3,右子樹都大于3,對(duì)于節(jié)點(diǎn)57來(lái)說(shuō),4小于5, 6大于5小于7, 8、9都大于7,符合B樹的定義,B樹的第5條定義雖然不好描述的,但是看圖的話還是比較好理解的。但是B樹的插入和刪除操作是不太好理解的,下面我們用兩節(jié)時(shí)間來(lái)詳細(xì)講一講。


2.1 2-3 B樹的插入操作


B樹的插入并不是直接創(chuàng)建新節(jié)點(diǎn),而是尋找在合適位置的葉子節(jié)點(diǎn)中插入元素,把自己的鍵值加入到這個(gè)葉子節(jié)點(diǎn)中去,如果這個(gè)葉子節(jié)點(diǎn)原本就只有一個(gè)元素,那么插入之后正好兩個(gè)元素,符合2-3 B樹的要求,插入完成。如果這個(gè)葉子節(jié)點(diǎn)已經(jīng)有兩個(gè)元素了,插入后就有三個(gè)元素,那么就把中間的元素送給父節(jié)點(diǎn),自己分裂成兩個(gè)節(jié)點(diǎn)。再看父節(jié)點(diǎn),如果接收之后父節(jié)點(diǎn)也變成了3元素節(jié)點(diǎn),那么就重復(fù)這個(gè)過(guò)程,直到根節(jié)點(diǎn)。如果此時(shí)根節(jié)點(diǎn)也是3元素節(jié)點(diǎn),那么就把中間的鍵值拿出來(lái)創(chuàng)建新節(jié)點(diǎn)并成為新的根節(jié)點(diǎn),原來(lái)的根節(jié)點(diǎn)分裂成兩個(gè)節(jié)點(diǎn)掛在新的根節(jié)點(diǎn)上,整個(gè)2-3 B樹的高度就增加了1。語(yǔ)言描述不太好懂的,下面我們用畫圖的方式詳細(xì)講一下把0-9依次插入一顆2-3 B樹的過(guò)程。

整個(gè)2-3 B樹的插入過(guò)程還是很簡(jiǎn)單的,都是在尋找到自己對(duì)應(yīng)位置的節(jié)點(diǎn),如果那個(gè)節(jié)點(diǎn)本來(lái)只有一個(gè)元素,再加入一個(gè)元素也沒(méi)有問(wèn)題,過(guò)程就就結(jié)束了。如果那個(gè)節(jié)點(diǎn)本來(lái)有2個(gè)元素就麻煩了,3個(gè)元素?cái)D在一起實(shí)在是太擠了,待不下,于是就把中間的元素送給父節(jié)點(diǎn),自己這個(gè)節(jié)點(diǎn)分成兩個(gè)節(jié)點(diǎn),左元素和右元素各占一個(gè)節(jié)點(diǎn),新建立的兩個(gè)節(jié)點(diǎn)連接到父節(jié)點(diǎn)上。如果父節(jié)點(diǎn)此時(shí)也變成了3個(gè)元素,那就繼續(xù)這一過(guò)程,直到根節(jié)點(diǎn)。如果到根節(jié)點(diǎn)還是3個(gè)元素的話,把中間的鍵值單獨(dú)建立節(jié)點(diǎn),成為新的根節(jié)點(diǎn),原來(lái)的根節(jié)點(diǎn)一分為二,分別連接到新的根節(jié)點(diǎn)上,這個(gè)新建立的中間節(jié)點(diǎn)成為新的根節(jié)點(diǎn)。


2.2 2-3 B樹的刪除操作


B樹的刪除邏輯是比較復(fù)雜的,首先要考慮的是刪除的是不是葉子節(jié)點(diǎn),如果不是葉子節(jié)點(diǎn)的話,先把它轉(zhuǎn)化成葉子節(jié)點(diǎn),轉(zhuǎn)化的方式是找到它的后繼元素,然后把后繼元素的值復(fù)制給自己,然后把它的后繼元素給刪了。這里面有兩個(gè)問(wèn)題,為什么這么操作是對(duì)的,怎么尋找后繼元素。這個(gè)留在本節(jié)的最后來(lái)回答。如果是葉子節(jié)點(diǎn)的話,那刪除就相對(duì)簡(jiǎn)單了,如果這個(gè)葉子節(jié)點(diǎn)是雙元素節(jié)點(diǎn)的話,那就更簡(jiǎn)單了,直接把元素刪除了就行了,不會(huì)破壞2-3 B樹的任何性質(zhì)。如果葉子節(jié)點(diǎn)是單元素節(jié)點(diǎn)的話,就比較復(fù)雜了,直接刪除的話就違背了2-3 B樹的性質(zhì)了。為此我們需要先借一個(gè)元素過(guò)來(lái),要在不破壞2-3 B樹的性質(zhì)的情況下借,借到之后我們就變成了一個(gè)雙元素節(jié)點(diǎn)了,就可以直接刪除元素了。借的話也分幾種情況,先給相鄰的兄弟節(jié)點(diǎn)借,如果兄弟節(jié)點(diǎn)有兩個(gè)元素的話就可以借,借的方式不是直接拿過(guò)來(lái),而是把被借的元素給父節(jié)點(diǎn),父節(jié)點(diǎn)再把另一個(gè)元素借給自己,被借和借到的元素不是同一個(gè),這么做的目的是為了保持2-3 B樹整體上仍然是有序樹。如果從相鄰的兄弟節(jié)點(diǎn)借不到,那就從父節(jié)點(diǎn)借,如果父節(jié)點(diǎn)有兩個(gè)元素的話就可以直接借,借來(lái)之后并不是直接加入到自己的節(jié)點(diǎn)中,而是自己和借來(lái)的元素和一個(gè)相鄰的兄弟節(jié)點(diǎn)合并成一個(gè)新節(jié)點(diǎn),這么做是因?yàn)楦腹?jié)點(diǎn)少了一個(gè)元素就少了一個(gè)子指針,所以得合并子節(jié)點(diǎn)才行。如果父節(jié)點(diǎn)是也是單元素節(jié)點(diǎn)的話怎么辦呢,沒(méi)有辦法,強(qiáng)行把父節(jié)點(diǎn)的一個(gè)元素借過(guò)來(lái),這時(shí)父節(jié)點(diǎn)空了,它就需要繼續(xù)借,如果能從它的兄弟節(jié)點(diǎn)或者父節(jié)點(diǎn)借來(lái)就沒(méi)事了,如果也借不來(lái)的話,就再?gòu)?qiáng)行向父節(jié)點(diǎn)的父節(jié)點(diǎn)借,一直持續(xù)這個(gè)過(guò)程,直到根節(jié)點(diǎn)。如果根節(jié)點(diǎn)也被借空了,那么整個(gè)2-3 B樹的高度就會(huì)減少1。


我們同樣以畫圖的方式把2-3 B樹的刪除過(guò)程給演示一遍。

這幾幅圖詳細(xì)地講解了如何把前面建立的2-3 B樹按照9-0的鍵值順序依次刪除。但是沒(méi)有刪除中間節(jié)點(diǎn)的情況,我們?cè)倥e例說(shuō)明一下刪除中間節(jié)點(diǎn)的情況。

我們來(lái)解釋一下前面留下來(lái)的疑問(wèn),為啥用后繼元素覆蓋被刪除元素然后刪除后繼元素是可行的。2-3 B樹是一顆排序樹,用后繼元素覆蓋被刪除元素后,不考慮后繼元素節(jié)點(diǎn),整個(gè)樹還是一顆排序樹,但是可能不是一顆2-3 B樹了,所以對(duì)后繼元素節(jié)點(diǎn)走個(gè)刪除流程,就能保證整個(gè)樹還是2-3 B樹。后繼元素如何尋找呢?首先我們可以發(fā)現(xiàn)2-3 B樹是一個(gè)完整的樹,也是說(shuō)它的內(nèi)部節(jié)點(diǎn)如果是一個(gè)單元素節(jié)點(diǎn)它一定存在兩個(gè)子節(jié)點(diǎn),如果是一個(gè)雙元素節(jié)點(diǎn),它一定存在3個(gè)子節(jié)點(diǎn)。對(duì)于任意內(nèi)部節(jié)點(diǎn)的元素,我們先到這個(gè)元素緊鄰的右子節(jié)點(diǎn),然后在這顆子節(jié)點(diǎn)樹上一直順著最左子節(jié)點(diǎn)找到底,就能找到我們想要的后繼元素了。



三、紅黑樹的操作邏輯


當(dāng)你明白了2-3 B樹的操作邏輯之后,就基本明白了紅黑樹的操作邏輯,紅黑樹就是對(duì)2-3 B樹的模擬,2-3 B樹的一個(gè)節(jié)點(diǎn)可能有兩個(gè)元素,而二叉樹的每個(gè)節(jié)點(diǎn)都只有一個(gè)元素,那么如何模擬呢,二叉樹給每個(gè)節(jié)點(diǎn)加了一個(gè)顏色屬性,黑色就代表是正常的連接(圖中的黑色都是用的藍(lán)色),紅色就代表是內(nèi)部連接,也就是原來(lái)的雙元素節(jié)點(diǎn)被分成了兩個(gè)節(jié)點(diǎn),它們之間用紅色連接,代表二者本來(lái)是同一個(gè)節(jié)點(diǎn)的,下面我們就來(lái)看看這個(gè)模擬的過(guò)程。

我們首先來(lái)看單個(gè)節(jié)點(diǎn)是怎么怎么模擬的:

單個(gè)節(jié)點(diǎn)的轉(zhuǎn)換是很簡(jiǎn)單的,單元素節(jié)點(diǎn)還是單元素節(jié)點(diǎn),雙元素節(jié)點(diǎn)就有兩種轉(zhuǎn)換方式了,用哪種方式都可以,為了邏輯簡(jiǎn)單,我們都選擇用左斜的轉(zhuǎn)換方式。我們把前面生成的2-3 B樹轉(zhuǎn)換成紅黑樹試一下。

可以看到這個(gè)轉(zhuǎn)換還是很簡(jiǎn)單的,轉(zhuǎn)換前2-3 B樹是一顆絕對(duì)平衡樹,所有的葉子節(jié)點(diǎn)都在同一層,轉(zhuǎn)換后雖然樹變得不太平衡了,有的葉子節(jié)點(diǎn)高度變高了,但是可以看出葉子最高的高度與最低的高度頂多相差一倍,所以紅黑樹是一顆相對(duì)平衡樹,相對(duì)平衡和絕對(duì)平衡都是平衡,都能保證樹的操作復(fù)雜度是O(logn)。


3.1 紅黑樹的定義


我們把2-3 B樹轉(zhuǎn)換成紅黑樹之后能不能對(duì)這個(gè)紅黑樹任意操作呢?不能,因?yàn)槿我獠僮髦罂赡芫蜔o(wú)法再轉(zhuǎn)換回2-3 B樹了,這就意味著這個(gè)紅黑樹可能不是平衡樹了。因此我們要對(duì)紅黑樹進(jìn)行定義,把對(duì)紅黑樹的操作限制在定義允許的范圍內(nèi),這樣紅黑樹就永遠(yuǎn)是紅黑樹,就一定對(duì)應(yīng)著一顆2-3 B樹,就一定是一顆平衡樹。下面我們來(lái)看一下紅黑樹的定義。


  1. 所有的節(jié)點(diǎn)不是黑色的就是紅色的

  2. 根節(jié)點(diǎn)是黑色的

  3. 所有葉子節(jié)點(diǎn)是黑色的

  4. 從每個(gè)葉子到根的所有路徑上不能有兩個(gè)連續(xù)的紅色結(jié)點(diǎn)

  5. 從任一結(jié)點(diǎn)到其每個(gè)葉子的所有路徑都包含相同數(shù)目的黑色結(jié)點(diǎn)


我們來(lái)分析這5條定義,1.所有節(jié)點(diǎn)都是黑色的或者紅色的,這是肯定的了,要不然能叫紅黑樹嘛。2.根節(jié)點(diǎn)是黑色的,這是肯定的了,紅色節(jié)點(diǎn)的含義是自己與父節(jié)點(diǎn)的連接是紅色的,也就是說(shuō)自己和父節(jié)點(diǎn)是內(nèi)部節(jié)點(diǎn),根節(jié)點(diǎn)沒(méi)有父節(jié)點(diǎn),只能是黑色的。3.所有的葉子節(jié)點(diǎn)都是黑色的,紅黑樹里規(guī)定把本來(lái)的每個(gè)葉子節(jié)點(diǎn)的左右子節(jié)點(diǎn)的那個(gè)空指針看做是NULL節(jié)點(diǎn),看成是葉子節(jié)點(diǎn),規(guī)定NULL節(jié)點(diǎn)是黑色的,這個(gè)規(guī)定好像沒(méi)啥用,沒(méi)有看出來(lái)這個(gè)規(guī)定的邏輯意義或者實(shí)現(xiàn)意義。4.從每個(gè)葉子到根的所有路徑上不能有兩個(gè)連續(xù)的紅色結(jié)點(diǎn),這一條是保證2-3 B樹的每個(gè)節(jié)點(diǎn)都是2節(jié)點(diǎn)或者3節(jié)點(diǎn)。5.從任一結(jié)點(diǎn)到其每個(gè)葉子的所有路徑都包含相同數(shù)目的黑色結(jié)點(diǎn),這一條是保證2-3 B樹的所有葉子節(jié)點(diǎn)都是等高的。

有了紅黑樹的這五條定義就能保證紅黑樹永遠(yuǎn)是紅黑樹,永遠(yuǎn)都對(duì)應(yīng)著一顆2-3 B樹,永遠(yuǎn)都是一顆平衡樹。這5條定義怎么記憶呢?我總結(jié)了4個(gè)四字短語(yǔ)來(lái)記憶,非紅即黑、根黑葉黑、紅子不紅、黑徑相等。


紅黑樹的定義只是給我提供了限制,我們的操作不能違背這個(gè)限制,但是紅黑樹的定義并沒(méi)有告訴我們?cè)趺床僮骷t黑樹,只要你的實(shí)現(xiàn)方法符合定義就行,下面我們來(lái)看一看紅黑樹應(yīng)該如何操作。


3.2 紅黑樹的旋轉(zhuǎn)與翻色


們構(gòu)建一顆紅黑樹并不是從2-3 B 樹轉(zhuǎn)換過(guò)來(lái)的,而是從一個(gè)空樹,按照紅黑樹的構(gòu)建規(guī)則不斷地插入和刪除而產(chǎn)生的一顆紅黑樹,在這個(gè)過(guò)程會(huì)出現(xiàn)很多的3元素節(jié)點(diǎn)需要我們來(lái)處理。一個(gè)3元素節(jié)點(diǎn)會(huì)對(duì)應(yīng)怎樣的紅黑樹片段呢,我們會(huì)遇到怎樣的情況呢,遇到了又應(yīng)該怎么處理呢,下面我們來(lái)看一下。

3元素節(jié)點(diǎn)的處理是紅黑樹的重點(diǎn),也是難點(diǎn),掌握了這個(gè)也就基本掌握了紅黑樹。

3元素節(jié)點(diǎn)轉(zhuǎn)換為二叉樹節(jié)點(diǎn)會(huì)有這五種可能的情況,當(dāng)然我們不會(huì)把3元素節(jié)點(diǎn)轉(zhuǎn)換成奇奇怪怪的形態(tài),我們一般會(huì)把3元素節(jié)點(diǎn)轉(zhuǎn)換成標(biāo)準(zhǔn)形態(tài),這樣比較好處理。但是我們?cè)谔幚砑t黑樹的過(guò)程中也會(huì)遇到這四種非標(biāo)準(zhǔn)形態(tài),如果遇到的話,就用下面的方法把它們轉(zhuǎn)為標(biāo)準(zhǔn)形態(tài)再進(jìn)行翻色就可以了。

標(biāo)準(zhǔn)形態(tài)本身并沒(méi)有違背紅黑樹的定義,所以紅黑樹中可以存在標(biāo)準(zhǔn)形態(tài)。但是本文用的是2-3 B樹模型,遇到了標(biāo)準(zhǔn)形態(tài)都會(huì)進(jìn)行翻色處理,所以最終的紅黑樹上不會(huì)有標(biāo)準(zhǔn)形態(tài)的,只有中間的處理過(guò)程中會(huì)產(chǎn)生標(biāo)準(zhǔn)形態(tài)。翻轉(zhuǎn)顏色(color flip)這個(gè)操作,不違背紅黑樹的前三條定義,也不違背黑徑相等的定義,但是有可能違背紅子不紅的定義。這是因?yàn)樗羞^(guò)7 8 的路徑以前有一個(gè)黑徑,現(xiàn)在還是一個(gè),對(duì)于過(guò)8 9的路徑也是如此,翻轉(zhuǎn)前后過(guò)7 8和8 9的路徑和黑色節(jié)點(diǎn)數(shù)不會(huì)發(fā)生變化。翻色分為向上翻色和向下翻色,向上翻色用于插入操作中,有可能會(huì)違背紅子不紅的規(guī)定,因?yàn)?的父節(jié)點(diǎn)有可能還是紅色的,所以對(duì)這個(gè)問(wèn)題要一直往上回溯處理。向下翻色用于刪除操作中,不會(huì)違背紅子不紅的定義。


可以看到非標(biāo)準(zhǔn)形態(tài)可以通過(guò)一次左旋和一次右旋轉(zhuǎn)換成標(biāo)準(zhǔn)形態(tài)。那么自旋又是怎么操作的呢?

從B樹的角度來(lái)看左旋和右旋,左旋右旋改變的是一個(gè)節(jié)點(diǎn)內(nèi)部的連接,并沒(méi)有改變B樹本身任何的性質(zhì)。從二叉樹的角度來(lái)看,左旋右旋不會(huì)改變紅子不紅的問(wèn)題,原來(lái)有紅子不紅的現(xiàn)在還繼續(xù)有,原來(lái)沒(méi)有現(xiàn)在還沒(méi)有。通過(guò)數(shù)過(guò)A B C的路徑的黑徑數(shù)可以發(fā)現(xiàn)其前后不變,所以左旋右旋沒(méi)有違背紅黑樹黑徑相等的定義。當(dāng)我們把旋轉(zhuǎn)和翻色都學(xué)明白之后就可以開始研究紅黑樹是怎么模擬2-3 B樹的插入操作了。


3.3 紅黑樹的插入操作


本文實(shí)現(xiàn)的是左斜紅黑樹,也就是所有的紅色節(jié)點(diǎn)都是左子節(jié)點(diǎn)。紅黑樹插入的時(shí)候節(jié)點(diǎn)默認(rèn)顏色是紅色,這個(gè)和2-3 B樹的所有鍵值只插入葉子節(jié)點(diǎn)是一致的,而且增加一個(gè)末端的紅色節(jié)點(diǎn)不會(huì)違背紅黑樹黑徑相等的定義,但是有可能違背紅色不紅的定義,所以要通過(guò)旋轉(zhuǎn)和翻色來(lái)解決這個(gè)問(wèn)題,并要遞歸解決這個(gè)問(wèn)題,直至根節(jié)點(diǎn)。節(jié)點(diǎn)插入之后首先要先考慮自己是否構(gòu)成3元素節(jié)點(diǎn),如果不構(gòu)成的話就結(jié)束了,如果構(gòu)成的話,就要進(jìn)行處理,構(gòu)成的條件是父節(jié)點(diǎn)是紅色或者父節(jié)點(diǎn)的另一個(gè)子節(jié)點(diǎn)是紅色,如果兩個(gè)子節(jié)點(diǎn)都是紅色就要進(jìn)行翻色處理,如果子節(jié)點(diǎn)和父節(jié)點(diǎn)是紅色節(jié)點(diǎn),則要按照前面說(shuō)的非標(biāo)準(zhǔn)形態(tài)進(jìn)行處理,先轉(zhuǎn)換成標(biāo)準(zhǔn)形態(tài),再進(jìn)行翻色。處理完成之后繼續(xù)對(duì)父節(jié)點(diǎn)進(jìn)行同樣的處理,直至根節(jié)點(diǎn)。


下面我們來(lái)看一看紅黑樹模擬2-3 B樹的插入操作的過(guò)程,加深對(duì)紅黑樹插入操作地理解。







可以看出紅黑樹一直都在進(jìn)行尋找3元素節(jié)點(diǎn),也就是兩個(gè)連續(xù)的紅色或者相鄰的紅色,然后進(jìn)行旋轉(zhuǎn)和翻色操作,直至整個(gè)紅黑樹都符合紅子不紅的定義為止。



3.4 紅黑樹的刪除操作


紅黑樹的刪除操作比較復(fù)雜,分為很多種情況,我們先來(lái)看幾個(gè)示例

紅黑樹的刪除有很多種情況,最簡(jiǎn)單的就是要?jiǎng)h除的節(jié)點(diǎn)是葉子節(jié)點(diǎn)并且是紅色,直接刪除就行了,然后是自己沒(méi)有右子,自己的左子是紅色,和左子做一個(gè)右旋就變成了前面的情況,可以直接把自己刪除。如果這兩種情況都不是,那就想辦法把自己變成紅色節(jié)點(diǎn)了,對(duì)應(yīng)2-3 B樹中的操作就是借元素。紅黑樹的刪除過(guò)程還有一些更復(fù)雜的過(guò)程,我們?cè)谙乱徽聦?shí)現(xiàn)里面再細(xì)說(shuō)。


四、紅黑樹的實(shí)現(xiàn)


理解了上面大部分的邏輯之后,我們就可以開始去實(shí)現(xiàn)紅黑樹了,本文用C語(yǔ)言實(shí)現(xiàn)了2-3 B樹對(duì)應(yīng)的左斜紅黑樹。


4.1 結(jié)構(gòu)體定義


首先我們要定義紅黑樹的結(jié)構(gòu)體和接口。

rbtree.h

enum color {BLACK, RED};struct rbnode{ int value; enum color color; struct rbnode *parent; struct rbnode *left; struct rbnode *right;}; struct rbtree{ struct rbnode *root; int size; int depth;}; int insert(struct rbtree *tree, int value);int delete(struct rbtree *tree, int value);struct rbnode * find(struct rbtree *tree, int value);


這個(gè)文件也很簡(jiǎn)單,定義了紅黑樹節(jié)點(diǎn)rbnode,顏色枚舉值RED BLACK,以及rbtree,rbtree用來(lái)放置紅黑樹的根節(jié)點(diǎn)和一些其它信息。還聲明了紅黑樹的三個(gè)接口函數(shù)insert、delete 和 find。


對(duì)于紅黑樹來(lái)說(shuō),find的實(shí)現(xiàn)很簡(jiǎn)單,二叉搜索樹的查找:
rbtree.c

struct rbnode * find(struct rbtree *tree, int value){ struct rbnode *p = tree->root; while(p){ if (value == p->value) return p; if (value < p->value) p = p->left; else p = p->right; } return NULL;}

紅黑樹本身也是個(gè)二叉搜索樹,所以它的查找函數(shù)和二叉搜索樹的查找是一樣的。


4.2 翻色與旋轉(zhuǎn)代碼


下面我們來(lái)說(shuō)下翻色與旋轉(zhuǎn)函數(shù)的實(shí)現(xiàn)

rbtree.c

static void color_flip_up(struct rbnode *node){ node->color = RED; node->left->color = BLACK; node->right->color = BLACK;} static void color_flip_down(struct rbnode *node){ node->color = BLACK; node->left->color = RED; node->right->color = RED;}

翻色函數(shù)很好理解,向上翻色就是把自己變紅,把左子和右子都變黑,向下翻色正好相反。

再來(lái)看一下旋轉(zhuǎn)函數(shù)
rbtree.c

void rotate_left(struct rbtree *tree, struct rbnode *y){ struct rbnode* x = y->right; if(!x) return;  x->parent = y->parent; if(y->parent){ int flag = y == y->parent->left; flag ? (y->parent->left = x) : (y->parent->right = x); } else { tree->root = x; }  y->right = x->left; if(x->left) x->left->parent = y;  x->left = y; y->parent = x;  enum color c = x->color; x->color = y->color; y->color = c; } void rotate_right(struct rbtree *tree, struct rbnode *y){ struct rbnode *x = y->left; if(!x) return;  x->parent = y->parent; if(y->parent){ int flag = y->parent->left == y; flag ? (y->parent->left = x) : (y->parent->right = x); } else { tree->root = x; }  y->left = x->right; if(x->right) x->right->parent = y;  x->right = y; y->parent = x; enum color color = x->color; x->color = y->color; y->color = color;}


我們以左旋為例講解一下。左旋旋轉(zhuǎn)的是自己和右子,往左旋把自己旋下去了,把右子旋上來(lái)了,右子占據(jù)了自己的位置,自己成了右子的左子。函數(shù)首先做的是交接parent,把右子先掛到parent上去,然后把右子的左子掛接到自己身上,接著讓自己成為右子的左子,最后交換二者的顏色,這么做是為了保持兩者的連接顏色不變。右旋的邏輯是類似的,這里就不再多講解了。


4.3 插入操作的代碼


代碼如下,主要是兩個(gè)函數(shù),insert和insert_fix。

rbtree.c

int insert(struct rbtree *tree, int value){ struct rbnode *p = malloc(sizeof(*p)); if(!p) return -1;  p->value = value; p->color = RED; p->parent = NULL; p->left = NULL; p->right = NULL;  if(!tree->root){ tree->root = p; p->color = BLACK; tree->size++; tree->depth++; return 0; }  struct rbnode *i = tree->root, *parent; int flag; while(i){ parent = i; if(value < i->value){ i = i->left; flag = 1; } else if(value > i->value){ i = i->right; flag = 0; } else { free(p); return -1; } }  p->parent = parent; flag ? (parent->left = p) : (parent->right = p); insert_fix(tree, p); tree->size++; return 0; } void insert_fix(struct rbtree *tree, struct rbnode *node){ struct rbnode *x = node, *y, *tmp; while(x->parent){ y = x->parent;  if(x == y->left){ if(y->color == BLACK) return; rotate_right(tree, y->parent); color_flip_up(y); x = y; } else { if(y->left && y->left->color == RED){ color_flip_up(y); x = y; } else { rotate_left(tree, y); tmp = x, x = y, y = tmp; if(y->color == BLACK) return; rotate_right(tree, y->parent); color_flip_up(y); x = y; } } } if(tree->root->color == RED){ tree->root->color = BLACK; tree->depth++; }}


函數(shù)insert的邏輯很簡(jiǎn)單,就是創(chuàng)建新的節(jié)點(diǎn),新節(jié)點(diǎn)的顏色設(shè)為紅色,按照二叉搜索樹的方式尋找要插入的位置,然后調(diào)用insert_fix,來(lái)保證整個(gè)樹是一個(gè)符合定義的紅黑樹。insert_fix的實(shí)現(xiàn)主體是一個(gè)while循環(huán),當(dāng)x的parent存在時(shí)就一直循環(huán),直到x是根節(jié)點(diǎn)為止,當(dāng)然while內(nèi)部有提前結(jié)束循環(huán)的地方。進(jìn)入循環(huán)體內(nèi),首先我們要明白的就是,此時(shí)x的顏色是RED,這是因?yàn)榈谝淮窝h(huán)的時(shí)候x是RED的,后面循環(huán)再次進(jìn)來(lái)的時(shí)候x也是RED的,如果不是的話就不會(huì)繼續(xù)循環(huán)了。先分x是左子還是右子兩種情況來(lái)討論,當(dāng)x是左子的時(shí)候,看parent是不是RED,如果是BLACK的話,直接return。如果是RED的話,此時(shí)x和y都是左斜的,y是左斜的是因?yàn)槲覀儗?shí)現(xiàn)的是左斜紅黑樹,原有的紅色節(jié)點(diǎn)只可能是左斜的。對(duì)于雙紅色左斜節(jié)點(diǎn)的處理,我把圖片又放到下面了,大家可以再看一看。這種情況我們應(yīng)當(dāng)對(duì)y的parent進(jìn)行右旋,右旋之后,y成了兩個(gè)紅色節(jié)點(diǎn)的父節(jié)點(diǎn),由于旋轉(zhuǎn)之前y是紅色,所以y的parent不可能是紅色,只能是黑色,所以旋轉(zhuǎn)之后交換了顏色,所以此時(shí)y肯定是黑色。y是黑色,左子右子都是紅色,正好可以翻色,翻色之后,左子右子都變成了黑色,y變成了紅色,讓x=y進(jìn)行下一輪循環(huán)。當(dāng)x是右子的時(shí)候,先看一下左兄弟是不是紅色,如果是的話,父節(jié)點(diǎn)y肯定是黑色,正好可以翻色,對(duì)y進(jìn)行翻色,讓x=y,繼續(xù)下一輪循環(huán)。如果x的左兄弟不是紅色的,先左旋y,左旋之后x y的父子關(guān)系變了,交換一下它們的值,讓x仍然是子,y仍然是父。判斷y的顏色,如果是黑色直接return函數(shù)結(jié)束,如果是紅色,x y又形成了雙左斜紅色的情況,和上面的處理方式是一樣的,右旋y的parent,翻色y,讓x=y,繼續(xù)下一輪循環(huán)。函數(shù)最后判斷根節(jié)點(diǎn)是否為紅色,如果為紅色的話,說(shuō)明之前的根節(jié)點(diǎn)經(jīng)歷過(guò)分割,樹的高度增加1,再把根節(jié)點(diǎn)重置為黑色。

4.4 刪除操作的代碼


刪除操作主要有兩個(gè)函數(shù),delete 和 delete_fix,delete比insert稍微復(fù)雜一點(diǎn),要處理被刪除的點(diǎn)不是葉子節(jié)點(diǎn)的情況。對(duì)于非葉子節(jié)點(diǎn)的情況,其右子樹一定存在,用右子樹的最小值,也就是右子樹的最左子節(jié)點(diǎn)的值覆蓋自己,然后刪除右子樹的最左子節(jié)點(diǎn)就可以了。

rbtree.c

int delete(struct rbtree *tree, int value){ if(!tree->root) return -1;  struct rbnode *i = tree->root, *j; while(i){ if(value < i->value){ i = i->left; } else if(value > i->value){ i = i->right; } else { if(!i->left || !i->right){ delete_fix(tree, i); tree->size--; return 0; } j = i->right; while(j->left) j = j->left; i->value = j->value; delete_fix(tree, j); tree->size--; return 0; } } return -1;} void delete_fix(struct rbtree *tree, struct rbnode *node){ if(node->left && node->left->color == RED) rotate_right(tree, node); if(node->color == RED) goto delete;  struct rbnode *self = node, *parent, *brother, *nephew; int red_borrowed = 0; while(self->parent){ parent = self->parent; if(self == parent->left){ // left child brother = parent->right; nephew = brother->left; if(nephew && nephew->color == RED){ nephew->color = BLACK; rotate_right(tree, brother); rotate_left(tree, parent); self->color = RED; if(red_borrowed){ red_borrowed = 0; self->color = BLACK; } break; } else { int old_red_borrowed = red_borrowed; red_borrowed = 0; if(parent->color == BLACK) red_borrowed = 1; color_flip_down(parent); if(old_red_borrowed) self->color = BLACK; rotate_left(tree, parent); if(red_borrowed){ self = brother; continue; } else { break; } } // left child end } else { // right child  brother = parent->left; nephew = brother->left; if(brother && brother->color == RED){ if(brother->right && brother->right->left && brother->right->left->color == RED){ struct rbnode *n = brother->right->left; rotate_right(tree, parent); rotate_right(tree, parent); n->color = BLACK; self->color = RED; if(red_borrowed){ red_borrowed = 0; self->color = BLACK; } rotate_left(tree, brother); break; } else { rotate_right(tree, parent); color_flip_down(parent); if(red_borrowed){ red_borrowed = 0; self->color = BLACK; } break; } } else if(nephew && nephew->color == RED){ rotate_right(tree, parent); nephew->color = BLACK; self->color = RED; if(red_borrowed){ red_borrowed = 0; self->color = BLACK; } break; } else { int old_red_borrowed = red_borrowed; red_borrowed = 0; if(parent->color == BLACK) red_borrowed = 1; color_flip_down(parent); if(old_red_borrowed) self->color = BLACK; if(red_borrowed){ self = parent; continue; } else { break; } } // right child end } }  if(red_borrowed) tree->depth--;  struct rbnode *child; delete: child = node->left ? node->left : node->right; if(node->parent){ int flag = node == node->parent->left; flag ? (node->parent->left = child) : (node->parent->right = child); } else { tree->root = child; if(!tree->root) tree->depth--; } if(child) child->parent = node->parent; free(node);}

函數(shù)delete_fix首先考慮兩種特殊情況,如果node是紅色的,或者node的左子是紅色的,直接右旋自己,把自己變成紅色,然后直接goto delete。剩下的情況就是node不是紅色的,也是說(shuō)node是單元素節(jié)點(diǎn),要想辦法把自己變成紅色的,也就是通過(guò)借元素變成雙元素節(jié)點(diǎn)。進(jìn)入循環(huán)的條件是當(dāng)前元素的parent不為空,循環(huán)內(nèi)部有一個(gè)變量red_borrowed控制著是否繼續(xù)循環(huán),如果為1則循環(huán),如果為0則結(jié)束循環(huán),也就是說(shuō)循環(huán)至少會(huì)執(zhí)行一次,然后x->parent和red_borrowed共同決定是否繼續(xù)循環(huán)。循環(huán)體內(nèi)分為兩種情況,自己是左子和自己是右子的情況,自己是左子的情況又分兩種,自己是右子的情況又分四種。下面通過(guò)畫圖給大家解析一下。字母與代碼中對(duì)應(yīng)的節(jié)點(diǎn)如下:
x:self
y:parent
z:brother
w:nephew(侄子)
注意在每次循環(huán)開始的時(shí)候x都是黑色的。由于我們是左斜紅黑樹,所以z不可能是紅色的,可能的情況只有w是紅色的或者是黑色的(下一種情況),對(duì)于這種情況,我們通過(guò)其對(duì)應(yīng)的2-3 B樹可以看出來(lái)我們應(yīng)該如何把紅黑樹調(diào)整成目標(biāo)的樣子。我們先把w的顏色置黑,因?yàn)樗罱K的顏色是黑色的,提前置黑能避免旋轉(zhuǎn)時(shí)的顏色交換。然后通過(guò)右旋z、左旋y把紅黑樹調(diào)整成目標(biāo)形態(tài),然后把x的顏色置紅,然后再根據(jù)red_borrowed把x置黑。此種情況我們幫x借到了紅色,對(duì)應(yīng)著2-3 B樹中借到了元素,所以循環(huán)結(jié)束。

這種情況是兄弟節(jié)點(diǎn)沒(méi)法借的情況,只有向父節(jié)點(diǎn)借,向父節(jié)點(diǎn)借有兩種情況,一是父節(jié)點(diǎn)是紅色,能借到,循環(huán)就結(jié)束了,二是父節(jié)點(diǎn)是黑色,借不到,只能強(qiáng)行借了,把red_borrowed設(shè)置為1。我們還要考慮上一輪的red_borrowed,如果為1,要把借來(lái)的紅色還了,也就是說(shuō)x剛借來(lái)紅色就還了,x還是黑色的。如果red_borrowed為0的話說(shuō)明是第一次循環(huán),x是要被刪除的元素,所以不存在違背紅子不紅的問(wèn)題。如果這一輪的red_borrowed是1的話則繼續(xù)循環(huán),否則就結(jié)束循環(huán)。

這是一種比較復(fù)雜的情況,對(duì)應(yīng)著2-3 B樹的情況是父節(jié)點(diǎn)和兄弟節(jié)點(diǎn)都有兩個(gè)元素的情況??醇t黑樹比較復(fù)雜,我們看2-3 B樹是怎么操作的,通過(guò)B樹的操作反推紅黑樹應(yīng)該如何操作。第一步我們右旋y,得到了一個(gè)x y o n ,一個(gè)局部有紅色節(jié)點(diǎn)的樹。第二步,只考慮這個(gè)樹,我們最后可以通過(guò)右旋y并調(diào)整顏色的方式把紅色轉(zhuǎn)移給x,這樣我們就達(dá)到目的了。然后我們發(fā)現(xiàn)o的顏色是右斜的,對(duì)z左旋一下,把它調(diào)整為一個(gè)左斜紅黑樹。

這種情況對(duì)應(yīng)的2-3 B樹是父節(jié)點(diǎn)有兩個(gè)元素,向父節(jié)點(diǎn)借一個(gè)元素并和兄弟節(jié)點(diǎn)合并,所以我們先右旋y,然后對(duì)y進(jìn)行翻色,x就變?yōu)榧t色了。通過(guò)前面的分析我們知道,雖然x是紅色右斜的,但是有兩種情況,要么x是被借紅的,所以還會(huì)變成黑色,要么x就是要被刪除的節(jié)點(diǎn),所以不需要把x旋轉(zhuǎn)為左斜的。

這種情況是父節(jié)點(diǎn)只有一個(gè)元素兄弟節(jié)點(diǎn)有兩個(gè)元素,直接向兄弟借元素,一個(gè)右旋y并調(diào)整顏色,就搞定了。

這種情況和左2是相同的,而且還比左2簡(jiǎn)單,由于z是左斜的,所以旋轉(zhuǎn)的動(dòng)作都省了。


五、總結(jié)回顧


紅黑樹是二叉樹和2-3 B樹結(jié)合而成的一種數(shù)據(jù)結(jié)構(gòu),理解紅黑樹的關(guān)鍵在于理解B樹的操作邏輯,理解了B樹就基本理解紅黑樹,紅黑樹的難點(diǎn)在于3元素節(jié)點(diǎn)的旋轉(zhuǎn)和翻色操作。本文先論述了紅黑樹和2-3 B樹之間的關(guān)系,然后用畫圖的方式詳細(xì)講解了2-3 B樹的操作邏輯,接著又用畫圖的方式了講解了紅黑樹是如何模擬2-3 B樹的操作邏輯的,最后用C語(yǔ)言實(shí)現(xiàn)了一個(gè)左斜紅黑樹。


本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉