天天看點

史上最快平衡樹——紅黑樹

紅黑樹是一種二叉搜尋樹,單次操作複雜度上限$logn$,效率極高,基本用指針實作。

為了減小常數,紅黑樹的操作全部非遞歸實作。

下面系統介紹一下紅黑樹,包括複雜度的證明和基本操作。

1、紅黑樹的結構:

  紅黑樹是二叉搜尋樹,滿足BST性質,左兒子數值都小于目前節點,右兒子數值都大于目前節點,中序周遊單調遞增。

  紅黑樹根節點的父親和葉節點的兒子指向同一個節點,稱為紅黑樹的葉節點。  

  葉節點不僅具有哨兵的作用,也可以減少情況,簡化代碼,哨兵可以當作普通的黑色節點。

  葉節點以外得點被稱為紅黑樹的内節點。

  每個節點有六個域,分别為$key$,$si$,$we$,$co$,$f$和$ch$,$key$代表目前點的權值,$we$代表目前值的個數,$si$代表子樹大小,$ch$代表左右兒子,$f$代表父親,$co$代表目前節點的顔色。

  設$bh(x)$為節點$x$到葉節點的一條路徑上的黑色節點數目,稱為該節點的黑高,紅黑樹的黑高為根節點的黑高。

  設$h(x)$為節點$x$到葉節點的所有路徑中最長的一條的長度。

  設$rt$代表紅黑樹的根。

  一顆完整的紅黑樹如下圖:

史上最快平衡樹——紅黑樹

2、紅黑樹的性質:

  一顆完整的紅黑樹具有以下性質:

    1、每個節點都是紅色或黑色;

    2、根節點和葉節點是黑色;

    3、紅色節點的兒子都是黑色;

    4、從根節點到葉節點的每條路徑上經過的黑色節點數相同。

  這些性質保證了紅黑樹的優秀複雜度。

3、紅黑樹複雜度的證明:

  我們要證明紅黑樹的時間複雜度為$O(logn)$。

  引理一:紅黑樹中沒有任何一條從根節點到葉節點的路徑比另一條長出一倍。

  證明:

    從每個節點到葉子的路徑上的黑色節點數稱為該節點的黑高度,記為$bh$。

    根據性質4,根節點到葉節點的每條路徑上黑色節點數相同;再根據性質3,紅色節點的兒子都是黑色。

    把根節點到葉節點的路徑看成一個序列,把紅色節點插入到黑色節點之間,則沒有兩個紅色節點相鄰。

    是以一條路徑上紅色節點數不會超過黑色節點數,沒有任何一條路徑比另一條長出一倍。

    證畢。

  引理二:以$x$為根的子樹中至少包含$2^{hb(x)}-1$個内節點。

    用數學歸納法證明。

    如果$hb(x)=0$,$x$一定是葉節點,結論顯然成立。

    對于其他節點,每個節點都有兩個兒子,根據兒子的顔色,每個兒子的黑高為$hb(x)$或$hb(x)-1$,并且兒子節點的黑高都小于目前節點的黑高。

    根據前一布的歸納可以得出,以兒子節點為根的子樹内至少有$2^{hb(x)-1}-1$個節點。

    是以最次情況下,即兩個兒子的黑高都是$hb(x)-1$的情況下,目前子樹大小可以去得最小值$2^{hb(x)}-1$,其餘情況下均大于這個值。

  引理三:一顆含有$n$個節點的紅黑樹,高度至多為$2log(n+1)$。

    根據性質3和性質4,從根到葉節點的其中一條簡單路徑上黑色節點占一半以上。

    是以$hb(x)>=\frac{h(x)}{2}$,當$x$為根時,根據引理二,可以得到不等式:

      $n>=2^{\frac{h}{2}}-1$

    移項後取對數可得:

      $h<=2lg(n+1)$

4、旋轉:

  旋轉是紅黑樹的必要操作。

  紅黑樹的旋轉和其他的帶旋平衡樹類似,會帶旋treap,splay,AVL或SBT的大佬可以選擇跳過。

  旋轉分為左旋和右旋,都是在維護BST性質下對樹的結構進行的局部調整。

  記住是局部調整,對紅黑樹的其他位置都沒有影響。

  定義$rotate(x,p)$表示以$x$的父親為支點左/右旋,0代表右旋,1代表左旋。

  一張圖了解一下:

    

史上最快平衡樹——紅黑樹

  旋轉要保證$x$和$y$均不為空(均不是哨兵),對于$\alpha$,$\beta$和$\gamma$則沒有特殊要求。

  以0代表左兒子,以1代表右兒子,就是$p^1$兒子過繼給父親,原來的祖父變為父親,原來的父親變為兒子。

  左旋和右旋改變的僅有父指針和兒子指針,以及pushup時更新的子樹大小,節點的其他域都沒有改變。

  經過旋轉,可以調節紅黑樹的整體結構,使其更加平衡。

  代碼如下:

史上最快平衡樹——紅黑樹
史上最快平衡樹——紅黑樹

void rotate(node *now,int pos){//旋轉操作
    node *c=now->ch[pos^1];
    now->ch[pos^1]=c->ch[pos];
    if(c->ch[pos]->si) c->ch[pos]->f=now;
    c->f=now->f;
    if(!now->f->si) rt=c;//旋到根
    else now->f->ch[now->f->ch[0]!=now]=c;
    c->ch[pos]=now;now->f=c;c->si=now->si;
    now->pushup();//更新子樹大小
}      

旋轉

  左旋和右旋的時間複雜度為$O(1)$但常數極大,制約了平衡樹的效率。

  而紅黑樹通過紅黑染色,減少旋轉的次數,使得每次旋轉的次數不超過$\frac{2}{3}logn$,$n$為目前的紅黑樹大小,極大地優化了常數,這也是紅黑樹效率高的原因。

5、紅黑樹的查詢

  由于紅黑樹的插入和删除非常繁瑣,我們在這裡先讨論查詢操作。

  紅黑樹僅在插入和删除時會有結構的調整,其他情況下結構是固定的,可以和BST一樣直接查詢。

  由于滿足BST性質,所有查詢都是從根開始。

  1、查某個數$val$的排名:

    如果目前節點權值大于$val$,查詢左兒子即可;

    如果目前節點權值小于$val$,把右子樹大小和目前節點大小累加進答案,然後查詢右兒子;

    如果目前節點權值等于$val$,結束查詢,答案加$1$(因為此時的答案為小于$val$的數的個數)。

  2、查詢排名為$rnk$的數:

    如果目前節點左子樹大小大于$rnk$,查詢左兒子;

    如果目前節點左子樹大小和目前節點大小之和小于$rnk$,查詢右兒子;

    其餘情況下結束查詢,傳回目前節點權值。

  3、查找某一個數$val$:

    如果目前節點權值大于$val$,查詢左兒子;

    如果目前節點權值小于$val$,查詢右兒子;

    如果目前節點權值等于$val$,結束查詢,傳回目前節點。

  4、查詢值$val$的前驅:

    如果目前節點權值小于$val$,用目前點權值更新答案(取$max$),查詢右兒子;

    如果目前節點權值等于$val$,結束查詢,傳回答案。

  5、查詢值$val$的後繼:

    初始化答案為$inf$。

    如果目前節點權值大于$val$,用目前點權值更新答案(取$minx$),查詢左兒子;

史上最快平衡樹——紅黑樹
史上最快平衡樹——紅黑樹
node *find(reg node *now,int key){//查找位置
    for(;now->si&&now->key!=key;now=now->ch[now->key<key]);
    return now;
}
int rnk(int key){//查排名
    reg int res,ans=0;
    for(reg node *now=rt;now->si;){
        res=now->ch[0]->si;
        if(now->key==key) break;
        else if(now->key>key) now=now->ch[0];
        else{
            ans+=res+now->we;now=now->ch[1];
        }
    }
    return ans+res+1;
}
int kth(int k){//查數
    reg int res;reg node *now=rt;
    for(;now->si;){
        res=now->ch[0]->si;
        if(k<=res) now=now->ch[0];
        else if(res+1<=k&&k<=res+now->we) break;
        else{
            k-=res+now->we;now=now->ch[1];
        }
    }
    return now->key;
}
int pre(int key){//前驅
    reg int res=0;
    for(reg node *now=rt;now->si;){
        if(now->key<key){
            res=now->key;now=now->ch[1];
        }
        else now=now->ch[0];
    }
    return res;
}
int nxt(int key){//後繼
    reg int res=0;
    for(reg node *now=rt;now->si;){
        if(now->key>key){
            res=now->key;now=now->ch[0];
        }
        else now=now->ch[1];
    }
    return res;
}      

查詢

6、紅黑樹的插入:

  如果樹中有對應權值,那麼問題很簡單,找到對應權值,将路徑上的節點$si$加一即可。

  但是對于其他情況如何處理。

  插入時會影響到紅黑樹的整體結構,破壞紅黑樹的性質。

  為了維持優秀的複雜度及常數,我們需要對插入進行修正。

  在插入的同時維護紅黑樹的性質并不簡單,我們可以先找到一個位置,将新節點插進去,再對樹進行調整。

  插入代碼如下:

史上最快平衡樹——紅黑樹
史上最快平衡樹——紅黑樹
inline void insert(int key){//插入節點
    reg node *now=rt,*fa=nul;int pos;
    for(;now->si;now=now->ch[pos]){
        now->si++;fa=now;
        pos=now->getpos(key);
        if(pos==-1){//找到對應值
            now->we++;return;
        }
    }
    now=New(key);//找到位置,插入節點
    if(fa->si) fa->ch[key>fa->key]=now;
    else rt=now;
    now->f=fa;insert_transfrom(now);
}      

插入

  然後我們就可以進行繁瑣的修正過程了。

  首先對節點有如下定義:

  

史上最快平衡樹——紅黑樹

  其中,$fa$代表父親,$gr$代表祖父,$un$代表叔叔,$br$代表兄弟。  

  插入的新節點要染一個顔色,那麼染什麼顔色好呢。

  由于我們并不知道樹的具體形态和着色方案,是以染紅色或黑色都有可能破壞樹的結構和性質。

  我們可以發現,染成黑色可能破壞性質4,染成紅色可能破壞性質3和2,然而可以發現性質3的修正比較容易,是以建立節點染成紅色。

  我們需要一個疊代過程進行修正。

  疊代的過程中要維持性質4不被破壞,并且破壞性質2和3的點不超過一個,不然我們的操作就沒有意義了。

  初始時最多有一個不滿足性質的節點,也就是新插入的節點,每次修正都會修正目前節點,并産生至多一個新的不滿足性質3的節點。而且這個節點的深度一定小于之前的點,是以紅黑樹修正的時間複雜度為$O(logn)$。

  可以發現因為破壞性質2和3都需要該點為紅色,而破壞性質3還需要父親為紅色。

  如果破壞性質2,那麼該點一定為根節點,直接染黑即可。

  其餘情況下,如果父親為黑色,一定滿足性質,疊代到達終點。而父親是紅色時,也就是需要調整時,祖父一定是黑色。

  情況一:叔叔為紅色。

  解決方法:将父親和祖父染黑,祖父染紅,繼續處理祖父。

  不難發現,紅黑樹的性質3得到修正,并且性質4沒有破壞。

  如圖所示:

史上最快平衡樹——紅黑樹

  情況二:叔叔為黑色,且目前點,父親和祖父不共線。

  解決方法:以父親為支點向目前點的反方向旋轉,繼續處理原父親。

  這樣可以在維持性質的同時轉化為情況三。

繼續閱讀