1. 程式人生 > >二叉樹之一BST樹,AVL樹詳解及B樹和紅黑樹原理分析

二叉樹之一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