1. 程式人生 > >紅黑樹的簡單實現

紅黑樹的簡單實現

紅黑樹

  • LINDA
  • 2018/9/25

前言

如果你還是對寫紅黑樹毫無頭緒,可以看一下我的思路,從普通二叉搜尋樹的插入操作是如何一步步“進化”為真正的紅黑樹的插入操作的。

紅黑樹的四個規則:

  • (1) 每個結點要麼是紅的,要麼是黑的;
  • (2) 根結點必須為黑的;
  • (3) 若結點為紅,它的子節點為黑;
  • (4) 從根結點到 nil 結點之間的黑結點個數相同。

由於規則 (4) ,新插入的結點必須為紅。

結點的資料結構:

class rbtree {
public:
    enum {RED, BLACK};
    struct node {
        int val;     // 插入值
        int
color; // 顏色 node* left; // 左子結點 node* right; // 右子結點 node(int v, int c = RED):val(v), color(c), left(nullptr), right(nullptr) { } }; };

插入操作

我們從普通的二叉搜尋的插入操作一步步進化成真正的紅黑樹插入操作

(1) 我們先來看看普通的二叉搜尋樹是如何插入元素的:

class rbtree {
public:
	void insert(int val) {
    	root = insert
(root, val); } private: node* root; node* insert(node* root, int val) { if(root == nullptr) return new node(val); if(root->val < val) root->right = insert(root->right, val); if(root->val > val) root->left =
insert(root->left, val); return root; } };

(2) 由規則 (2) ,我們必須把 root 結點改為黑色:

class rbtree {
public:
    void insert(int val) {
        root = insert(root, val);
        root->color = BLACK; // 規則(2)
    }
//...
};

(3) 插入結點時,如果它的父節點為黑,就直接插入,如果父節點為紅,就需要做旋轉調整平衡

旋轉分兩種,一種是單旋,一種是雙旋,和AVL樹是一樣的,只是要互換旋轉結點的顏色。

單旋(右旋)如下:
單旋

P、G結點為需要旋轉的結點,旋轉後它們的顏色交換。這是為了保證規則 (3)、(4) 成立。

什麼時候會發生單旋呢?

對於右旋,插入結點X小於所在根結點G左子結點P的值,而且左子結點P它的左子結點X的顏色都為紅色

對於左旋,插入結點大於所在根結點右子結點的值,而且右子結點它的右子結點的顏色都為紅色

為什麼不直接判斷左子節點P為紅色就好了(因為新插入結點X一定為紅色)?

這是考慮到其他情況——當 X 不是新插入結點的時候。下面會說到。

雙旋 (先左再右) 如下:

雙旋

什麼時候發生雙旋呢?

對於先左再右,插入結點 X 的值大於所在根結點G左子結點P,且左子結點P它的右子結點X的顏色均為紅色

對於先右再左,插入結點 X 的值小於所在根結點右子結點,且右子結點它的左子結點的顏色均為紅色

同樣也考慮到其他情況 —— 當結點 X 不是新插入結點。下面會講到。

接著,我就要新增這部分的程式碼了:

class rbtree {
// ...
private:
    node* root;
    node* insert(node* root, int val) {
        if(root == nullptr)
            return new node(val);
        
        if(root->val < val) {
            root->right = insert(root->right, val);
        	if(root->right->color == RED
                 && root->right->val < val
                 && root->right->right->color == RED) {
                // 左旋
            }
            if(root->right->color == RED
                 && root->right->val > val
                 && root->right->left->color == RED) {
                // 先右再左
            }

        }
        if(root->val > val) {
            root->left  = insert(root->left, val);
            if(root->left->color == RED
                 && root->left->val > val
                 && root->left->left->color == RED) {
                 // 右旋
            }
            if(root->left->color == RED
                 && root->left->val < val
                 && root->left->right->color == RED) {
                 // 先左再右
            }
        }
        return root;
    }
};

(4) 細心的你可能已注意到上面兩種情況轉換後的所在根結點 P 為黑色,如果轉換後所在根結點 P 為紅色呢?

看下圖:

情況3

此時,如果P的父節點為黑色,那就沒關係,而如果父節點為紅色,就需要回溯到上層的根結點繼續調整了。回溯操作就會使程式碼很複雜,所以有一種自頂向下的方法來將這種情況轉為 (3) 中的兩種情況:沿著插入路徑,如果發現某個結點為黑色而它的兩個子結點均為紅色,則將該結點變為紅色,它的兩個子結點變為黑色:

自頂向下

此時,就可以發現結點P和結點X顏色都為紅,它需要做右旋操作,也就是上面堅持要檢查結點 P 的左子結點為紅色的原因

繼續新增到程式碼中:

class rbtree {
// ...
private:
    node* root;
    node* insert(node* root, int val) {
        if(root == nullptr)
            return new node(val);
        
        // 先沿路徑檢查
        if(root->color == BLACK 
                && root->left && root->left->color == RED 
                && root->right && root->right->color == RED) {
            root->color = RED;
            root->left->color  = BLACK;
            root->right->color = BLACK;
        }
        //... 
    }
};

這就完成了插入操作了。

程式碼位置

圖來自【STL原始碼剖析】