STL源碼剖析 - RB-tree


在我看來,看源碼是一件既痛苦又興奮的事。當我們在推敲其中的難點時,是及其痛苦的,但當發現實現代碼是那么絲滑簡潔時,“wc, nb!”。


1. 導語

如果我們去看關聯式容器map、set、multimap、multiset源代碼,我們發現絕大部分操作如插入、修改、刪除、搜索,均是由其內含的紅黑樹來完成的,我們有必要去揭開她的神秘面紗,一覽她的絕世風姿。

(如果你手頭還沒有《STL源碼剖析》時,強烈建議你現在就去買一本or文末的百度雲鏈接or網路上的其他資源)

關鍵詞:RB-tree、BST、AVL tree 、STL Sources


 從哪里講起呢?

二叉搜索樹,每個節點最多有兩個子節點,而每個節點鍵值一定大於左子樹鍵值節點鍵值,而小於右子樹節點鍵值。這樣一來,就可以提供對數時間的插入和搜索。

當然,較為復雜的是它的刪除操作。

1.如果刪除的是葉子,那么直接刪除delete該指針即可;

2. 如果刪除的不是葉子,而他只有一個節點,那么就將其子節點連至它的父節點;

3. 如果刪除的不是葉子,而他有兩個節點,那么就用它的右子樹的最小節點代替他(值賦給它),刪除該右子樹最小節點。

(這是為什么?因為刪除之后還要保證二叉樹的搜索,所以替代他的元素就需要是比他大的下一個元素,而比他大的元素都在右子樹,且最小的哪一個就是最左邊的那一個。其實在二叉搜索樹中,要找最大就一直向右子樹找,要找最小就一直往左子樹找)

 

 

  上述例子是在二叉樹平衡的情況下進行的。平衡即左右子樹高度相近,完全平衡則要求左右子樹高度(深度/層數)完全相等。要是完全平衡的條件下,我們的搜索和插入操作就會是對數時間,這無疑是相當快的(當然比起Hashtable的大致O(1)來說是較慢的),這在關聯式容器中是我們所追求的。因為它們的底層均不是線性結構,能達到常數時間的查找/搜索。

  但是要知道,維護一個二叉樹的完全平衡是非常耗時的,比如我插入之后,很大概率就會使得二叉樹不完全平衡,就需要復雜度旋轉移位操作,這對於插入來說非常不划算,也就是說,我們沒必要為了平衡而平衡,只要達到大致平衡,就可以得到統計上的對數查找插入時間。

那么進入我們的正題,平衡二叉搜索樹。


 2. 平衡二叉搜索樹

這里的平衡,是指沒有任何節點的深度過大,而非絕對的平衡。

代表結構:RB-tree、AVL-tree、AA-tree

本片主要介紹紅黑樹,AVL-tree將在其他篇章中講解。


3. RB-tree

紅黑樹,一種自平衡的二叉查找樹。它的最壞情況運行時間也是良好的,並且在實踐中是高效的,它可以在O(log n)時間內做查找,插入和刪除。(來自百度百科)

無疑,它的實現是非常復雜的,搜索幾乎是是它最為簡單的操作,復雜度O(log n),最壞也是如此。而插入和刪除就比較困難了,查找到插入節點/刪除節點復雜度O(log n),在插入時、插入后、刪除后都需要滿足它的紅黑規則限定👇,進行變色和旋轉

  1. 各個節點非黑即紅
  2. 根節點黑
  3. 紅色節點的子節點必須為黑
  4. 任一節點到null的路徑所含黑色節點相等

隱含意思是,不能出現連續的父子紅色節點,且新增節點必須為紅色(如果是黑色,經過他那一條路徑上的黑色節點不就多了一個嗎?不符合規則4),其父節點必須為黑色。

對於AVL-tree的“左左、右右、左右、右左”的情況,我們需要在插入后,進行單旋轉和雙旋轉來使得它的每個節點的左右子樹深度相差不超過1,紅黑樹難度更甚。她不僅要進行單旋轉和雙旋轉,旋轉之后還要進行變色。

我無意把問題講的極其復雜,數圖以蔽之。

插入節點的旋轉與變色

1. 樹空,插入節點只要變為黑色即可;

2. 插入節點的父節點是黑色,不違反規則,不做旋轉變色調整。

例:插入節點->70/85。

 

 

 

 

 

 

 

 

 

 

 

 

3. 插入節點在外側,父節點是紅色,“叔叔”是紅色,違反規則,需要右旋和變色(插入節點)。

 

4. 插入節點在內側,父節點是紅色,“叔叔”是紅色,違反規則,需要雙旋轉(先左旋。再右旋)和變色

 

5. 插入節點在外側,父節點是紅色,“叔叔”是黑色,違反規則,需要右旋和變色(父節點和父父節點)。

(這里后來想想,應該是先左旋再右旋,最終使得99黑色成為根,90,100紅分別為左右兒子,80黑為90紅左兒子, 120黑為100紅右兒子)

6. 插入節點在內側,父節點是紅色,“叔叔”是黑色,違反規則,需要右旋和變色(父節點和父父節點)

至此,紅黑樹的插入操作的旋轉和變色,我們就討論的差不多了。

下面,我們來看源碼。


4. RB-tree 部分源碼

紅黑樹節點有紅黑二色,節點要訪問父節點和子節點,所以不難想到節點的數據結構

 

所以節點模型如下(一種顏色三個指針,和vector三個指針一樣簡潔)


元素操作(主要介紹插入和搜索,后續的旋轉和變色比較晦澀,上圖已大致說明,自行參考源碼消化)

搜索( O(logn) )

從root節點開始向下搜索,若節點值大於搜索值,走左子樹,否則走右子樹。用一個指針保存最后一個不小於給定值的節點指針,最后用它來判斷是否查找到。最后的邏輯看的很迷,要么找到直接跳出返回就好,要么在最后判斷保存的節點值與給定值相等與否,返回結果即可。要我實現,我不會這樣做(大言不慚😄)。

插入(insert_unique, insert_equal)( O(logn) )

rb-tree提供兩種插入操作,一種執行不重復插入,供map,set使用,還有一種允許重復插入,供multimap,multiset使用。

 

(8line, y = x)

(line3, 第二元素),這個插入函數與上面那個有較大不同,要排除重復插入的情況。先遍歷找到插入點,即子節點為空,得到它的父節點y,如果父節點是最左節點,可以插入。如果不是最左節點,那么無法插入(因為要往父節點的左子節點插入,但是該點有節點),那么--迭代器,使得插入父節點下移,以期能夠插入(到達最左節點,可以插入)。最后,還不能滿足的話,則是有重復元素值,不插入。

更為底層的插入操作


 看源碼函數,最好自己畫些例子,能更快的理解。其中我也有一些無法理解的地方,很迷,其中也不乏一些錯誤之處,歡迎指正。如果你有好的見解,歡迎留言討論。


免責聲明!

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



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