LLRB——紅黑樹的現代實現


一、本文內容
以一種簡明易懂的方式介紹紅黑樹背后的邏輯實現2-3-4樹,以及紅黑樹的插入、刪除操作,重點在2-3-4樹與紅黑樹的對應關系上,並理清紅黑樹相關操作的來龍去脈。拋棄以往復雜的實現,而分析紅黑樹的一種簡單實現LLRB。
 
 
二、算法應用
紅黑樹,給人以強烈的第一聽覺沖擊力——紅與黑,好像很高端的感覺。事實上的確如此,紅黑樹是一種高級數據結構,在C++、Java的標准庫里作為set、map的底層數據結構實現,以及linux中進程的公平調度。
 
 
三、2-3-4樹
標題是紅黑樹,為什么講2-3-4樹?因為紅黑樹就是2-3-4樹的一種等價形式,更准確地來說,我們用紅黑樹來完成2-3-4樹的各種操作(如插入、刪除)。原因就是2-3-4樹的實現即維護太麻煩。所以理解2-3-4樹才能真正理解紅黑樹。而歷史就是這么發展的,了解過去,現在的一切才有了意義。算法導論關於紅黑樹這一節就忽略了這一點,讓人知其然而不知其所以然。
 
OK,暫時先忽略復雜的紅黑樹,從簡單的2-3-4樹開始。
 
1、定義
 
2-3-4樹是一種泛化的BST,它的每個結點允許1,2或者3個鍵(key),那么對應的有三種結點:
2-node:一個key,兩個孩子;
3-node:二個key,三個孩子;
4-node:三個key,四個孩子。
注:k-node表示有k個鏈接(link)。泛化的BST還有2-3樹,B樹等。
 
從圖中可以看出2-3-4樹的另一個性質:它是完全平衡的(等高),即從根結點到葉子結點距離相等。
 
2、插入操作
2-3-4樹本身就是一種查找樹(中序遍歷有序),故其查找操作同二叉查找。
 
2-3-4樹的插入操作類似二叉查找樹,先是查找操作失敗(從根結點查找到葉子結點),然后在底部的葉子結點插入。
因為2-3-4樹的結點有三種類型,所以操作有點差異。對於2-node和3-node,分別直接插入可變成3-node,4-node;但是對於4-node若直接插入則違反了定義。在4-node插入之前,先分裂4-node成2個2-node,再將待插入的key插入對應的2-node。 如下圖,H查找失敗,在H插入4-node(由三個key F、G、J組成)之前,先對該4-node分裂(將三個key的中間值提上父節點,剩余的二個key分別作為中間key的左右孩子),然后再將H插入2-node J中。這樣操作的結果是查找到達底部葉子結點時,始終是2-node或者3-node。
 
插入算法思想:自下而上的算法由原作者Bayer在1972年提出,自上而下的算法由Guibas-Sedgewick(紅黑樹這個名字來源於他們)在1978年提出,然后30年后也就是2008年Sedgewick教授又改進了紅黑樹的操作,也就是后面要介紹的LLRB。
 
自上而下的算法思路是,從根結點向下的查找過程中,遇到4-node就分裂,最后在底部的葉子結點插入。
那么為什么遇到4-node就分裂呢?4-node不是2-3-4樹的一種合法結點類型嗎?
答案可以從后面LLRB的算法思路可以得出。
 
因為遇到4-node就分裂就保證了當前結點不是4-node,則分裂孩子的4-node有兩種情形:
分裂4-node的case 1
 
 
分裂4-node的case 2

注:上面的變換在樹中任意位置都成立。
 
 
下面兩張圖是完整的插入過程(只有分裂結點類型為4-node的根結點才會導致樹高增1):
 
 
 
 
3、平衡性分析
2-3-4樹的樹高在最壞情況下為lgN(所有結點都是2-node型),最好情況下為lg4 N = 1/2 lgN(所有結點都是4-node型),2-3-4樹的查找、插入操作都是lgN。
 
 
四、紅黑樹
 
終於到了高富帥——紅黑樹。。。
從2-3-4樹的介紹可以看出,對2-node、3-node、4-node的不同數據類型進行轉換,但所涉及的大部分任務使用這種 直接的表示方法來實現並不方便。所以可以用
一種 統一的方式完成轉換,而只需很小的開銷。這就是紅黑樹存在的意義,既有BST的標准搜索過程,又有2-3-4樹的簡單插入平衡過程。
 
下面介紹LLRB(Left-leaning red-black trees),而不是標准的紅黑樹。
1、定義
LLRB有三個特點:
(1)用BST來表示2-3-4樹;
(2)用紅邊(紅鏈接)來連接2-node來表示3-node和4-node(如下圖);
(3)3-node必須是向左傾斜的(兩者的大者作為根)。
 
LLRB相對於標准的RB多了特點3,在標准的RB中右向傾斜的紅鏈接是允許的。對於特點2,在物理上用一個bit(紅或黑)來存儲以表示指向該結點的紅鏈接。
紅鏈接來連接3-node或者4-node的內部key,而黑鏈接則連接外部的key;為了理解,可以消除紅鏈接並將它們連接的結點都折疊起來(即將看做紅鏈接連
接的點縮為一個點),則可以看出黑鏈接個數不變。
2-3-4樹與紅黑樹是一一對應的關系
 
 
且上下關系中不允許2個連續的紅邊
 
由特點3可以推出LLRB的一個特性,紅黑樹與2-3-4樹一一對應。
 
2、插入算法
同樣地,在LLRB中查找操作同BST。
在插入之前要知道一個操作: 旋轉。它有兩種情況:左旋,右旋。
 
 
左旋 右旋
 
插入算法思路:即前面介紹的2-3-4樹
具體實現時,插入一個結點時,始終是紅結點,即用紅邊鏈接該結點。對於2-node、3-node直接插入(k-node有k個插入點),如違反上面的左紅鏈接和連續的紅鏈接,則旋轉作調整。對於4-node(左右都為紅鏈接),先分裂,物理實現是一個 翻轉(左右紅鏈接變黑,父鏈接變紅)。
2-node插入的兩種case
 
 
3-node插入的三種case
 
 
 
4-node分裂操作
 
 
由4-node的分裂可知黑高度不變,分裂操作即翻轉在圖片上對應為紅鏈接向上傳遞。
在介紹2-3-4樹時,4-node分裂操作有兩種情況,4-node的parent是2-node和3-node;再結合k-node有k個插入點,則總共有6種情況。
4-node的分裂case 1
 
 
 
4-node的分裂case 2
 
 
看了上面兩幅圖后,也許會讓人覺得紅黑樹太復雜了,這么多case,其實不然,在LLRB實現中只有兩種操作: 旋轉翻轉。旋轉的目的是保持平衡,翻轉的目的是分裂4-node。
看了下面的LLRB插入算法,你就會明白上面4-node的翻轉、旋轉其實是分開的兩個過程(翻轉自上而下,旋轉自下而上),只是為了統一這個完整的過程而畫在了一起,才會有那么多case。
 
LLRB的插入算法:
首先結合2-3-4樹的插入算法思路,先自上至下查找(遇到4-node則翻轉),然后在底部葉子結點插入,因為在自上至下的過程中,可能會產生不滿足LLRB的性質的情況,故插入結點后需要自下至上調整以恢復LLRB性質。
下圖是插入算法的核心代碼,第2是分裂即翻轉,第1是插入操作,第3、4是調整。
 
從插入算法可以看出,如果自下而上再分裂4-node,則會出現它的parent也可能是4-node,祖父結點也可能是4-node;我們可以一直向上分裂,這也正是上面提到的自下而上的思路(原作者:Bayer);而更簡單的方法是,在沿樹向下的過程中,遇到4-node就分裂,這也正是自上而下與自下而上的區別。
插入算法的核心代碼
 
上圖的核心代碼按照自上而下和自下而上的順序放入BST的插入(遞歸版本)操作中即得到下圖的完整的插入算法。
注:分裂(即翻轉)是自上而下,所以放在遞歸之前;調整(即旋轉)是自下而上,所以放在遞歸之后。
完整的插入代碼
 
 
如果將分裂操作放到遞歸之后,也就是先自上而下查找,插入結點,然后自下而上調整也可同樣完成插入操作而不破壞LLRB的性質。
2-3樹的插入操作
 
 
其實上述描述的就是2-3樹的插入操作,它與2-3-4樹的插入的區別在於:2-3樹先插入,再分裂(down)、調整(up);2-3-4樹先分裂(down),再插入、調整(up)。又因為插入總是在最后一層進行,故翻轉的位置決定了對應樹的實現。
這也是為什么2-3-4樹叫top-down,而2-3樹叫bottom-up。
 
3、刪除算法
LLRB的刪除類似於插入,只不過處理剛好相反。插入、刪除都有臨界點:插入4-node,刪除2-node,對臨界點的操作都會引起突變,因為它們會破壞LLRB的性質(黑高度)。
所以同插入一樣,先從上至下查找,如果查找在3-node或4-node結束,則直接刪除;
3-node和4-node的刪除
 
 
對於2-node的刪除同4-node的插入相反,2-node的刪除是先合並2個2-node為1個4-node,然后再安全地刪除對應的2-node中的key。
同樣地,因為parent不為2-node(遇到即合並),再結合兄弟結點的2、3、4-node,則刪除總共有6種情況(2-node的兄弟為2-node, 3-node,4-node,父親為3-node,4-node,總共2*3=6種情況)。同樣,實際的刪除實現也沒這么復雜。
2-node的刪除(其實合並和借都是借,2-node不能直接刪除,先合並或者借再刪。)
 
 
在介紹刪除任意一個結點時,先分析刪除樹中最小的結點。因為它是刪除任意結點的一部分,后面可以看出來。
首先,為了保證可以直接刪除最小的某個結點,需要假設當前結點h或者h.left是紅色鏈。
然后從上而下查找過程中,2個2-node要變為1個4-node,則需反向翻轉(紅色父鏈接變黑,黑色子鏈接變紅),
為了將紅鏈從上向左子樹傳遞(刪除紅結點,不改變黑高度),需保證h為紅,h.left和h.left.left為黑;
當h.left和h.left.left都為黑時,
如果h.right.left為紅,則要從右邊借兄弟(下圖case 2),如果h.right.left為黑,則不需要(下圖case1)。
注:在翻轉的同時,右子樹可能會產生連續的紅鏈,則需調整。
case 1
 
 
 
case 2
 
 
 
紅鏈向左移動                   紅鏈向左移動對應的example
 
 
deleteMin的實現
 
 
 
 
deleteMin的example
 
 
完成了deleteMin就完成了LLRB的刪除操作的一大半。現在是刪除LLRB的任意一個key,
自上而下查找過程中,左邊查找用moveRedLeft;右邊查找用moveRedRight;直到最后的底部葉子結點,直接刪除即可;同樣,自下而上調整。
 
怎樣將delete操作歸約到delteMin去呢?算法導論提供的一個技巧是:replace,deleteMin(即用后繼的key代替當前的key,再刪除右孩子的最小結點)。
刪除技巧
 
 
 
完整刪除代碼
 
 
 
參考:
《算法導論》
《algorithm in c》
 

PS:9.9憶山東兄弟,必登高望遠,一覽縱山小。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM