1. 程式人生 > 實用技巧 >紅黑樹-結點的插入

紅黑樹-結點的插入

紅黑樹介紹

紅黑樹本質上是一種二叉查詢樹,但它在二叉查詢樹的基礎上額外添加了一個標記(顏色),同時具有一定的規則。這些規則使紅黑樹保證了一種平衡,插入、刪除、查詢的最壞時間複雜度都為 O(logn)。它的統計效能要好於平衡二叉樹(AVL樹),這也正解釋了紅黑樹為什麼使用這麼廣的原因。

紅黑樹的5個特性

  1. 每個結點要麼是紅色,要麼是黑色。

  2. 根節點永遠是黑色的。

  3. 所有葉子結點(nil結點)都是黑色的。(這裡說的葉子結點是NULL結點)

  4. 每個紅色結點的兩個子結點一定都是黑色(不能有兩個連續(父-子連續)的紅色結點,可以有連續的黑色結點)。

  5. 從任一結點到其子樹中每個葉子結點的路徑都包含相同數量的黑色結點。(黑高相同)

正由於平衡二叉樹一樣,由性質決定了二叉樹是不是一個平衡二叉樹,當向平衡二叉樹插入一個結點後可能失衡的依據也是性質決定的,性質定義了什麼是平衡二叉樹。所以說,紅黑樹的性質解釋了什麼是紅黑樹,以及插入結點後,樹是否還能保持紅黑樹的性質。

紅黑樹是一個自平衡(不是像AVL絕對的平衡)的二叉查詢樹BST,但是也是需要保持平衡,它不是像AVL那樣,刻意的保持平衡,而是由紅黑樹的性質間接保持了Reb-Black-Tree的平衡,如不能有兩個連續的紅色結點,連續插入兩個結點必然會使性質 4不成立,通過recolor或者rotation來滿足性質4,調整的過程也間接使紅黑樹維持了相對平衡。如果熟悉了AVL如何調整平衡,那麼我感覺Red-Black-Tree的調整平衡應該也不在話下。

如下圖,就是一棵紅黑樹,NIL是一個黑色的空結點。

紅黑樹結點的插入

紅黑樹結點的插入操作:

  1. 紅黑樹本身是一棵二叉查詢樹,所以在插入新結點時,完全可以按照二叉查詢樹插入結點的方法,找到新結點插入的位置。
  2. 將插入的結點初始化為紅色,為什麼是紅色而不是黑色?若為黑色,則違背性質5。
  3. 插入結點後,可能會破壞紅黑樹的性質,此時需要調整二叉查詢樹,通過recolor或者rotatin,使其重新成為紅黑樹。

紅黑樹的調整主要有兩個操作,比AVL樹多了一個操作:

  1. recolor(變色,重新標記為黑色或紅色)
  2. rotation(旋轉)

優先recolor,recolor不能解決在rotation。

紅黑樹中插入結點後,主要有3大種6小種:

  • 紅色結點作為根節點,塗黑,直接插入。
  • 父結點是黑結點,直接插入紅色結點。
  • 父結點是紅色結點,這時候直接插入紅色結點,不滿足性質4。這時候就需要根據其叔叔結點是紅色還是黑色分別處理。(分為6小種)
    • 叔叔結點是紅色,uncle and parent discolor, 左右支處理情況相同
    • 叔叔結點是黑色,父結點位於祖父結點的左分支,插入結點位於父結點的左分支,旋轉加變色;插入結點位於父結點的右分支,旋轉旋轉加變色。
    • 叔叔結點是黑色,父結點位於祖父結點的右分支,插入結點位於父結點的右分支,旋轉加變色;插入結點位於父結點的左分支,旋轉旋轉加變色。

圖(4)中存在一個錯誤,即旋轉後P應該是C的子結點,程式碼中需要提前x = x->parent,然後下次迴圈時x->parent->parent才能正確處理。

圖(3)中U也可以看做是NIL結點(空結點),對於圖(1)根節點變紅,這不是違背了性質2,程式碼最後對根結點做了處理,最後總會變黑的!

在看程式碼之前,我覺得有必要了解紅黑樹結構的程式碼定義以及初始化一棵紅黑樹。

#include <stdio.h>
#include <stdlib.h>

typedef enum {RED, BLACK} ColorType;
typedef int ElemType;
/*樹結點型別定義*/
typedef struct RBNode {
    ElemType key;
    struct RBNode * lchild;
    struct RBNode * rchild;
    struct RBNode * parent;
    ColorType color;
} RBNode, *RBTree;

/*我把它理解為樹的頭結點*/
typedef struct RBTRoot {
    RBTree root;
    RBTree nil;
} RBTRoot;

/**
* 初始化一棵紅黑樹頭結點
*/
RBTRoot* RBTree_Init() {
   RBTRoot* T = (RBTRoot*) malloc(sizeof(RBTRoot));
   T->nil = (RBTree) malloc(sizeof(RBNode));
   if (T == NULL || T->nil == NULL)
        exit(-1);
   T->nil->lchild = T->nil->rchild = NULL;
   T->nil->parent = NULL;
   T->nil->color = BLACK;

   T->root = T->nil;
   return T;
}

下面以插入元素{10,9,8,7,6,5,4,3,2,1}為例,檢視紅黑樹如何從0構造為一棵樹。結合動圖看程式碼,我覺得能更容易理解程式碼:

首先插入元素10和9

繼續插入8之後,破壞了紅黑樹,此時需要旋轉加變色了。

看下涉及到的具體程式碼

/**
* 插入結點
* @param T 紅黑樹的根
* @param key 插入結點值
* @description 找到位置,初始化結點,插入結點,調整紅黑樹
*/
void RBTree_Insert(RBTRoot** T, ElemType key) {
    RBTree pre; // pre儲存了上一個x位置的指標(引用)
    RBTree x = (*T)->root;
    pre = x;
    while (x != (*T)->nil) {
        pre = x;
        if (x->key == key) {
            printf("\n%d已存在\n",key);
            return;
        } else if (key < x->key)
            x = x->lchild;
        else
            x = x->rchild;
    }

    // 初始化插入結點,將結點設為紅色
    x = (RBTree) malloc(sizeof(RBNode));
    x->key = key;
    x->lchild = x->rchild = (*T)->nil;
    x->color = RED;
    x->parent = pre;

    // 根節點引用沒有發生變化,
    // 插入根節點,直接插入,否則,向儲存x父結點的pre左孩子或者右孩子插入x
    if ((*T)->root == (*T)->nil)
        (*T)->root = x;
    else if (key < pre->key)
        pre->lchild = x;
    else
        pre->rchild = x;
    // 插入之後,進行修正處理
    RBTree_Insert_FixUp(*T, x);
}

/**
* 紅黑樹插入修正函式
* @param T 紅黑樹的根
* @param x 插入的結點
* @description 用到變色和旋轉
*/
void RBTree_Insert_FixUp(RBTRoot* T, RBTree x) {
   // 當前結點的父結點為紅色需調整 注:根節點 parent是指向nil
   // (1) 叔叔結點是紅色: (1.1,1.2)左右分支,一樣處理,變色即可
   // (2) 叔叔結點是黑色:(2.1,2.2)左分支下左右孩子 (2.3,2.4)右分支下左右孩子

   while (x->parent->color == RED) {
        // 首先父結點是祖父結點的左分支還是右分支
        if (x->parent == x->parent->parent->lchild) {
            // 判斷叔叔結點顏色
            if (x->parent->parent->rchild->color == RED) {
                // 將父結點和叔叔結點改為黑色,祖父結點改為紅色
                x->parent->color = BLACK;
                x->parent->parent->rchild->color = BLACK;
                x->parent->parent->color = RED;
                x = x->parent->parent;
            } else {
                // 叔叔結點的顏色為黑色:分父結點的(1)左孩子 (2)右孩子
                // (1)父結點左孩子:父結點顏色變為BLACK,祖父節點變RED,右旋處理
                if (x->parent->lchild == x) {
                    x->parent->color = BLACK;
                    x->parent->parent->color = RED;
                    // 注意這裡是x->parent->parent 以祖父節點作為旋轉的根節點
                    RBTree_R_Rotate(T, x->parent->parent);
                } else {
                // (2)右孩子,先左旋轉,轉為上面(1)父結點左孩子的情況
                // 藉助迴圈下次就會執行到(1)然後右旋

                    RBTree_L_Rotate(T, x->parent);
                }
            }
        } else {
            // 祖父結點的右分支
            if (x->parent->parent->lchild->color == RED) {
                // 叔叔結點為紅色,左右分支都一樣處理
                x->parent->color = BLACK;
                x->parent->parent->lchild->color = BLACK;
                x->parent->parent->color = RED;
                x = x->parent->parent;
            } else {
                // 叔叔為黑色
                // 父結點的右分支
                if (x->parent->rchild == x) {
                    x->parent->color = BLACK;
                    x->parent->parent->color = RED;
                    RBTree_L_Rotate(T, x->parent->parent);  // 左旋
                } else {
                    // 父結點的左分支
                    RBTree_R_Rotate(T, x->parent);  // 右旋
                }
            }
        }
        // 每次迴圈結束需要將根節點塗黑,因為可能在某次變色時,根節點變為紅色
        // 不可放在迴圈外部
        // T->root->color = BLACK;
   }
    T->root->color = BLACK;
}

void RBTree_R_Rotate(RBTRoot* T, RBTree x) {
    RBTree lc;
    lc = x->lchild;
    x->lchild = lc->rchild;
    if (lc->rchild != T->nil)
        lc->rchild->parent = x;

    lc->parent = x->parent;
    if (lc->parent == T->nil)
        T->root = lc;
    else if (x->parent->lchild == x)
        x->parent->lchild = lc;
    else if (x->parent->rchild == x)
        x->parent->rchild = lc;
    x->parent = lc;
    lc->rchild = x;
}

動手構建一棵紅黑樹,構建的過程中去程式碼中找構建的規律,去編寫程式碼理解程式碼。接下來把剩餘的結點插入。

這一個地方需要關注一下在插入3之後,眼看紅黑樹慢慢演變為單支樹,但是由於其性質,避免了單支樹的產生。這也是為什麼紅黑樹也是一棵自平衡樹。

參考