前面介紹treeMap時候提到了treeMap是基於紅黑樹(Red-Black tree)的 NavigableMap實現的。今天就粗略瞭解了一下紅黑樹。
說紅 黑樹之前,不得不把我們的數組、鏈表、還有上文的二叉搜索樹“批判“一番。數組的優點在於查找快,而插入刪除麻煩。鏈表正好相反,插入刪除方便,但是查找麻煩。這樣可就呵呵噠了,聰明的前輩們就想着折中,查找、插入和刪除都不能慢。於是乎有了樹這種結構,樹的結構也有很多種,其中的二叉搜索樹就很nice,刪除和插入很快自不必說,查找也很快,時間複雜度O(logn)。這樣一看啦,二叉搜索樹就夠用了,爲啥還要整個紅黑樹呢?問題就在於,如果構建二叉樹的時候,由一組隨意的數據構建還好,要是由一組基本有序的數構建出來的基本是個鏈表,查找的時間複雜度又退化到O(n)。爲了能保證在O(logn)的時間複雜度內查找,需要保證二叉樹儘可能是平衡的,即每個結點的左右子樹的結點個數儘可能的相等。一旦因爲插入和刪除操作破環了平衡就要進行調整,保證平衡。
可以知道,紅黑樹是在二叉搜索樹進行了改進,至於如何才能思考出使用怎樣的結構才能達到平衡的目的,這個博主也沒思考過,我們就站在巨人的肩膀直接來學習紅黑樹的性質和用法。
性質
紅黑樹在二叉搜索樹的基礎上增加了一個存儲位來表示結點的顏色,可以是紅色或者黑色,通過對任意一條從根結點出發到葉子結點的路徑上各個結點的顏色進行約束,確保最大路徑也不會比最長路徑大過兩倍。紅黑樹滿足如下五條性質:
- 根結點是黑色的
- 紅色結點的子結點都是黑色的
- 每個結點不是黑色的就是紅色的
- 葉子(NIL)結點都是黑色的
- 任意結點出發的到所有後代葉結點的路徑上包含的黑色結點數相同
對於第四條規則,可以暫時忽略尾部增加的NIL葉子結點,關注內部結點即可。
修正
上面說紅黑樹的魔力在於對二叉樹的修正,而它修正的方式包括:圖色和旋轉。
對於圖色比較好理解,如果新插入的結點或者剛刪除一個結點,導致現在的二叉樹不滿足以上原則,則需要通過重新圖色使得符合紅黑樹性質。具體的用法在後面插入刪除操作中介紹。
旋轉包括兩種:左旋和右旋。左旋和右旋也很好理解,分別給大家看一個直觀的動態圖。
看完效果圖,就是上代碼了
對於左旋,先處理s和e結點,再處理s的左子樹和e的右子樹。
void leftRotate(Node root,Node e){
if(root!=null&&e!=null){
Node s=e.rChild;
if(e.p==null){//如果e是根結點
root=s;//s爲根結點
}else if(e.p.lChild==e){
s.p=e.p;
e.p.lChild=s;//e原來的父結點左子樹指向s
}else{
s.p=e.p;
e.p.rChild=s;//e原來的父結點的右子樹指向s
}
e.p=s;//e的父結點指向s
s.lChild=e;//s的左子樹指向e
e.rChild=s.lChild;//e的右子樹指向s的左子樹
if(s.lChild!=null){//如果s的左結點不爲空,則e指向s的左子樹結點
s.lChild.p=e;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
註釋已經很詳細了,可以看出,右旋和左旋很像,只是改動的子樹不同,看代碼:
void rightRotate(Node root,Node s){
if(root==null||s==null){
return;
}
Node e=s.lChild;
if(s.p==null){//s是根結點
root=e;
}else{//調整e到原來s的位置
e.p=s.p;
if(s.p.lChild==s){//修改e和父結點的指針
s.p.lChild=e;
}else{
s.p.rChild=e;
}
}
s.lChild=e.rChild;//修改e的右子樹到s的左子樹
if(e.rChild!=null){
e.rChild.p=s.lChild;
}
e.rChild=s;//修改s到e的右子樹
s.p=e;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
差別不大,應該都能看懂。
插入
紅黑樹是基於二叉搜索樹的,所以插入操作是在二叉搜索樹的插入的基礎上增加了修正操作。
二叉搜索樹的插入,就是找到對應的結點位置,然後插入,比較簡單,不明白的同學可以參看博主的文章二叉搜索樹。
插入結點一般有兩種顏色選擇:紅色或者黑色,一般選擇插入紅色結點,爲什麼呢?因爲插入黑色結點的話,一定會違背上文提到的紅黑樹的性質5,但是插入紅色的話不會違背性質5,但是有可能違背性質2(概率1/2,父結點紅黑都有可能),而且違背性質2比違背性質5更容易調整,所以插入結點的顏色都設爲紅色。
插入可分爲如下五種情形:
- 原樹爲空,只需重新上色爲黑色即可
- 父結點爲黑色,不違背規則,直接插入即可
- 父結點和叔叔結點均爲紅色
- 父結點紅色,叔叔結點黑色,插入父結點的右子樹
- 父結點紅色,叔叔結點黑色,插入父結點的左子樹
前兩種情況比較簡單,重點看後面三種情況:
情況3,考慮這樣一種情況,如圖:
圖(a)中,當前結點爲4,父結點5和叔叔結點8都是紅色,對應的調整到圖(b)。調整操作爲將父結點5和叔叔結點8塗黑,爺結點塗紅,將當前結點指向爺結點,狀態由情況3轉到了情況4,我們來看情況4。
先將圖(b)中的當前結點7的父結點2作爲新的當前結點,對2做左旋操作,得到圖(c)
由於當前結點是2,所以左旋後得到的紅黑樹結構對應於情況5,父結點7紅色和叔結點14黑色,2位於結點7點左子樹。這個情況下我們該如何操作呢?父結點7塗黑,爺結點11塗紅,再以爺結點11做右旋操作,塗黑根結點,如圖(d)。這樣插入結點4的調整操作就完成了,紅黑樹從圖(a)調整到圖(d)。當然,不是每次插入操作都需要這樣調整,調整前的狀態可能位於情況3,也可能是情況4,甚至直接是情況5。
我們剛剛以當前結點的父結點位於爺結點右子樹的情況舉例,如果位於爺結點的左子樹呢?左右子樹的選取以及旋轉略有不同,我們直接看代碼:
void rbInsert(Node root,Node z){
Node uncle;
while(z.p!=null&&z.p.color==RED){
if(z.p==z.p.p.lChild){//z的父結點位於爺結點的左子樹
uncle=z.p.p.rChild;
if(uncle.color==RED){//第三種情況
uncle.color=BLACK;//叔結點變黑
z.p.color=BLACK;//父結點變黑
z.p.p=RED;//爺結點變紅
z=z.p.p;
}else if(uncle.color==BLACK&&z.p.rChild==z){//第四種情況
z=z.p;//當前結點指向父結點
leftRotate(root,z);//左旋
}
z.p.color=BLACK;
z.p.p.color=RED;
rightRotate(root,z.p.p);
}else{//位於爺結點右子樹
uncle=z.p.p.lChild;
if(uncle.color==RED){//第三種情況
uncle.color=BLACK;//叔結點變黑
z.p.color=BLACK;//父結點變黑
z.p.p=RED;//爺結點變紅
z=z.p.p;
}else if(uncle.color==BLACK&&z.p.rChild==z){//第四種情況
z=z.p;//當前結點指向父結點
leftRotate(root,z);//左旋
}
/*第五種情況*/
z.p.color=BLACK;
z.p.p.color=RED;
rightRotate(root,z.p.p);
}
}
root.color==BLACK;//根結點設置爲黑色,對應於情況1
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
插入操作就說完了,主要考慮的就是情況3、4、5三種,他們的區別在於插入結點的父結點和叔結點的顏色不同,以及當前結點位於父結點的子樹位置不同。理清楚這三種情況即可完成插入操作。
刪除
刪除可以算是紅黑樹中最複雜的操作了,不是它時間複雜度高,而是操作起來略顯麻煩。
與添加一樣,紅黑樹的插入操作是在二叉搜索樹插入操作的基礎上進行了調整。二叉搜索樹的刪除操作分三種情況考慮,詳情請參考二叉搜索樹。
- 葉子結點,直接刪除
- 只有一個子結點,葉子結點頂替被刪除的結點位置
- 兩個子結點,找到要刪除結點的中繼結點,如果中繼結點爲右結點,移動右子樹;如果不是,則進行一系列操作,balabala。
刪除某個節點後,被刪除位置補位結點會塗成原有的顏色,所以不會出現破壞紅黑樹平衡的事情。但是補位的結點的子結點和補位結點的父結點構成的新樹,可能會破壞紅黑樹的結構。
爲了簡述,我們稱補位結點的子結點爲當前結點,有四種情況需要調整紅黑樹結構:
1. 當前結點黑色,兄弟結點紅色
2. 當前結點黑色,兄弟結點黑色,且兄弟結點的子結點黑色
3. 當前結點黑色,兄弟結點黑色,兄弟結點的左結點紅色,右結點黑色
4. 當前結點黑色,兄弟結點黑色,兄弟結點的右結點紅色,左結點任意色(紅色或黑色)
情況1如圖(B表示補位結點的父結點,A是補位結點的右子樹,D是補位結點父結點的右子樹,下同):
對應操作爲:父結點塗紅,兄弟結點塗黑,對父結點左旋。
情況2如圖:
對應操作爲:兄弟結點塗紅,當前結點指向父結點,父結點指向當前結點的祖父結點。
情況3如圖:
對應操作爲:兄弟結點塗紅,兄弟結點的左結點塗黑,對兄弟結點進行右旋。
情況4如圖:
對應操作爲:兄弟結點塗紅,父結點塗黑,兄弟結點的右結點塗黑,對父結點左旋。
可以看出,如果是從情況1開始發生的,後續可能是情況2,3,4中的一種:如果是情況2開始,就不可能再出現3和4;如果是情況3開始,必然會導致情況4的出現;如果2和3都不是,那必然是4。
除了這四種情況外,其他還有兩種簡單的情況:當前結點是黑色的根節點,那麼不用任何操作,因爲並沒有破壞樹的平衡性(子樹中黑色結點的個數沒變,紅色結點的子結點依然都是黑色的——刪除結點的父結點如果是紅色,那麼新的子結點仍是黑色)。如果當前節點是紅色的,說明剛剛移走的節點是黑色的,那麼不管替換節點的父節點是啥顏色,我們只要將當前節點塗黑就可以了,規則5可以保證,規則4也可以保證。
刪除的代碼,本文暫時不貼了,這兒比較複雜,過幾天博主補上,有興趣的同學,可以通過【數據結構和算法05】 紅-黑樹(看完包懂~)和 教你初步瞭解紅黑樹兩篇文章先了解。
總結
to be continued…
很慚愧,做了一點微小的貢獻!