通用算法 - [樹結構] - 紅黑樹

<1> 紅黑樹

紅黑樹是一種二叉查找樹,但在每個結點上增加了一個存儲位表示結點的顏色,可以是RED或者BLACK。通過對任何一條從根到葉子的路徑上各個着色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。本章主要介紹了紅黑樹的性質、左右旋轉、插入和刪除。重點分析了在紅黑樹中插入和刪除元素的過程,分情況進行詳細討論。一棵高度爲h的二叉查找樹可以實現任何一種基本的動態集合操作,如SEARCH(查找)、PREDECESSOR(前驅)、SUCCESSOR(後繼)、MIMMUM(最小)、MAXMUM(最大)、INSERT(插入)、DELETE(刪除)等。當二叉查找樹的高度較低時,這些操作執行的比較快,但是當樹的高度較高時,這些操作的性能可能不比用鏈表好。**紅黑樹(red-black tree)是一種平衡的二叉查找樹,它能保證在最壞情況下,基本的動態集合操作運行時間爲O(lgn)。**本章內容有些複雜,看了兩天,才大概清楚其插入和刪除過程,日後需要經常回顧,爭取完全消化掉。紅黑樹的用途非常廣泛,例如STL中的map就是採用紅黑樹實現的,效率非常之高,有機會可以研究一下STL的源代碼。

1、紅黑樹的性質

紅黑樹中的每個結點包含五個域:color、key、left、right和parent。如果某結點沒有一個子結點或父結點,則該結點相應的指針parent域包含值爲NIL(NIL並是是空指針,此處有些迷惑,一會解釋)。把NIL視爲指向紅黑樹的外結點(葉子)的指針,而把帶關鍵字的結點視爲紅黑樹的內結點。紅黑樹結點結構如下所示:

 1 #define RED  0
 2 #define BLACK 1
 3 struct RedBlackTreeNode
 4 { 
 5     T key;
 6     struct RedBlackTreeNode * parent;
 7     struct RedBlackTreeNode * left;
 8     struct RedBlackTreeNode * right;
 9     int color;
10 };

紅黑樹的性質如下:

(1)每個結點或是紅色,或是黑色。
(2)根結點是黑色。
(3)每個葉子結點(NIL)是黑色。
(4)如果有一個結點是紅色,則它的兩個兒子都是黑色。
(5)對每個結點,從該結點到其孫子(?) 結點的所有路徑上包含相同數目的黑色結點。

如下是一棵紅黑樹:
在這裏插入圖片描述
從圖可以看出NIL不是空指針,而是一個葉子結點。實際操作的時候可以將NIL視爲哨兵,這樣便於對黑紅色進行操作。紅黑樹的操作主要是對內部結點操作,因爲內部結點存儲了關鍵字的值。書中爲了便於討論,忽略了葉子結點的,如是上圖紅黑樹變成如下圖所示:
在這裏插入圖片描述
書中給出了黑高度的概念:從某個結點x出發(不包含該結點)到達一個葉子結點的任意一條路徑上,黑色結點的個數稱爲該結點的黑高度。由紅黑樹的性質(5)可知,從該結點出發的所有下降路徑都有相同的黑色結點個數。紅黑樹的黑高度定義爲其根結點的黑高度。
  書中給出了一個引理來說明爲什麼紅黑樹是一種好的查找樹,並對引理進行了證明(採用歸納法進行證明的,需要很強的歸納推理知識,正是我的不足之處,看書的痛苦在於此)。
引理:一棵有n個內結點的紅黑樹的高度之多爲2lg(n+1)。
關於紅黑樹的一些疑問:
1.紅黑樹和平衡二叉樹(AVL樹)的區別與聯繫
2.紅黑樹爲啥增加插入的節點要置爲紅節點
3.紅黑樹在哪裏用的比較多,它相對其他的平衡搜索樹有何優點?
對應解答:
1、 紅黑樹並不追求“完全平衡”——它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。

紅黑樹能夠以O(log2 n) 的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構 能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。紅黑樹的算法時間複雜度和AVL相同,但統計性能比AVL樹更高。

平衡二叉樹嚴格的高度控制,左右子樹的高度差距不能大於1,這會導致插入刪除有較多的旋轉調整的步驟,並且其樹的高度一定是lgn,也就是說這種樹的平均時間複雜度就是O(lgn)
但是紅黑樹,只用保證任何節點到葉子節點均包含相同數目的黑色節點,通過顏色來約束樹的形狀,主要有以下幾個特徵:

1.紅黑樹的高度總是低於2lg(n+1),n爲節點的個數
2.紅黑樹的時間複雜度爲O(h(x))= O(2lg(n+1))=O(lgn)
3.任何插入刪除導致的不平衡都可以在三次旋轉操作內完成平衡,降低了實現的複雜度。

查找較多的可以選擇用AVL Tree、插入刪除較多的可以用RBTree。
2、我們可以看插入的點爲紅色和黑色時可能導致的情況:
假設新插入的節點是黑色,那麼不管原來的紅黑樹是什麼樣的,這樣一定會破壞平衡,因爲原來的樹是平衡的,現在在這一條路徑上多了一個黑色節點,必然違反了性質5(不記得的時候多看幾遍性質,並理解是最好的)。
假設是新插入的節點是紅色,那麼既有有可能會破壞平衡,也有可能不破壞平衡。
破壞平衡主要可能是違反了性質4(如果一個節點爲紅色,那麼它的兩個孩子節點必須爲黑色),比如下圖中,新插入的一個key=21的節點,並把該節點置爲紅色,由於它的父節點22爲紅色,導致紅色節點的孩子中出現了紅色節點。
不破壞平衡的情況也有可能出現,比如下圖中,如果我插入一個key=0的節點。把0這個節點置爲紅色,並不會影響原來樹的平衡,因爲0的父節點是黑色。

如下圖:
在這裏插入圖片描述

2、紅黑樹的應用

紅黑樹是一種近似平衡的二叉搜索樹,通過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,確保沒有一條路徑會比其他路徑長2倍,因而是近似平衡的。所以相對於嚴格要求平衡的AVL樹來說,它的旋轉保持平衡次數較少。用於搜索時,插入刪除次數多的情況下我們就用紅黑樹來取代AVL。
紅黑樹的應用比較廣泛:

· 廣泛用在C++的STL中。map和set都是用紅黑樹實現的。
· 著名的linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊。
· epoll在內核中的實現,用紅黑樹管理事件塊
· nginx中,用紅黑樹管理timer等
· Java的TreeMap實現

3、紅黑樹常見的面試題

1.stl中的set底層用的什麼數據結構?
2.紅黑樹的數據結構怎麼定義的?
3.紅黑樹有哪些性質?
4.紅黑樹的各種操作的時間複雜度是多少?
5.紅黑樹相比於BST和AVL樹有什麼優點?
6.紅黑樹相對於哈希表,在選擇使用的時候有什麼依據?
7.如何擴展紅黑樹來獲得比某個結點小的元素有多少個?
8.擴展數據結構有什麼步驟?
9 爲什麼一般hashtable的桶數會取一個素數?

詳細解答:

1.STL中的set底層用的什麼數據結構?
紅黑樹。

2.紅黑樹的數據結構怎麼定義?

 1. enum Color  
 2. {  
 3.           RED = 0,  
 4.           BLACK = 1  
 5. };  
 6.   
 7. struct RBTreeNode  
 8. {  
 9.            struct RBTreeNode*left, *right, *parent;  
 10.            int   key;  
 11.            int data;  
 12.            Color color;  
 13. };  

3.紅黑樹有哪些性質?
一般的,紅黑樹,滿足以下性質,即只有滿足以下全部性質的樹,我們才稱之爲紅黑樹:

1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。 5)對於任一結點而言,其到葉結點樹尾端NIL指針的每一條路徑都包含相同數目的黑結點。

4.紅黑樹的各種操作的時間複雜度是多少?
能保證在最壞情況下,基本的動態集合操作的時間均爲O(lgn)。

5.紅黑樹相比於BST和AVL樹有什麼優點?
紅黑樹是犧牲了嚴格的高度平衡的優越條件爲代價,它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。紅黑樹能夠以O(log2 n)的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。

相比於BST,因爲紅黑樹可以能確保樹的最長路徑不大於兩倍的最短路徑的長度,所以可以看出它的查找效果是有最低保證的。在最壞的情況下也可以保證O(logN)的,這是要好於二叉查找樹的。因爲二叉查找樹最壞情況可以讓查找達到O(N)。

紅黑樹的算法時間複雜度和AVL相同,但統計性能比AVL樹更高,AVL樹在插入和刪除中所做的後期維護操作肯定會比紅黑樹要耗時好多,但是他們的查找效率都是O(logN),所以紅黑樹應用還是高於AVL樹的。

實際上插入 AVL 樹和紅黑樹的速度取決於你所插入的數據.如果你的數據分佈較好,則比較宜於採用 AVL樹(例如隨機產生系列數),但是如果你想處理比較雜亂的情況,則紅黑樹是比較快的。

6.紅黑樹相對於哈希表,在選擇使用的時候有什麼依據?
權衡三個因素:== 查找速度, 數據量, 內存使用,可擴展性。==

總體來說,hash查找速度會比map快,而且查找速度基本和數據量大小無關,屬於常數級別;而map的查找速度是log(n)級別。並不一定常數就比log(n) 小,hash還有hash函數的耗時,明白了吧,如果你考慮效率,特別是在元素達到一定數量級時,考慮考慮hash。但若你對內存使用特別嚴格, 希望程序儘可能少消耗內存,那麼一定要小心,hash可能會讓你陷入尷尬,特別是當你的hash對象特別多時,你就更無法控制了,而且 hash的構造速度較慢。

紅黑樹並不適應所有應用樹的領域。如果數據基本上是靜態的,那麼讓他們待在他們能夠插入,並且不影響平衡的地方會具有更好的性能。如果數據完全是靜態的,例如,做一個哈希表,性能可能會更好一些。

在實際的系統中,例如,需要使用動態規則的防火牆系統,使用紅黑樹而不是散列表被實踐證明具有更好的伸縮性。Linux內核在管理vm_area_struct時就是採用了紅黑樹來維護內存塊的。
紅黑樹通過擴展節點域可以在不改變時間複雜度的情況下得到結點的秩。

7.如何擴展紅黑樹來獲得比某個結點小的元素有多少個?
這其實就是求節點元素的順序統計量,當然任意的順序統計量都可以需要在O(lgn)時間內確定。
在每個節點添加一個size域,表示以結點 x 爲根的子樹的結點樹的大小
則有size[x] = size[[left[x]] + size [right[x]] + 1;,這時候紅黑樹就變成了一棵順序統計樹。

利用size域可以做兩件事:

1). 找到樹中第i小的結點;

 1. OS-SELECT(x;,i)  
 2. r = size[left[x]] + 1;  
 3. if i == r  
 4.      return x  
 5. elseif i < r  
 6.      return OS-SELECT(left[x], i)  
 7. else return OS-SELECT(right[x],  i)  

思路:size[left[x]]表示在對x爲根的子樹進行中序遍歷時排在x之前的個數,遞歸調用的深度不會超過O(lgn);

2).確定某個結點之前有多少個結點,也就是我們要解決的問題;

 1. OS-RANK(T,x)  
 2. r = x.left.size + 1;  
 3. y = x;  
 4. while y != T.root  
 5.          if y == y.parent.right  
 6.                  r = r + y.parent.left.size +1  
 7.          y = y.p  
 8. return r  

思路:x的秩可以視爲在對樹的中序遍歷中,排在x之前的結點個數加上一。最壞情況下,OS-RANK運行時間與樹高成正比,所以爲O (lgn).

8.擴展數據結構有什麼步驟?

1).選擇基礎數據結構;
2).確定要在基礎數據結構種添加哪些信息;
3).驗證可用基礎數據結構上的基本修改操作來維護這些新添加的信息;
4).設計新的操作。

9 爲什麼一般hashtable的桶數會取一個素數
設有一個哈希函數
H( c ) = c % N;
當N取一個合數時,最簡單的例子是取2n2^n,比如說取23=82^3=8,這時候
H( 11100(二進制) ) = H( 28 ) = 4
H( 10100(二進制) ) = H( 20 )= 4

這時候c的二進制第4位(從右向左數)就”失效”了,也就是說,無論第c的4位取什麼值,都會導致H( c )的值一樣.這時候c的第四位就根本不參與H( c )的運算,這樣H( c )就無法完整地反映c的特性,增大了導致衝突的機率.

取其他合數時,都會不同程度的導致c的某些位”失效”,從而在一些常見應用中導致衝突.
但是取質數,基本可以保證c的每一位都參與H( c )的運算,從而在常見應用中減小衝突機率

4、紅黑樹的代碼實現

<2> 自平衡的二叉查找樹(AVL)

平衡二叉樹,一般是用平衡因子差值決定並通過旋轉來實現,左右子樹樹高差不超過1,那麼和紅黑樹比較它是嚴格的平衡二叉樹,平衡條件非常嚴格(樹高差只有1),只要插入或刪除不滿足上面的條件就要通過旋轉來保持平衡。由於旋轉是非常耗費時間的。我們可以推出AVL樹適合用於插入刪除次數比較少,但查找多的情況。

應用相對其他數據結構比較少。windows對進程地址空間的管理用到了AVL樹。
具體參考:平衡搜索樹-AVLTree

<3> B-與B+樹

1、B-樹和B+樹的定義:

B-樹是一種多路搜索樹(並不是二叉的),其定義如下:

1.定義任意非葉子結點最多隻有M個兒子,且M>2;
2.根結點的兒子數爲[2, M];
3.除根結點以外的非葉子結點的兒子數爲[M/2, M];
4.每個結點存放至少M/2-1(取上整)和至多M-1個關鍵字;(至少2個關鍵字)
5.非葉子結點的關鍵字個數=指向兒子的指針個數-1;
6.非葉子結點的關鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的 子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;
8.所有葉子結點位於同一層;
在這裏插入圖片描述
B+樹是B-樹的變體,也是一種多路搜索樹:

1.其定義基本與B-樹同,除了:
2.非葉子結點的子樹指針與關鍵字個數相同;
3.非葉子結點的子樹指針P[i],指向關鍵字值屬於[K[i], K[i+1])的子樹(B-樹是開區間);
5.爲所有葉子結點增加一個鏈指針;
6.所有關鍵字都在葉子結點出現;
在這裏插入圖片描述

2、B+樹的應用

B樹,B+樹:它們特點是一樣的,是多路查找樹,一般用於數據庫中做索引,因爲它們分支多層數少,因爲磁盤IO是非常耗時的,而像大量數據存儲在磁盤中所以我們要有效的減少磁盤IO次數避免磁盤頻繁的查找。
B+樹是B樹的變種樹,有n棵子樹的節點中含有n個關鍵字,每個關鍵字不保存數據,只用來索引,數據都保存在葉子節點。是爲文件系統而生的。

B+樹相對B樹磁盤讀寫代價更低:因爲B+樹非葉子結點只存儲鍵值,單個節點佔空間小,索引塊能夠存儲更多的節點,從磁盤讀索引時所需的索引塊更少,所以索引查找時I/O次數較B-Tree索引少,效率更高。而且B+Tree在葉子節點存放的記錄以鏈表的形式鏈接,範圍查找或遍歷效率更高。Mysql InnoDB用的就是B+Tree索引。

具體參考:
1、B+樹圖文詳解
2、 B樹、B-樹、B+樹與紅黑樹

<4> 單詞查找樹(Trie)

又名單詞查找樹,一種樹形結構,常用來操作字符串。它是不同字符串的相同前綴只保存一份。

相對直接保存字符串肯定是節省空間的,但是它保存大量字符串時會很耗費內存(是內存)。
類似的有:前綴樹(prefix tree),後綴樹(suffix tree),radix tree(patricia tree, compactprefix tree),crit-bit tree(解決耗費內存問題),以及前面說的double array trie。

前綴樹:字符串快速檢索,字符串排序,最長公共前綴,自動匹配前綴顯示後綴。
後綴樹:查找字符串s1在s2中,字符串s1在s2中出現的次數,字符串s1,s2最長公共部分,最長迴文串。

trie 樹的一個典型應用是前綴匹配,比如下面這個很常見的場景,在我們輸入時,搜索引擎會給予提示。還有比如IP選路,也是前綴匹配,一定程度會用到trie:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章