資料結構演算法 - 紅黑樹
紅黑樹是一棵自平衡的二叉搜尋樹,因此在學習紅黑樹之前,我們需要回顧一下之前所學的知識 二叉搜尋樹和平衡二叉樹 。
1.二叉搜尋樹
二叉搜尋樹又叫二叉查詢樹或者二叉排序樹,它首先是一個二叉樹,而且必須滿足下面的條件:
1)若左子樹不空,則左子樹上所有結點的值均小於它的根節點的值;
2)若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值
3)左、右子樹也分別為二叉搜尋樹

二叉搜尋樹示例
2.平衡二叉樹
二叉搜尋樹解決了許多問題,比如可以快速的查詢最大值和最小值,可以快速找到排名第幾位的值,快速排序等等。但普通的二叉搜尋樹有可能出現極不平衡的情況(斜樹),這樣我們的時間複雜度就有可能退化成 O(N) 的情況。比如我們現在插入的資料是 [1,2,3,4,5,6,7] 轉換為二叉樹如下:

斜樹
由於普通的二叉搜尋樹會出現極不平衡的情況,那麼我們就必須得想想辦法了,這個時候平衡二叉樹就能幫到我們了。什麼是平衡二叉樹?平衡二叉搜尋樹(Self-balancing binary search tree)又被稱為AVL樹(有別於AVL演算法),且具有以下性質:它是一 棵空樹或 它的左右兩個子樹的高度差的絕對值不超過1 ,並且左右兩個子樹都是一棵平衡二叉樹。
平衡二叉樹有一個很重要的性質:左右兩個子樹的高度差的絕對值不超過1。那麼解決方案就是如果二叉樹的左右高度超過 1 ,我們就把當前樹調整為一棵平衡二叉樹。這就涉及到 左旋 、 右旋 、 先右旋再左旋 、 先左旋再右旋 。
2.1 右旋:

右旋.png
TreeNode<K, V> *R_Rotation(TreeNode<K, V> *pNode) { TreeNode<K, V> *left = pNode->left; TreeNode<K, V> *right = left->right; left->right = pNode; pNode->left = right; // 重新調整高度 pNode->height = max(getHeight(pNode->left), getHeight(pNode->right)) + 1; left->height = max(getHeight(left->left), getHeight(left->right)) + 1; return left; }
2.2 左旋:

左旋
TreeNode<K, V> *L_Rotation(TreeNode<K, V> *pNode) { TreeNode<K, V> *right = pNode->right; TreeNode<K, V> *left = right->left; right->left = pNode; pNode->right = left; // 重新調整高度 pNode->height = max(getHeight(pNode->left), getHeight(pNode->right)) + 1; right->height = max(getHeight(right->left), getHeight(right->right)) + 1; return right; }
2.3 先右旋再左旋:

先右旋再左旋
TreeNode<K, V> *R_L_Rotation(TreeNode<K, V> *pNode) { pNode->right = R_Rotation(pNode->right); return L_Rotation(pNode); }
2.4 先左旋再右旋:

先左旋再右旋
TreeNode<K, V> *L_R_Rotation(TreeNode<K, V> *pNode) { pNode->left = L_Rotation(pNode->left); return R_Rotation(pNode); }
2.紅黑樹
紅黑樹用法就比較廣了,比如 JDK 1.8 的 HashMap,TreeMap,C++ 中的 map 和 multimap 等等。紅黑樹學習起來還是有一點難度的,這時如果我們心中有 B 樹就有助於理解它,如果沒有 B 樹也沒有關係。
紅黑樹的特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 [注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

紅黑樹
假設我們現在要插入一個新的節點,如過插入的這個新的節點為黑色,那麼必然會違反性質(5),所以我們把新插入的點定義為紅色的。但是如果插入的新節點為紅色,就可以能會違反性質(4) ,因此我們需要對其進行調整,使得整棵樹依然滿足紅黑樹的性質,也就是雙紅修正。接下來我們只要分情況分析就可以了:
- 如果沒有出現雙紅現象,父親是黑色的不需要修正;
- 叔叔是紅色的 ,將叔叔和父親黑,然後爺爺染紅;
- 叔叔是黑色的,父親是爺爺的左節點,且當前節點是其父節點的右孩子,將“父節點”作為“新的當前節點”,以“新的當前節點”為支點進行左旋。然後將“父節點”設為“黑色”,將“祖父節點”設為“紅色”,以“祖父節點”為支點進行右旋;
- 叔叔是黑色的,父親是爺爺的左節點,且當前節點是其父節點的左孩子,將“父節點”設為“黑色”,將“祖父節點”設為“紅色”,以“祖父節點”為支點進行右旋;
- 叔叔是黑色的,父親是爺爺的右節點,且當前節點是其父節點的左孩子,將“父節點”作為“新的當前節點”,以“新的當前節點”為支點進行右旋。然後將“父節點”設為“黑色”,將“祖父節點”設為“紅色”,以“祖父節點”為支點進行左旋;
- 叔叔是黑色的,父親是爺爺的右節點,且當前節點是其父節點的右孩子,將“父節點”設為“黑色”,將“祖父節點”設為“紅色”,以“祖父節點”為支點進行左旋;
上面的雙紅修正現象看似比較負責,但實際上只有三種情況,一種是沒有雙紅現象,另一種是父親和叔叔都是紅色的,最後一種是叔叔是黑色的。我們來畫個例項看下:

template<class K, class V> void map<K, V>::solveDoubleRedTree(RB_Node *pNode) { RB_Node *node = pNode; // 不是根節點,父親是紅色 while (node->parent && node->parent->color == RB_COLOR_RED) { // 情況1 // 需要判斷的親戚 RB_Node *parent = node->parent; RB_Node *uncle = brother(parent); RB_Node *grandfather = parent->parent; // 叔叔是紅色 if (getColor(uncle)) { // 情況2 // 父親和叔叔染黑,爺爺染紅 grandfather->color = RB_COLOR_RED; parent->color = RB_COLOR_BLACK; uncle->color = RB_COLOR_BLACK; // 繼續遞迴 node = grandfather; } else { // 叔叔是黑色的,且父親是爺爺的左兒子 if (parent == grandfather->left) { // 自己是父親的左兒子 if (parent->right == node) {// 情況3 grandfather->color = RB_COLOR_RED; node->color = RB_COLOR_BLACK; node = parent; L_Rotation(parent); R_Rotation(grandfather); } else { // 情況4 grandfather->color = RB_COLOR_RED; parent->color = RB_COLOR_BLACK; R_Rotation(grandfather); } } else { // 自己是父親的右兒子 if (parent->left == node) {// 情況5 grandfather->color = RB_COLOR_RED; node->color = RB_COLOR_BLACK; node = parent; R_Rotation(parent); L_Rotation(grandfather); } else { // 情況6 grandfather->color = RB_COLOR_RED; parent->color = RB_COLOR_BLACK; L_Rotation(grandfather); } } } } // 根節點必須要是黑色的 root->color = RB_COLOR_BLACK; }