算法導論 之 紅黑樹 - 添加[C語言]

 

 

 

 

1 引言

    在之前的博文中,本人對平衡二叉樹的處理做了較詳盡的分析,有興趣的朋友可以參閱博文《算法導論 之 平衡二叉樹 - 創建 插入 搜索 銷燬》和《算法導論 之 平衡二叉樹 - 刪除》。平衡二叉樹AVL是嚴格的平衡樹,在增刪結點時,其旋轉操作的次數較多;而紅黑樹RBT則通過非嚴格的平衡來換取增刪結點時旋轉次數的降低。在應用中,如果搜索次數大於增刪次數,則選擇平衡二叉樹更好一些;而如果搜索與增刪次數接近時,紅黑樹則是更好的選擇。在有些資料中顯示,紅黑樹的統計性能要優於平衡二叉樹。

 

2 性質分析

    紅黑樹是一種二叉查找樹,但在每個節點上增加一個存儲位表示結點的顏色[RED或BLACK]。通過對任一結點到葉子結點的路徑上各個結點着色方式的限制,確保沒有一條路徑會是其他路徑的2倍,因而是接近平衡的!因此,它能保證在最壞情況下,基本的動態集合操作的時間複雜度爲O(log2@N)[log2@N:以2爲底數,N爲對數][注:理論上平衡二叉樹的性能要優於紅黑樹,但是仍處同一數量級]

圖1 紅黑樹

    紅黑樹有以下5種性質,所有對紅黑樹的處理都是圍繞這5種性質進行的。但是想通過以下5種性質的簡單描述就期望能夠深入理解紅黑樹,這似乎是不太可能的事情。因此,下面將結合圖1對其5種性質進行分別的解析。

 ①、每個節點要麼是紅色的,要麼是黑色的;

    解析:任何一個結點都有一種顏色 —— 非紅即黑。從圖1中可以清楚的看出這一點。

 ②、根結點是黑色的;

    解析:根結點只能爲黑色,不可能爲紅色。如圖1中的根結點爲13,其爲黑色。

 ③、所有葉子結點(NIL)都是黑色的;

    解析:在圖1中的所有葉子結點(NIL)的顏色只能爲黑色的,不可能爲紅色。

 ④、如果一個結點是紅色,則它的兩個兒子都是黑色的;

    解析:如圖1中的結點6、8、17、22、27爲紅色結點,其左右孩子結點只能爲黑色。即:樹中決不允許存在兩個連續的紅色結點。[注:但是允許兩個連續的黑色結點]

 ⑤、對任何一個結點,從該結點通過其子孫結點到達葉子結點(NIL)的所有路徑上包含相同數目的黑結點。

    解析:以根結點爲例,其通過子孫結點到達葉子節點(NIL)的路徑有如下幾種情況:

        路徑1:13(b) -> 8(r) -> 1(b) -> 葉子(b)            | 3個黑結點:13、1、葉子

        路徑2:13(b) -> 8(r) -> 11(b) -> 葉子(b)           | 3個黑結點:13、11、葉子

        路徑3:13(b) -> 8(r) -> 1(b) -> 6(r) -> 葉子(b)    | 3個黑結點:13、1、葉子

        路徑4: 13(b) -> 17(r) -> 25(b) -> 22(r) -> 葉子(b) | 3個黑結點:13、25、葉子

        路徑5: 13(b) -> 17(r) -> 25(b) -> 27(r) -> 葉子(b) | 3個黑結點:13、25、葉子

        路徑6: 13(b) -> 17(r) -> 15(b) -> 葉子(b)          | 3個黑結點:13、15、葉子

        ....

      至此,大家應該能夠明白性質⑤的真實含義了。

    爲了便於處理紅黑樹代碼的邊界條件,我們採用一個哨兵來代表NIL。對於一個紅黑樹而言,哨兵NIL是一個與樹內普通結點有相同域的對象。它的color域爲BLACK,而其他域(parent,lchild,rchild和key)的值我們並不關心。

    總之,紅黑樹是通過以上5種性質的限制約束了該樹的平衡性能 —— 即:該樹上的最長路徑長度不可能大於最短路徑長度的2倍,從而確保對樹操作的時間複雜度達到O(log2@N)。

 

3 編碼實現

3.1 結構定義

①、常值定義:增強代碼可讀性、方便代碼修改

/* 常值定義 */
#define RBT_COLOR_BLACK    'b'     /* 顏色:黑色 */
#define RBT_COLOR_RED      'r'     /* 顏色:紅色 */

#define RBT_LCHILD         (0)     /* 類型:左孩子 */
#define RBT_RCHILD         (1)     /* 類型:右孩子 */

#define RBT_MAX_DEPTH      (512)   /* 棧的深度(棧處理紅黑樹時使用) */

代碼1 常值定義

②、節點結構:在二叉查找樹的結構基礎上,新增color字段

/* 結點結構 */
typedef struct _rbt_node_t
{
    int key;                        /* 關鍵字 */
    int color;                      /* 結點顏色: RBT_COLOR_BLACK(黑) 或 RBT_COLOR_RED(紅) */
    struct _rbt_node_t *parent;     /* 父節點 */
    struct _rbt_node_t *lchild;     /* 左孩子節點 */
    struct _rbt_node_t *rchild;     /* 右孩子節點 */
}rbt_node_t;

代碼2 結點結構

③、樹結構:sentinel字段用於表示葉子結點(NIL)。當樹內結點的左(右)孩子爲葉子結點時,則將左(右)孩子指針指向sentinel字段。

/* 紅黑樹結構 */
typedef struct
{
    rbt_node_t *root;               /* 根節點 */
    rbt_node_t *sentinel;           /* 哨兵節點 */
}rbt_tree_t;

代碼3 樹結構

④、錯誤碼:用來記錄錯誤返回的類型,以便快速的確定程序中存在的異常

/* 錯誤碼 */
typedef enum
{
    RBT_SUCCESS                     /* 成功 */
    , RBT_FAILED = ~0x7fffffff      /* 失敗 */
    , RBT_NODE_EXIST                /* 結點存在 */
}rbt_ret_e;

代碼4 錯誤碼

⑤、宏定義:可有效的增強代碼的簡潔性、複用性、易讀性

#define rbt_set_color(node, c)  ((node)->color = (c))
#define rbt_set_red(node)   rbt_set_color(node, RBT_COLOR_RED)
#define rbt_set_black(node) rbt_set_color(node, RBT_COLOR_BLACK)
#define rbt_is_red(node)    (RBT_COLOR_RED == (node)->color)
#define rbt_is_black(node)   (RBT_COLOR_BLACK == (node)->color)

/* 設置左孩子 */
#define rbt_set_lchild(tree, node, left) \
{ \
    (node)->lchild = (left); \
    if (tree->sentinel != left) { \
        (left)->parent = (node); \
    } \
}

/* 設置右孩子 */
#define rbt_set_rchild(tree, node, right) \
{ \
    (node)->rchild = (right); \
    if (tree->sentinel != right) { \
        (right)->parent = (node); \
    } \
}

/* 設置孩子節點 */
#define rbt_set_child(tree, node, type, child) \
{ \
    if (RBT_LCHILD == type) { \
        rbt_set_lchild(tree, node, child); \
    } \
    else { \
        rbt_set_rchild(tree, node, child); \
    } \
}

代碼5 宏定義

3.2 創建對象

    創建的初始紅黑樹對象是一棵空樹,其根節點爲NULL,但是此時必須爲葉子結點(哨兵)分配好空間,並將葉子結點的顏色置爲黑色(性質3),以方便後續對樹的操作處理。

/******************************************************************************
 **函數名稱: rbt_creat
 **功    能: 創建紅黑樹對象(對外接口)
 **輸入參數: NONE
 **輸出參數: NONE
 **返    回: 紅黑樹對象地址
 **實現描述: 
 **注意事項: 
 **     1、每個結點要麼是紅色的,要麼是黑色的;
 **     2、根結點是黑色的;
 **     3、所有葉子結點(NIL)都是黑色的;
 **     4、如果一個結點是紅色,則它的兩個兒子都是黑色的;
 **     5、對任何一個結點,從該結點通過其子孫結點到達葉子結點(NIL)
 **         的所有路徑上包含相同數目的黑結點。
 **作    者: # Qifeng.zou # 2013.12.24 #
 ******************************************************************************/
rbt_tree_t *rbt_creat(void)
{
    rbt_tree_t *tree = NULL;
    
    tree = (rbt_tree_t *)calloc(1, sizeof(rbt_tree_t));
    if (NULL == tree) {
        return NULL;
    }

    tree->sentinel = (rbt_node_t *)calloc(1, sizeof(rbt_node_t));
    if (NULL == tree->sentinel) {
        free(tree);
        return NULL;
    }

    tree->sentinel->color = RBT_COLOR_BLACK;
    tree->root = tree->sentinel;

    return tree;
}

代碼6 創建對象

3.3 旋轉處理

    在添加和刪除過程中,可能破壞紅黑樹的5個性質之一,和平衡二叉樹的處理相似,可通過旋轉來恢復紅黑樹的性質,但紅黑樹的旋轉只有右旋和左旋2種。

右旋處理:

    以結點N爲支點,進行右旋轉的處理描述:結點N的左孩子A取代N的位置,並將結點A的右孩子AR作爲結點N的左孩子,再將結點N作爲結點A的右孩子。

圖2 右旋處理

[注:旋轉過程並不關注結點顏色]

右旋處理對應的代碼如下所示:

/******************************************************************************
 **函數名稱: rbt_right_rotate
 **功    能: 右旋處理
 **輸入參數: 
 **     tree: 紅黑樹
 **     node: 旋轉支點
 **輸出參數: NONE
 **返    回: RBT_SUCCESS:成功 RBT_FAILED:失敗
 **實現描述: 
 **        G                       G
 **        |                       |
 **        N            ->         L
 **      /   \                   /   \
 **     L     R                 LL    N
 **    / \   / \                     / \
 **   LL LR RL RR                   LR  R
 **                                    / \
 **                                   RL RR
 **            說明: 節點N爲旋轉支點
 **注意事項: 
 **作    者: # Qifeng.zou # 2014.01.15 #
 ******************************************************************************/
void rbt_right_rotate(rbt_tree_t *tree, rbt_node_t *node)
{
    rbt_node_t *parent = node->parent, *lchild = node->lchild;
    
    if (tree->sentinel == parent) {
        tree->root = lchild;
        lchild->parent = tree->sentinel;
    }
    else if (node == parent->lchild) {
        rbt_set_lchild(tree, parent, lchild);
    }
    else {
        rbt_set_rchild(tree, parent, lchild);
    }
    rbt_set_lchild(tree, node, lchild->rchild);
    rbt_set_rchild(tree, lchild, node);
}

代碼7 右旋處理

左旋處理:

    以結點N爲支點,進行右旋轉的處理描述:結點N的左孩子B取代N的位置,並將結點B的左孩子BL作爲結點N的右孩子,再將結點N作爲結點B的左孩子。

圖3 左旋處理

[注:旋轉過程並不關注結點顏色]

左旋處理對應的代碼如下:

/******************************************************************************
 **函數名稱: rbt_left_rotate
 **功    能: 左旋處理
 **輸入參數: 
 **     tree: 紅黑樹
 **     node: 旋轉支點
 **輸出參數: NONE
 **返    回: RBT_SUCCESS:成功 RBT_FAILED:失敗
 **實現描述: 
 **        G                       G
 **        |                       |
 **        N            ->         R
 **      /   \                   /   \
 **     L     R                 N    RR
 **    / \   / \               / \
 **   LL LR RL RR             L  RL
 **                          / \
 **                         LL LR
 **            說明: 節點N爲旋轉支點
 **注意事項: 
 **作    者: # Qifeng.zou # 2014.01.15 #
 ******************************************************************************/
void rbt_left_rotate(rbt_tree_t *tree, rbt_node_t *node)
{
    rbt_node_t *parent = node->parent, *rchild = node->rchild;
    
    if (tree->sentinel == parent) {
        tree->root = rchild;
        rchild->parent = tree->sentinel;
    }
    else if(node == parent->lchild) {
        rbt_set_lchild(tree, parent, rchild);
    }
    else {
        rbt_set_rchild(tree, parent, rchild);
    }
    rbt_set_rchild(tree, node, rchild->lchild);
    rbt_set_lchild(tree, rchild, node);
}

代碼8 左旋處理

3.4 添加操作

    1)當向一棵空樹中添加結點時,則新結點將作爲整棵樹根結點,且爲黑色(性質2);

圖4 空樹中添加根結點R

    2)當向一棵非空樹中添加一個結點時,新結點的顏色都是爲紅色。添加成功後,需要判斷是否破壞了紅黑樹的5個性質。經過分析,可以發現,向非空樹中添加一個結點,不可能破壞性質1、2、3、5,唯一可能被破壞只有性質4 —— 出現2個連續的紅結點[新節點和父節點爲紅色],且性質4被破壞,只有如下六種情況:

============================================================================

前提1:父節點P爲祖父節點G的左孩子

============================================================================

  情況1):叔結點U爲紅色

   前提條件:新結點N和父結點P都爲紅色,父結點P爲G的左孩子

   情況描述:叔結點U爲紅色時 [注:此時不必關心新結點N是左孩子還是右孩子]

   處理過程:[代碼:參考rb_insert_fixup()中的case 1]

        ①、將父結點P和叔結點U的顏色改爲黑色,將祖父結點G改爲紅色

        ②、把祖父結點作爲下一次判斷的對象

圖5 叔結點U爲紅色,新結點N爲左孩子

圖6 叔結點U爲紅色,新結點N爲右孩子

  情況2):叔結點爲黑色,新結點N爲右孩子

   前提條件:新結點N和父結點P都爲紅色,父結點P爲G的左孩子

   情況描述:叔結點U也爲黑色,新結點N爲右孩子時

   處理過程:[代碼:參考rb_insert_fixup()中的case 2]

        ①、調整顏色:將父結點P改爲黑色,將祖父結點改爲紅色

        ②、向右旋轉90度:父結點P取代祖父結點G的位置,同時將父結點的右子樹PR作爲祖父結點G的左子樹,祖父結點G作爲父結點P的右孩子

        ③、把祖父結點的父結點GP改爲下一次判斷的對象

圖7 叔結點U爲黑色,新結點N爲左孩子

[注意:藍色結點表示顏色可能爲紅,也可能爲黑]

  情況3):叔結點爲黑色,新結點N爲右孩子

   前提條件:新結點N和父結點P都爲紅色,父結點P爲G的左孩子

   情況描述:叔結點U也爲黑色,新結點N爲右孩子時

   處理過程:[代碼:參考rb_insert_fixup()中的case 3]

        ①、向左旋轉90度:新結點N取代父結點P的位置,同時將新結點的左子樹NL作爲父結點P的右子樹,父結點P作爲新結點N的左孩子

        ②、把父結點P改爲下一次判斷的對象 [注意:經過①、②、③處理後,情況3演變了情況2]

圖8 叔結點U爲黑色 新結點N爲右孩子

[注意:藍色表示顏色可能爲紅,也可能爲黑]

============================================================================

前提2:父節點P爲祖父節點G的右孩子

 

============================================================================

  情況4):叔結點U爲紅色

    前提條件:新結點N和父結點P都爲紅色,父結點P爲祖父G的右孩子

    情況描述:叔結點U爲紅色時 [注:此時不必關心新結點N是左孩子還是右孩子]

    處理過程:[代碼:參考rb_insert_fixup()中的case 4]

        ①、將父結點P和叔結點U的顏色改爲黑色,將祖父結點G改爲紅色

        ②、把祖父結點作爲下一次判斷的對象

圖9 叔結點U爲紅色 新結點N爲右孩子

圖10 叔結點U爲紅色 新結點N爲左孩子

  情況5):叔結點U爲黑色,新結點N爲父結點P的左孩子

    前提條件:新結點N和父結點P都爲紅色,父結點P爲祖父結點G的右孩子

    情況描述:叔結點U爲黑色,新結點N爲左孩子時

    處理過程:[代碼:參考rb_insert_fixup()中的case 5]

        ①、向右旋轉90度:新結點N取代父結點G的位置,同時將新結點的右子樹NR作爲父結點G的左子樹,父結點G作爲新結點的右孩子

        ②、把父結點P改爲下一次判斷的對象[此時case 5演變爲case 6]

圖11 叔結點U爲黑色 新結點N爲左孩子

[注意:藍色表示顏色可能爲紅,也可能爲黑]

  情況6):叔結點U爲黑色,新結點N爲父結點P的右孩子

    前提條件:新結點N和父結點P都爲紅色,父結點P爲G的右孩子

    情況描述:叔結點U爲黑色,新結點N爲右孩子時

    處理過程:[代碼:參考rb_insert_fixup()中的case 6]

        ①、顏色調整:將祖父結點G改爲紅色,父結點P改爲黑色

        ②、向左旋轉90度:父結點P取代祖父結點G的位置,同時將父結點的左子樹PL作爲祖父結點G的右子樹,祖父結點G作爲父結點P的左孩子

        ③、把祖父結點的父結點GP改爲下一次判斷的對象

 

圖12 叔結點G爲黑色 新結點N爲右孩子

[注意:藍色表示顏色可能爲紅,也可能爲黑]

/******************************************************************************
 **函數名稱: rbt_creat_node
 **功    能: 創建關鍵字爲key的結點(內部接口)
 **輸入參數: 
 **     key: 紅黑樹
 **     color: 結點顏色
 **     type: 新結點是父結點的左孩子還是右孩子
 **     parent: 父結點
 **輸出參數: NONE
 **返    回: RB_SUCCESS:成功 RB_FAILED:失敗
 **實現描述: 
 **注意事項: 新結點的左右孩子肯定是葉子結點
 **作    者: # Qifeng.zou # 2013.12.23 #
 ******************************************************************************/
rbt_node_t *rbt_creat_node(rbt_tree_t *tree, int key, int color, int type, rbt_node_t *parent)
{
    rbt_node_t *node = NULL;

    node = (rbt_node_t *)calloc(1, sizeof(rbt_node_t));
    if (NULL == node) {
        return NULL;
    }

    node->color = color;
    node->key = key;
    node->lchild = tree->sentinel;
    node->rchild = tree->sentinel;
    if (NULL != parent) {
        rbt_set_child(tree, parent, type, node);
    }
    else {
        node->parent = tree->sentinel;
    }

    return node;
}

代碼9 創建key值結點

/******************************************************************************
 **函數名稱: rbt_insert
 **功    能: 向紅黑樹中增加節點(對外接口)
 **輸入參數: 
 **     tree: 紅黑樹
 **     key: 需被添加的關鍵字
 **輸出參數: NONE
 **返    回: RBT_SUCCESS:成功 RBT_FAILED:失敗 RBT_NODE_EXIST:節點存在
 **實現描述: 
 **     1. 當根節點爲空時,直接添加
 **     2. 將節點插入樹中, 檢查並修復新節點造成紅黑樹性質的破壞
 **注意事項: 
 **  紅黑樹的5點性質:
 **     1、每個結點要麼是紅色的,要麼是黑色的;
 **     2、根結點是黑色的;
 **     3、所有葉子結點(NIL)都是黑色的;
 **     4、如果一個結點是紅色,則它的兩個兒子都是黑色的;
 **     5、對任何一個結點,從該結點通過其子孫結點到達葉子結點(NIL)
 **         的所有路徑上包含相同數目的黑結點。
 **注意事項: 插入節點操作只可能破壞性質(4)
 **作    者: # Qifeng.zou # 2013.12.23 #
 ******************************************************************************/
int rbt_insert(rbt_tree_t *tree, int key)
{
    rbt_node_t *node = tree->root,
              *add = NULL, *parent = NULL;

    /* 1. 當根節點爲空時,直接添加 */
    if (tree->sentinel == tree->root) {
        /* 性質2: 根結點是黑色的 */
        tree->root = rbt_creat_node(tree, key, RBT_COLOR_BLACK, 0, NULL);
        if (tree->sentinel == tree->root) {
            return RBT_FAILED;
        }
        return RBT_SUCCESS;
    }
    
    /* 2. 將節點插入樹中, 檢查並修復新節點造成紅黑樹性質的破壞 */
    while (tree->sentinel != node) {
        if (key == node->key) {
            return RBT_NODE_EXIST;
        }
        else if (key < node->key) {
            if (tree->sentinel == node->lchild) {
                add = rbt_creat_node(tree, key, RBT_COLOR_RED, RBT_LCHILD, node);
                if(NULL == add) {
                    return RBT_FAILED;
                }
                
                return rb_insert_fixup(tree, add); /* 防止紅黑樹的性質被破壞 */
            }
            node = node->lchild;
        }
        else {
            if (tree->sentinel == node->rchild) {
                add = rbt_creat_node(tree, key, RBT_COLOR_RED, RBT_RCHILD, node);
                if (NULL == add) {
                    return RBT_FAILED;
                }

                return rb_insert_fixup(tree, add); /* 防止紅黑樹的性質被破壞 */
            }
            node = node->rchild;
        }
    }

    return RBT_SUCCESS;
}

代碼10 增加key值結點(內部接口)

/******************************************************************************
 **函數名稱: rb_insert_fixup
 **功    能: 插入操作修復(內部接口)
 **輸入參數: 
 **     tree: 紅黑樹
 **     node: 新增節點的地址
 **輸出參數: NONE
 **返    回: RBT_SUCCESS:成功 RBT_FAILED:失敗
 **實現描述: 
 **     1. 檢查紅黑樹性質是否被破壞
 **     2. 如果被破壞,則進行對應的處理
 **注意事項: 插入節點操作只可能破壞性質④
 **作    者: # Qifeng.zou # 2013.12.23 #
 ******************************************************************************/
int rb_insert_fixup(rbt_tree_t *tree, rbt_node_t *node)
{
    rbt_node_t *parent = NULL, *uncle = NULL, *grandpa = NULL, *gparent = NULL;

    while (rbt_is_red(node)) {
        parent = node->parent;
        if (rbt_is_black(parent)) {
            return RBT_SUCCESS;
        }
        
        grandpa = parent->parent;
        if (parent == grandpa->lchild) { /* 父節點爲左節點 */
            uncle = grandpa->rchild;
            
            /* case 1: 父節點和叔節點爲紅色 */
            if (rbt_is_red(uncle)) {
                rbt_set_black(parent);
                rbt_set_black(uncle);
                if(grandpa != tree->root) {
                    rbt_set_red(grandpa);
                }
                node = grandpa;
                continue;
            }
            /* case 2: 叔結點爲黑色,結點爲左孩子 */
            else if (node == parent->lchild) {
                /* 右旋轉: 以grandpa爲支點 */
                gparent = grandpa->parent;
                rbt_set_red(grandpa);
                rbt_set_black(parent);

                rbt_right_rotate(tree, grandpa);
                node = gparent;
                continue;
            }
            /* case 3: 叔結點爲黑色,結點爲右孩子 */
            else {
                /* 左旋轉: 以parent爲支點 */
                rbt_left_rotate(tree, parent);
                
                node = parent;
                continue;
            }
        }
        else {                       /* 父節點爲右孩子 */
            uncle = grandpa->lchild;
            
            /* case 1: 父節點和叔節點爲紅色 */
            if (rbt_is_red(uncle)) {
                rbt_set_black(parent);
                rbt_set_black(uncle);
                if (grandpa != tree->root) {
                    rbt_set_red(grandpa);
                }

                node = grandpa;
                continue;
            }
            /* case 2: 叔結點爲黑色,結點爲左孩子 */
            else if (node == parent->lchild) {
                /* 右旋轉: 以parent爲支點 */
                rbt_right_rotate(tree, parent);
                node = parent;
                continue;
            }
            /* case 3: 叔結點爲黑色,結點爲右孩子 */
            else {
                /* 左旋轉: 以grandpa爲支點 */
                gparent = grandpa->parent;
                rbt_set_black(parent);
                rbt_set_red(grandpa);

                rbt_left_rotate(tree, grandpa);
                node = gparent;
                continue;
            }
        }
    }

    return RBT_SUCCESS;
}

代碼11 紅黑樹修復

3.4 結果展示

    調用函數rb_insert,隨機添加20個不同的關鍵字,其最終生成的紅黑樹如下圖所示: [注意:紅黑樹的打印可以參考《算法導論 之 紅黑樹 - 打印》]

圖13 打印構建

圖11對應的紅黑樹結構如下圖所示:

圖14 樹型結構

[注:未繪製葉子結點]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章