文章目錄
紅黑樹
時間複雜度
是一種確保擁有對數級高度的二叉搜索樹。
能保證在最壞的情況,所有動態操作的時間複雜度內爲O(logn)。
問題一:STL中的set底層使用了什麼數據結構?
答:紅黑樹
STL中set、map、multiset 、multimap底層都是紅黑樹。
java集合框架底層中 TreeMap 和 TreeSet的數據結構是紅黑樹。
問題二:紅黑樹有哪些性質?
同時滿足以下四個性質:
1)每個節點或是紅的,或者是黑的。
2)根節點和每個葉子節點(NIL)是黑色的
3)如果一個節點是紅色的,那麼他的兩個兒子都是黑的。
4)對於每個節點,從該節點到子孫節點的所有路徑上包含相同數目的黑色節點。
如圖爲一種紅黑樹
如果一棵樹所有的節點顏色都是黑色,那麼很容易滿足紅黑樹性質,但是難點在於,在動態的刪除和插入時,如何還能保證二叉搜索樹有紅黑樹的性質。
關於紅黑樹的高度與證明
可以保證紅黑樹的高度 h <= 2log2(n+1)
證明:
以上圖爲例,因爲根據紅黑樹紅色節點的雙親一定是黑色節點,可以將紅黑樹的紅色節點併入到雙親節點
然後會成爲下圖:
這是一顆2-3-4樹。
每個節點的葉子節點可以是2個3個和四個。
很明顯,構成的這棵2-3-4樹,還有一個性質,就是所有的葉子節點都有相同的黑高度。
假設之前的紅黑樹的高度是h ,現在這棵2-3-4樹的高度是h1
根據紅黑樹的第三個性質,紅色節點不可能連續,從根節點到任意一個葉子節點的路徑,紅色節點的數量肯定不會過半。
得到 h < = 2h1
因爲紅黑樹除了葉子節點,所有的節點度都爲2,所以n個非葉子節點,就有n+1個葉子節點。
可以歸納的證明,當紅黑樹只有1個根節點時,有2個葉子節點,
在這之後,每插入一個節點,首先會-1個葉子節點,然後會+2個葉子節點
-1+2 = +1
所以n個非葉節點,就有n+1個葉節點。
對於這棵2-3-4樹,因爲樹的高度是h1,想知道葉節點數leaves的範圍,那麼下界的情況應該是每個節點的子節點數都是2,上界的情況應該是每個節點的子節點數都是4
那麼 2h1 <= leaves <= 4h1
已經知道,leaves = n+1
所以2h1 <= n+1
兩邊取對數,得到
h1 <= log2(n+1)
因爲h <= 2h1
所以 h <= 2log2(n+1)
問題三: 紅黑樹的數據結構怎麼定義?
enum Color{
RED = 0;
BLACK =1;
};
struct RBTreeNode{
struct RBTreeNode *left,*right,*parent;
int key;
int data;
Color color;
};
關於紅黑樹的更新操作:插入和刪除
在插入和刪除節點時,爲了維護紅黑樹性質,一般需要:
1)重新着色,爲周邊的節點重新着色
2)對樹進行重新排列,即旋轉
左旋和右旋
對同一個節點,左旋和右旋是可逆的
這種操作保持了二叉搜索樹的性質。
左旋或者右旋後,其中序遍歷序列都爲 :{ X 、 A、 Y、 B、 Z}
因爲只需要修改一些節點的指針,所以這種操作是時間複雜度是常數級的
重新着色和旋轉過程
1)首先根據平衡搜索樹的性質,判斷插入節點的key值應該插入的位置
2) 對這個節點進行染色,染爲紅色
爲什麼是紅色,因爲BST樹根節點是一開始初始化好的,那麼要插入的節點肯定不會 一定是黑色,因爲那樣會違反性質2。關鍵是性質4,一定會維持,如果插入的是紅節點,那麼所有節點的黑高度肯定不會發生變化。
但是如果插入節點的是紅色,有可能會違反性質3。
可以修補破壞的性質3 , 把破壞帶來的結果一步步上移,從節點X開始,往根節點上移。
先考慮重新着色,上色不了再考慮旋轉;
旋轉之後,可能還要重新着色
以上圖代碼爲例
在這棵樹插入15,
這個時候11 和 15 都是紅色,產生衝突,需要重新着色
幸運的是,因爲15的祖父節點的顏色是黑色,並且15祖父節點的兩個孩子節點的顏色都是紅色,那麼剛好可以將10重新着色成紅色,8和11重新着色爲黑色。
這個時候10和18都是紅色會產生衝突,但是10的祖父節點的兩個孩子一個是紅色一個是黑色,不能像剛纔那樣重新着色(先不論根節點不能爲紅色),所以需要旋轉,進行右旋。
這個時候,7, 10 , 18 三個節點會在一條線,而不是人字形折了一下,正是我們想要的。
這個時候,再將根節點左旋,會得到
這個時候,10的左子樹的黑高度會+1,右子樹的黑高度會-1,將7重新染色成紅色,10染色成黑色
此時紅黑樹滿足四條紅黑性質。
紅黑樹僞代碼
情況A和情況B
RB-INSERT(T X)
TREE-INSERT(T X); //首先平衡樹操作,找到節點要插入的位置
color[x] = RED; //要插入的節點爲紅色
//從插入的節點一直開始上移到根節點或者當前節點染成黑色,用循環表示
while(X != root[X] || color[x]!=BLACK)
do if(p[x] = left[p[p[x]]]) //稱之爲A類情況
y = right[p[p[x]]] ;
if(color[y] = RED)
Then <Case 1>
else
if(x = right[p[x]]) //x是父節點的右孩子,p[x]是x祖父節點的左孩子,即“人”形狀
Then <Case 2>
Then <Case 3>
else if(p[x] = right[p[p[x]]]) { //稱之爲B類情況,與A類鏡像
//同A情況,鏡像處理
}
//將根節點染成黑色,如果根節點被染成了紅色,則需要改爲黑色,因爲維護性質1,但是並不會對
//左右子樹造成影響違反其他性質。
color[root[T]] = BLACK
對於情況A:
Case1:
如果x與p[x]衝突都爲紅色,而且y = right[p[p[x]]] 也爲紅色,那麼可以將p[x]和y重新着色成黑色,將p[p[x]]重新着色爲紅色
Case2:
此時,如果x是p[x]的右孩子,p[x]是p[p[x]]的左孩子。
如果x與p[x]衝突都爲紅色,但是y = right[p[p[x]]] 爲黑色,那麼可以p[x]右旋。
此時x是p[x]的左孩子,p[x]是p[p[x]]的左孩子
Case3:
此時x是p[x]的左孩子,p[x]是p[p[x]]的左孩子,將p[x]左旋,並將p[p[x]]重新着色爲紅色。
紅黑樹相比於BST和AVL樹有什麼優點
紅黑樹是犧牲了嚴格的高度平衡的優越條件爲代價,它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。紅黑樹能夠以O(log2 n)的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。相比於BST,因爲紅黑樹可以能確保樹的最長路徑不大於兩倍的最短路徑的長度,所以可以看出它的查找效果是有最低保證的。在最壞的情況下也可以保證O(logN)的,這是要好於二叉查找樹的。因爲二叉查找樹最壞情況可以讓查找達到O(N)。紅黑樹的算法時間複雜度和
AVL 相同,但統計性能比 AVL 樹更高,所以在插入和刪除中所做的後期維護操作肯定會比紅黑樹要耗時好多,但是他們的查找效率都是O(logN),所以紅黑樹應用還是高於 AVL 樹的. 實際上插入 AVL 樹和紅黑樹的速度取決於你所插入的數據.如果你的數據分佈較好,則比較宜於採用 AVL 樹(例如隨機產生系列數),但是如果你想處理比較雜亂的情況,則紅黑樹是比較快的
紅黑樹相比於哈希表,在使用時有什麼依據?
證的。在最壞的情況下也可以保證O(logN)的,這是要好於二叉查找樹的。因爲二叉查找樹最壞情況可以讓查找達到O(N)。紅黑樹的算法時間複雜度和
AVL 相同,但統計性能比 AVL 樹更高,所以在插入和刪除中所做的後期維護操作肯定會比紅黑樹要耗時好多,但是他們的查找效率都是O(logN),所以紅黑樹應用還是高於 AVL 樹的. 實際上插入 AVL 樹和紅黑樹的速度取決於你所插入的數據.如果你的數據分佈較好,則比較宜於採用 AVL 樹(例如隨機產生系列數),但是如果你想處理比較雜亂的情況,則紅黑樹是比較快的
紅黑樹相比於哈希表,在使用時有什麼依據?
權衡三個因素: 查找速度, 數據量, 內存使用,可擴展性。總體來說,hash 查找速度會比 map 快,而且查找速度基本和數據量大小無關,屬於常數級別;而 map 的查找速度是 log(n)級別。並不一定常數就比 log(n)小,hash 還有 hash 函數的耗時,明白了吧,如果你考慮效率,特別是在元素達到一定數量級時,考慮考慮hash。但若你對內存使用特別嚴格, 希望程序儘可能少消耗內存,那麼一定要小心,hash 可能會讓你陷入尷尬,特別是當你的 hash對象特別多時,你就更無法控制了,而且hash 的構造速度較慢。紅黑樹並不適應所有應用樹的領域。如果數據基本上是靜態的,那麼讓他們待在他們能夠插入,並且不影響平衡的地方會具有更好的性能。如果數據完全是靜態的,例如,做一個哈希表,性能可能會更好一些。