演算法導論 之 紅黑樹 - 插入 C語言
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
- 作者:鄒祁峰
- 郵箱:[email protected]
- 部落格:http://blog.csdn.net/qifengzou
- 日期:2013.12.24 21:00
- 轉載請註明來自"
1 引言
在之前的博文中,本人對平衡二叉樹的處理做了較詳盡的分析,有興趣的朋友可以參閱博文《演算法導論 之 平衡二叉樹 - 建立 插入 搜尋 銷燬》和《演算法導論 之 平衡二叉樹 - 刪除》。平衡二叉樹AVL是嚴格的平衡樹,在增刪結點時,其旋轉操作的次數較多;而紅黑樹RBT則通過非嚴格的平衡來換取增刪結點時旋轉次數的降低。在應用中,如果搜尋次數大於增刪次數,則選擇平衡二叉樹更好一些;而如果搜尋與增刪次數接近時,紅黑樹則是更好的選擇。在有些資料中顯示,紅黑樹的統計效能要優於平衡二叉樹。2 性質分析
紅黑樹是一種二叉查詢樹,但在每個節點上增加一個儲存位表示結點的顏色[RED或BLACK]。通過對任一結點到葉子結點的路徑上各個結點著色方式的限制,確保沒有一條路徑會是其他路徑的2倍,因而是接近平衡的!因此,它能保證在最壞情況下,基本的動態集合操作的時間複雜度為O([email protected])[[email protected]:以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([email protected])。
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為左孩子
前提條件:新結點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 樹型結構 [注:未繪製葉子結點]