二叉樹之一BST樹,AVL樹詳解及B樹和紅黑樹原理分析
BST樹,AVL樹詳解及B樹和紅黑樹原理分析
網際網路面試中樹尤其是BST,AVL是提問的重點也是難點,甚至B樹乃至高階資料結構紅黑樹都是提問的重點,像阿里雲面試就曾經問過map實現機制(紅黑樹)及其原理,這裡我們要做到對BST/AVL完全熟悉能給出全部程式碼實現,紅黑樹、b樹之類,有能力的需要完全理解,程式碼就不用掌握了。紅黑樹和b樹看會就行了,當碰到你感覺他們不懂這方面的面試官的時候,可以逮著他們狂扯這部分,然後讓他們感覺你很高大上。
以二叉樹或者樹作為表的組織形式,稱為樹表。樹表在進行增刪操作時可以方便維護表有序,不需要移動記錄,比順序儲存的表效率要高,開銷要低。常見樹表BST/AVL,B樹等。
一 二查搜尋樹BST
二叉排序樹BST其定義:
1 首先它也是一個二叉樹,故滿足遞迴定義;
2 其次每個節點只存在一個值;
3 需滿足左子樹值<=根值<=右子樹,故按照中序遍歷會得到一個非遞減序列。
BST涉及操作主要增,刪,查,除了刪麻煩一點,其他操作均可遞迴實現,二叉查詢樹的一般性質:1.在一棵二叉查詢樹上,執行查詢、插入、刪除等操作,的時間複雜度為O(lgn)。因為,一棵由n個結點,隨機構造的二叉查詢樹的高度為lgn,所以順理成章,一般操作的執行時間為O(lgn)。2.但若是一棵具有n個結點的線性鏈,則此些操作最壞情況執行時間為O(n)。
程式碼:
struct node{ int key; node*left; node*right; }; int bstinsert(node*&p,int k){ if(p==NULL){p=(node*)malloc(sizeof(node));p->key=k;p->left=p->right=NULL;return 1;} else {if(k==p->key)return 0; else if(k<p->key)return bstinsert(p->left,k); else return bstinsert(p->right,k); } } void bstcreate(node*&r,int a[],int n) { int i; r==NULL; for(i=0;i<n;i++) bstinsert(r,a[i]); } node* bstsearch(node*root,int k){ if(root==NULL)return root; else if(k==root->key)return root; else if(k<root->key)return bstsearch(root->left,k); else return bstsearch(root->right,k); } int bstdelete(node*&root,int k){ node *p=root,*pa,*r; pa=NULL;//父節點 while(p&&p->key!=k){ pa=p; if(p->key>k)p=p->left; else p=p->right; } if(p==NULL)//沒有找到k {cout<<"Not Found!"<<endl;return 0;} //分四種情況討論 if(p->left==NULL&p->right==NULL){ if(pa==NULL)root=NULL; else if(pa->left==p)pa->left=NULL; else pa->right=NULL; delete p;}//葉子直接刪除 else if(p->left==NULL){ if(pa==NULL)root=p->right;//p是根節點 else if(pa->left==p){pa->left=p->right;delete p;}//p是父節點左孩子,則用其右孩子取代 else {pa->right=p->right;delete p;}//p是父節點右孩子,則用其右孩子取代 }//只含右節點 else if(p->right==NULL){ if(pa==NULL)root=p->left;//p是根節點 else if(pa->left==p){pa->left=p->left;delete p;}//p是父節點左孩子,則用其右孩子取代 else {pa->right=p->left;delete p;}//p是父節點右孩子,則用其右孩子取代 }//只含左節點 else { //需要找左子樹最左下或者右子樹最左下孩子r取代p故先找到r node*p1=p;r=p->left; while(r->right){p1=r;r=r->right;} //找到r節點,且該節點一定無右子樹,符合上面的無右子樹情況 if(p1->left==r){p1->left=r->left;} else if(p1->right==r){p1->right=r->left;} //刪除r後再把r替代p r->left=p->left; r->right=p->right; if(pa==NULL)root=r; else if(pa->left==p)pa->left=r; else pa->right=r; delete p; }//既有左孩子又有右孩子 p=NULL; return 1; } void inorder(node*r){ if(r){ inorder(r->left); cout<<r->key<<" "; inorder(r->right); } } void bstdis(node *r){ inorder(r); cout<<endl; }
特點:就時間複雜度來說,BST的查詢與二分查詢效能差不多,但是增刪時BST無需移動記錄,只需移動指標比順序儲存好很多;一般效能都在O(log n)。
缺點:面對序列原本就是順序:
如右邊順序序列所示,此時效能和順序儲存無異。所以,使用BST樹還要考慮儘可能讓BST樹保持左圖的結構,和避免右圖的結構,也就是所謂的“平衡”問題j 進而引出AVL。
二 平衡二叉樹AVL
平衡二叉樹AVL是帶有平衡條件的BST,這個平衡條件必須容易保持,且需要保持樹的深度O(log N),一般想法是要求每個節點都必須要有相同高度,但是這樣要求太嚴格,難以實現,故退一步引出AVL:
1 首先仍是一棵二叉樹,滿足遞迴定義;
2 其次又是一棵BST,滿足有序;
3 每個節點左右子樹高度差的絕對值不能超過1
AVL樹各節點的平衡因子定義為左子樹深度減去右子樹深度,AVL的平衡因子一般都只能-1,0,1,當絕對值大於1時則AVL失去平衡。例如在完成插入後,失衡只有那些從插入點到根節點路徑上的平衡可能被打破,當沿著這條路徑上溯到根,找到插入新節點後失去平衡的最小子樹根節點的指標x,然後再調整這個子樹使之重新平衡。注意當失去平衡的最小子樹(這樣的最小子樹指的是離插入點最近)調整平衡後,原有的其他所有不平衡樹無需調整,整個二叉樹迴歸平衡,使得BST操作效能始終維持在對數級別。這種調整所需要的主要技術就是旋轉,下面具體介紹:
1 插入點位於x的左孩子的左子樹中--左左LL;
2 插入點位於x的左孩子的右子樹-中-左右LR;
3 插入點位於x的右孩子的左子樹-中-右左RL;
4 插入點位於x的右孩子的右子樹-中-右右RR;
1和4是對稱的,成為外側插入,通過單旋轉解決;2和3是對稱的,成為內側插入,通過單旋轉解決。
1 LL型調整(右旋)
設最深節點k2,最深節點左孩子k1,在k1左子樹再插入一個新節點,使得k2平衡因子大於1不平衡,如圖,k2原始平衡因子為1,矩形代表一個高度h的子樹,帶陰影方框為插入的一個節點,採用的方法單向右轉:
class avlnode{
private:
int key;
int height;
int freq;
avlnode*left;
avlnode*right;
avlnode(int h=1):left(NULL),right(NULL),height(h),freq(1){}
};
int get_height(avlnode *r){
if(r)return r->height;
else return 0;
}
void singlerotate_left(avlnode *&r){
avlnode *k2=r;
avlnode *k1=k2->left;
k2->left=k1->right;
k1->right=k2;
k2->height=max(get_height(k2->left),get_height(k2->right))+1;
k1->height=max(get_height(k1->left),get_height(k1->right))+1;
}
2 RR型調整如圖,採用方法單向左旋:
void singlerotate_right(avlnode*&r){
avlnode*k2=r;
avlnode*k1=k2->right;
k2->right=k1->left;
k1->left=k2;
k2->height=max(get_height(k2->left),get_height(k2->right))+1;
k1->height=max(get_height(k1->left),get_height(k1->right))+1;
}
3 LR型調整此時在最深節點k2的左孩子k1的右子樹插入節點,使得k2平衡因子大於1,調整的方法是先單向左旋即將k2,最後單向右旋:
void doubleLeft_right(avlnode*&r){
avlnode*k2=r;
avlnode*k1=r->left;
singlerotate_right(k1);
singlerotate_left(k2);
}
4 RL型調整
類似LR型直接貼出程式碼就行。
void doubleright_left(avlnode*&r){
avlnode*k2=r;
avlnode*k1=r->right;
singlerotate_left(k1);
singlerotate_right(k2);
}
相應增刪查操作如下:
注意增加操作類似BST,只不過在完成插入後需要進行調整;查詢操作一樣;刪除操作也分為幾種情況:
首先在樹中搜尋是否有節點的元素值等於需要刪除的元素。如未搜尋到,直接返回。否則執行以下操作。
(1)要刪除的節點是當前根節點T。如果左右子樹都非空。在高度較大的子樹中實施刪除操作。分兩種情況:A、左子樹高度大於右子樹高度,將左子樹中最大的那個元素賦給當前根節點,然後刪除左子樹中元素值最大的那個節點。B、左子樹高度小於右子樹高度,將右子樹中最小的那個元素賦給當前根節點,然後刪除右子樹中元素值最小的那個節點。如果左右子樹中有一個為空,那麼直接用那個非空子樹或者是NULL替換當前根節點即可。
(2)要刪除的節點元素值小於當前根節點T值,在左子樹中進行刪除。遞迴呼叫,在左子樹中實施刪除。這個是需要判斷當前根節點是否仍然滿足平衡條件,如果滿足平衡條件,只需要更新當前根節點T的高度資訊。否則,需要進行旋轉調整:如果T的左子節點的左子樹的高度大於T的左子節點的右子樹的高度,進行相應的單旋轉。否則進行雙旋轉。(3)要刪除的節點元素值大於當前根節點T值,在右子樹中進行刪除。過程與上述步驟類似。
程式碼:
//AVL
int max1(int x,int y){
return (x>y? x:y);
}
class avlnode{
public:
int key;
int height;
int freq;
avlnode*left;
avlnode*right;
avlnode(int h=0):left(NULL),right(NULL),height(h),freq(1){}
};
int get_height(avlnode *r){
if(r)return r->height;
else return 0;
}
avlnode* singlerotate_left(avlnode *k2){
avlnode *k1=k2->left;
k2->left=k1->right;
k1->right=k2;
k2->height=max1(get_height(k2->left),get_height(k2->right))+1;
k1->height=max1(get_height(k1->left),get_height(k1->right))+1;
return k1;
}
avlnode* singlerotate_right(avlnode*k2){
avlnode*k1=k2->right;
k2->right=k1->left;
k1->left=k2;
k2->height=max1(get_height(k2->left),get_height(k2->right))+1;
k1->height=max1(get_height(k1->left),get_height(k1->right))+1;
return k1;
}
avlnode* doubleLeft_right(avlnode*k2){
avlnode*k1=k2->left;
k2->left=singlerotate_right(k1);
return singlerotate_left(k2);
}
avlnode* doubleright_left(avlnode*k2){
avlnode*k1=k2->right;
k2->right=singlerotate_left(k1);
return singlerotate_right(k2);
}
avlnode* avlinsert(avlnode*&r,int k){
if(r==NULL){r=new avlnode();r->key=k;}
else{
if(r->key>k){
r->left=avlinsert(r->left,k);//類似BST
if(get_height(r->left)-get_height(r->right)==2)
{ if(r->left->key>k)r=singlerotate_left(r);//LL
else r=doubleLeft_right(r);//LR
}//不平衡
}//BST左子樹
else if(r->key<k){
r->right=avlinsert(r->right,k);
if(get_height(r->right)-get_height(r->left)==2)
{if(r->right->key<k)r=singlerotate_right(r);//RR
else r=doubleright_left(r);
}
}//BST右子樹
else ++(r->freq);
}
r->height=max1(get_height(r->left),get_height(r->right))+1;
return r;
}
void avlinsert1(avlnode*&r,int k){
if(r==NULL){r=new avlnode();r->key=k;}
else{
if(r->key>k){
avlinsert1(r->left,k);//類似BST
}//BST左子樹
else if(r->key<k){
avlinsert1(r->right,k);
}//BST右子樹
else ++(r->freq);
}
r->height=max1(get_height(r->left),get_height(r->right))+1;
}
void avlcreate(avlnode*&r,int a[],int n){
int i;
r=NULL;
for(i=0;i<n;i++)
avlinsert(r,a[i]);
}
int avldelete(avlnode*&root,int k){
if(root==NULL)return 0;
else {int res;
if(k<root->key){
res=avldelete(root->left,k);
if(res){
if(get_height(root->right)-get_height(root->left)==2)
{if(root->right->left&&get_height(root->right->left)>get_height(root->right->right))
doubleright_left(root);
else singlerotate_right(root);
}
}
}//左子樹
else if(k>root->key){
res=avldelete(root->right,k);
if(res){
if(get_height(root->left)-get_height(root->right)==2)
{if(root->left->right&&get_height(root->left->right)>get_height(root->left->left))
doubleLeft_right(root);
else singlerotate_left(root);
}
}
}//右子樹
else{
if(root->left&&root->right){
if(get_height(root->left)>get_height(root->right))
{avlnode*p1=root;avlnode*p=root->left;
while(p->right){p1=p;p=p->right;}
//選出左子樹最大值
root->key=p->key;
avldelete(root->left,root->key);//刪除左子樹最大值
}
else {
avlnode*p1=root;avlnode*p=root->right;
while(p->left){p1=p;p=p->left;}
//選出右子樹最小值
root->key=p->key;
avldelete(root->right,root->key);
}//選出右子樹最小值
}//左右子樹非空,看哪邊高
//這裡也可以採用BST寫法不是賦值而是節點取代,只不過麻煩一丟
else {
avlnode*tmp=root;
root=(root->left? root->left:root->right);
delete tmp;
tmp=NULL;
}//左右子樹有一個不為空
}
}
return 1;
}
void inorder(avlnode*r){
if(r){inorder(r->left);
cout<<r->key<<" "<<r->height<<endl;
inorder(r->right);
}
}
void avldis(avlnode*r){
inorder(r);cout<<endl;
}
void bfs(avlnode*r){
if(r==NULL)return;
avlnode *q[100];
int front=-1,rear=-1;
rear++;
q[rear]=r;
while(front<rear){
front++;
avlnode*p=q[front];
cout<<p->key<<" ";
if(p->left){rear++;q[rear]=p->left;}
if(p->right){rear++;q[rear]=p->right;}
}
}
void avlbfs(avlnode*r){
bfs(r);
cout<<endl;
}
avlnode* avlsearch(avlnode*r,int k){
if(r==NULL)return NULL;
else if(k==r->key)return r;
else if(k<r->key)return avlsearch(r->left,k);
else return avlsearch(r->right,k);
}
AVL樹的測試
首先新建一棵AVL樹,然後 依次新增" 3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9 " 到AVL樹中;新增完畢之後,再將8 從AVL樹中刪除。AVL樹的新增和刪除過程如下圖:(01) 新增3,2
新增3,2都不會破壞AVL樹的平衡性。
(02) 新增1
新增1之後,AVL樹失去平衡(LL),此時需要對AVL樹進行旋轉(LL旋轉)。旋轉過程如下:
(03) 新增4
新增4不會破壞AVL樹的平衡性。
(04) 新增5
新增5之後,AVL樹失去平衡(RR),此時需要對AVL樹進行旋轉(RR旋轉)。旋轉過程如下:
(05) 新增6
新增6之後,AVL樹失去平衡(RR),此時需要對AVL樹進行旋轉(RR旋轉)。旋轉過程如下:
(06) 新增7
新增7之後,AVL樹失去平衡(RR),此時需要對AVL樹進行旋轉(RR旋轉)。旋轉過程如下:
(07) 新增16
新增16不會破壞AVL樹的平衡性。
(08) 新增15
新增15之後,AVL樹失去平衡(RR),此時需要對AVL樹進行旋轉(RR旋轉)。旋轉過程如下:
(09) 新增14
新增14之後,AVL樹失去平衡(RL),此時需要對AVL樹進行旋轉(RL旋轉)。旋轉過程如下:
(10) 新增13
新增13之後,AVL樹失去平衡(RR),此時需要對AVL樹進行旋轉(RR旋轉)。旋轉過程如下:
(11) 新增12
新增12之後,AVL樹失去平衡(LL),此時需要對AVL樹進行旋轉(LL旋轉)。旋轉過程如下:
(12) 新增11
新增11之後,AVL樹失去平衡(LL),此時需要對AVL樹進行旋轉(LL旋轉)。旋轉過程如下:
(13) 新增10
新增10之後,AVL樹失去平衡(LL),此時需要對AVL樹進行旋轉(LL旋轉)。旋轉過程如下:
(14) 新增8
新增8不會破壞AVL樹的平衡性。
(15) 新增9
但是新增9之後,AVL樹失去平衡(LR),此時需要對AVL樹進行旋轉(LR旋轉)。旋轉過程如下:
新增完所有資料之後,得到的AVL樹如下:
接著,刪除節點8.刪除節點8並不會造成AVL樹的不平衡,所以不需要旋轉,操作示意圖如下:
三 紅黑樹
歷史上AVL樹流行的另一個版本是紅黑樹RBT。對紅黑樹的操作在最壞情況下花費O(log n)時間,而且相對於普通AVL樹可能更容易實現。相比較AVL,
1 AVL是嚴格平衡樹,故在增加或者刪除節點時,調整所需旋轉較多;
2 RBT是平衡與操作複雜性上做了tradeoff,是弱平衡,用非嚴格的平衡換取增刪節點時操作的簡單。故當操作主要集中在搜尋而非增刪時應採用AVL;如果增刪操作更多,為了提高可操作性應該選擇RBT。其定義:
1 首先是BST,故中序遍歷後是非遞減序列;
2 弱化的平衡樹;
3 不同於BST,AVL,RBT每個節點含有五個域,color/key/left/right/p;
4 根節點一定是黑的(特性2),全空的這種葉子節點也是黑的(特性3),其他每個節點非黑即紅(特性1);
5 如果葉子節點是紅的,那麼兩個孩子肯定都是黑的(特性4);
6 對於每一個節點,從該節點到其子孫節點(直到NULL)的所有路徑上包含相同數目的黑節點(特性5)
這種著色原則導致RBT高度最多2log(n+1),因此查詢肯定是對數操作。通過每條路徑上各個節點著色方式的限制,RBT確保沒有一條路徑會比其他路徑長出2倍,因而是一種廣泛意義上的平衡。同其他BST一樣,問題在於一個新節點的插入,通常做法是將其放在葉子上。如果把它改成黑色,那麼肯定違反路徑同數目黑色的原則,因此肯定要標為紅色:如果它的父節點是黑色,那麼插入完成;如果父節點是紅色,那麼違反父紅子黑原則,故需要調整。主要是顏色的改變和旋轉。
我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那麼可能會違背紅黑樹的性質。為了保持紅黑樹的性質,我們可以通過對樹進行旋轉,即修改樹種某些結點的顏色及指標結構,以達到對紅黑樹進行插入、刪除結點等操作時,紅黑樹依然能保持它特有的性質.
RBT的旋轉:
1 左旋(逆時針旋轉)
可以看到類似AVL裡的LL型調整,也是逆時針旋轉
2 右旋(順時針旋轉)
可以看到類似AVL樹裡的RR型調整即順時針旋轉。
但是旋轉只能保持作為平衡樹的性質,而RBT紅黑性不能保持,故還需要顏色重塗來保證。總結來看就是兩條:1 部分結點顏色,重新著色;2 調整部分指標的指向,即左旋、右旋。
下面將通過圖和程式碼來闡述複雜的插入和刪除:
1 RBT的插入
向一棵含有n個結點的紅黑樹插入一個新結點的操作可以在O(lgn)時間內完成。
插入前:選擇的插入節點是紅色,按照BST準則找到合適位置;
插入時:
情況1 插入的就是根節點
原樹是空樹,插入紅節點,違背根黑;
對策:需要把節點塗成黑色;
void insert_case1(node *n){
if(n->parent==NULL)
n->color=BLACK;//就是根節點沒有父節點
else
insert_case2(n);
}
情況2 插入的節點的父節點是黑色
成功插入
void insert_case2(node *n){
if(n->parent&&n->parent->color==BLACK)
return;
else insert_case3(n);
}
情況3 插入的節點的父節點是紅色且叔叔節點是紅色
此時祖父節點一定存在,與此同時,又分為父結點是祖父結點的左子還是右子,對於對稱性,我們只要解開一個方向就可以,在此,我們只考慮父結點為祖父左子的情況。
對策:將當前節點的父節點和叔叔節點塗黑,祖父結點塗紅,把當前結點指向祖父節點,從新的當前節點重新開始演算法。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
“當前節點”和“父節點”都是紅色,違背“特性(4)”。所以,將“父節點”設定“黑色”以解決這個問題。但是,將“父節點”由“紅色”變成“黑色”之後,違背了“特性(5)”:因為,包含“父節點”的分支的黑色節點的總數增加了1。 解決這個問題的辦法是:將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”。關於這裡,說明幾點:第一,為什麼“祖父節點”之前是黑色?這個應該很容易想明白,因為在變換操作之前,該樹是紅黑樹,“父節點”是紅色,那麼“祖父節點”一定是黑色。 第二,為什麼將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;能解決“包含‘父節點’的分支的黑色節點的總數增加了1”的問題。這個道理也很簡單。“包含‘父節點’的分支的黑色節點的總數增加了1”
同時也意味著 “包含‘祖父節點’的分支的黑色節點的總數增加了1”,既然這樣,我們通過將“祖父節點”由“黑色”變成“紅色”以解決“包含‘祖父節點’的分支的黑色節點的總數增加了1”的問題; 但是,這樣處理之後又會引起另一個問題“包含‘叔叔’節點的分支的黑色節點的總數減少了1”,現在我們已知“叔叔節點”是“紅色”,將“叔叔節點”設為“黑色”就能解決這個問題。 所以,將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;就解決了該問題。按照上面的步驟處理之後:當前節點、父節點、叔叔節點之間都不會違背紅黑樹特性,但祖父節點卻不一定。若此時,祖父節點是根節點,直接將祖父節點設為“黑色”,那就完全解決這個問題了;若祖父節點不是根節點,那我們需要將“祖父節點”設為“新的當前節點”,接著對“新的當前節點”進行分析。
void insert_case3(node *n){
if(uncle(n)&&uncle(n)->color==RED)
{ n->parent->color=BLACK;
uncle(n)=BLACK;
grnadpa(n)->color=RED;
insert_case1(grandpa(n));}//把祖父節點當作新節點重新開始
else
insert_case4(n);//否則叔叔是黑色節點
}
插入4節點變化前後:
情況4 插入的節點的父節點是紅色且叔叔節點是黑色或者空NIL,當前節點是父節點右子(父節點仍然是祖父的左孩子)
對策:當前節點的父節點做為新的當前節點,以新當前節點為支點左旋。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
首先,將“父節點”作為“新的當前節點”;接著,以“新的當前節點”為支點進行左旋。 為了便於理解,我們先說明第(02)步,再說明第(01)步;為了便於說明,我們設定“父節點”的代號為F(Father),“當前節點”的代號為S(Son)。為什麼要“以F為支點進行左旋”呢?根據已知條件可知:S是F的右孩子。而之前我們說過,我們處理紅黑樹的核心思想:將紅色的節點移到根節點;然後,將根節點設為黑色。既然是“將紅色的節點移到根節點”,那就是說要不斷的將破壞紅黑樹特性的紅色節點上移(即向根方向移動)。
而S又是一個右孩子,因此,我們可以通過“左旋”來將S上移!按照上面的步驟(以F為支點進行左旋)處理之後:若S變成了根節點,那麼直接將其設為“黑色”,就完全解決問題了;若S不是根節點,那我們需要執行步驟(01),即“將F設為‘新的當前節點’”。那為什麼不繼續以S為新的當前節點繼續處理,而需要以F為新的當前節點來進行處理呢?這是因為“左旋”之後,F變成了S的“子節點”,即S變成了F的父節點;而我們處理問題的時候,需要從下至上(由葉到根)方向進行處理;也就是說,必須先解決“孩子”的問題,再解決“父親”的問題;所以,我們執行步驟(01):將“父節點”作為“新的當前節點”。
void insert_case4(node *n){
if(n==n->parent->right&&n->parent==grandpa(n)->left)
{ rotate_left(n->parent);
n=n->left;//作為新節點
}
else if(n==n->parent->left&&n->parent==grandpa(n)->right)
{ rotate_right(n->parent);
n=n->right;//作為新節點
}
insert_case5(n);
}
接著上圖,7看成新插入的節點:
情況5 當前節點的父節點是紅,叔叔是黑或者空NIL,當前節點是父節點的左子(父節點又是祖父的左孩子)
解法:父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
為了便於說明,我們設定“當前節點”為S(Original Son),“兄弟節點”為B(Brother),“叔叔節點”為U(Uncle),“父節點”為F(Father),祖父節點為G(Grand-Father)。S和F都是紅色,違背了紅黑樹的“特性(4)”,我們可以將F由“紅色”變為“黑色”,就解決了“違背‘特性(4)’”的問題;但卻引起了其它問題:違背特性(5),因為將F由紅色改為黑色之後,所有經過F的分支的黑色節點的個數增加了1。那我們如何解決“所有經過F的分支的黑色節點的個數增加了1”的問題呢? 我們可以通過“將G由黑色變成紅色”,同時“以G為支點進行右旋”來解決。
void insert_case5(node *n){
n->parent->color=BLACK;
grandpa(n)->color=RED;
if(n==n->parent->left&&n->parent==grandpa(n)->left)
rotate_right(grandpa(n));//順時針
else
rotate_left(grandpa(n));/n 是其父節點的右孩子,而父節點P又是其父G的右孩子
}
接著上圖,相當於新節點為2
2 RBT的刪除
下面所有的文字,則是針對紅黑樹刪除結點後,所做的修復紅黑樹性質的工作:
前面我們將"刪除紅黑樹中的節點"大致分為兩步,在第一步中"將紅黑樹當作一顆二叉查詢樹,將節點刪除"後,可能違反"特性(2)、(4)、(5)"三個特性。第二步需要解決上面的三個問題,進而保持紅黑樹的全部特性。為了便於分析,我們假設"x包含一個額外的黑色"(x原本的顏色還存在),這樣就不會違反"特性(5)"。為什麼呢?通過刪除演算法,我們知道:刪除節點y之後,x佔據了原來節點y的位置。 既然刪除y(y是黑色),意味著減少一個黑色節點;那麼,再在該位置上增加一個黑色即可。這樣,當我們假設"x包含一個額外的黑色",就正好彌補了"刪除y所丟失的黑色節點",也就不會違反"特性(5)"。
因此,假設"x包含一個額外的黑色"(x原本的顏色還存在),這樣就不會違反"特性(5)"。現在,x不僅包含它原本的顏色屬性,x還包含一個額外的黑色。即x的顏色屬性是"紅+黑"或"黑+黑",它違反了"特性(1)"。現在,我們面臨的問題,由解決"違反了特性(2)、(4)、(5)三個特性"轉換成了"解決違反特性(1)、(2)、(4)三個特性"。RB-DELETE-FIXUP需要做的就是通過演算法恢復紅黑樹的特性(1)、(2)、(4)。刪除後修正的思想是:將x所包含的額外的黑色不斷沿樹上移(向根方向移動),直到出現下面的姿態:a)
x指向一個"紅+黑"節點。此時,將x設為一個"黑"節點即可。b) x指向根。此時,將x設為一個"黑"節點即可。c) 非前面兩種姿態。
情況1:當前節點是紅色:
解法,直接把當前節點染成黑色,結束。此時紅黑樹性質全部恢復。
情況2: 當前節點是黑色但也是根節點
void delete_case2(node*n){
if(n->color==BLACK&&n->parent==NULL)
return;
delete_case3(n);
}
解法:刪除成功,結束
情況3: 當前節點x是黑色且兄弟節點w是紅色(則(此時父節點和兄弟節點的子節點分為黑))
解法:把父節點染成紅色,把兄弟結點染成黑色,之後重新進入演算法(我們只討論當前節點是其父節點左孩子時的情況)。然後,針對父節點做一次左旋。此變換後把問題轉化為兄弟節點為黑色的情況。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
這樣做的目的是將“Case 3”轉換為“Case 4”、“Case 5”或“Case 6”,從而進行進一步的處理。對x的父節點進行左旋;左旋後,為了保持紅黑樹特性,就需要在左旋前“將x的兄弟節點設為黑色”,同時“將x的父節點設為紅色”;左旋後,由於x的兄弟節點發生了變化,需要更新x的兄弟節點,從而進行後續處理。
void delete_case3(node *n){
if(w->color==RED)
{
w->color=BLACK;
n->parent->color=RED;
rotate_left(n->parent);//左旋
}
else
delete_case4(n);
}
情況4:當前節點是黑色,
解法:把當前節點和兄弟節點中抽取一重黑色追加到父節點上,把父節點當成新的當前節點,重新進入演算法。
void delete_case4(node *n){
if(w->left->color==BLACK&&w->right->color==BLACK){
w->color=RED;
n=n->parent;//父節點作為新節點
}
else delete_case1(n);//重新進入演算法
}
情況5:當前節點顏色是黑色,兄弟節點是黑色,兄弟的左子是紅色,右子是黑色。
解法:把兄弟結點染紅,兄弟左子節點染黑,之後再在兄弟節點為支點解右旋,之後重新進入演算法。此是把當前的情況轉化為情況6,而性質得以保持。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
我們處理“Case 3”的目的是為了將“Case 5”進行轉換,轉換成“Case 6”,從而進行進一步的處理。轉換的方式是對x的兄弟節點進行右旋;為了保證右旋後,它仍然是紅黑樹,就需要在右旋前“將x的兄弟節點的左孩子設為黑色”,同時“將x的兄弟節點設為紅色”;右旋後,由於x的兄弟節點發生了變化,需要更新x的兄弟節點,從而進行後續處理。
void delete_case5(node*n){
if(w->left->color==RED&&w->right->color==BLACK){
w->left->color=BLACK;
w->color=RED;
rotate_right(w);
w=n->parent->right; }
else
delete_case6(n);
}
情況6:當前節點顏色是黑色,它的兄弟節點是黑色,但是兄弟節點的右子是紅色,兄弟節點左子的顏色任意。
解法:把兄弟節點染成當前節點父節點的顏色,把當前節點父節點染成黑色,兄弟節點右子染成黑色,之後以當前節點的父節點為支點進行左旋,此時演算法結束,紅黑樹所有性質調整正確。
下面談談為什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
我們處理“Case 6”的目的是:去掉x中額外的黑色,將x變成單獨的黑色。處理的方式是“:進行顏色修改,然後對x的父節點進行左旋。下面,我們來分析是如何實現的。為了便於說明,我們設定“當前節點”為S(Original Son),“兄弟節點”為B(Brother),“兄弟節點的左孩子”為BLS(Brother's Left Son),“兄弟節點的右孩子”為BRS(Brother's Right Son),“父節點”為F(Father)。我們要對F進行左旋。但在左旋前,我們需要調換F和B的顏色,並設定BRS為黑色。為什麼需要這裡處理呢?因為左旋後,F和BLS是父子關係,而我們已知BL是紅色,如果F是紅色,則違背了“特性(4)”;為了解決這一問題,我們將“F設定為黑色”。
但是,F設定為黑色之後,為了保證滿足“特性(5)”,即為了保證左旋之後:第一,“同時經過根節點和S的分支的黑色節點個數不變”。若滿足“第一”,只需要S丟棄它多餘的顏色即可。因為S的顏色是“黑+黑”,而左旋後“同時經過根節點和S的分支的黑色節點個數”增加了1;現在,只需將S由“黑+黑”變成單獨的“黑”節點,即可滿足“第一”。第二,“同時經過根節點和BLS的分支的黑色節點數不變”。若滿足“第二”,只需要將“F的原始顏色”賦值給B即可。之前,我們已經將“F設定為黑色”(即,將B的顏色"黑色",賦值給了F)。至此,我們算是調換了F和B的顏色。第三,“同時經過根節點和BRS的分支的黑色節點數不變”。在“第二”已經滿足的情況下,若要滿足“第三”,只需要將BRS設定為“黑色”即可。經過,上面的處理之後。紅黑樹的特性全部得到的滿足!接著,我們將x設為根節點,就可以跳出while迴圈(參考虛擬碼);即完成了全部處理。至此,我們就完成了Case
6的處理。理解Case 4的核心,是瞭解如何“去掉當前節點額外的黑色”。
void delete_case6(node *n){
w->color=n->parent->color;
n->parent->color=BLACK;
w->right->color=BLACK;
rotate_left(n->parent);
}
紅黑樹的插入、刪除情況時間複雜度的分析
因為每一個紅黑樹也是一個特化的二叉查詢樹,因此紅黑樹上的只讀操作與普通二叉查詢樹上的只讀操作相同。然而,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。恢復紅黑樹的屬性需要少量(O(log n))的顏色變更(實際是非常快速的)和不超過三次樹旋轉(對於插入操作是兩次)。雖然插入和刪除很複雜,但操作時間仍可以保持為 O(log n) 次。
四 B-/B+樹
由於篇幅有限,再加上B樹獨有特徵,B樹相關知識在下一篇介紹。
具體參考:
http://www.cnblogs.com/skywang12345/p/3245399.html#a34