算法筆記:紅黑樹

紅黑樹,一種平衡二叉樹,最爲著名的應用就是C++ STL中的map,是有序集合最爲理想的存儲方式之一。除了二叉樹所具備的屬性以後,紅黑樹中每一個節點多了一個「顏色」屬性,能夠是紅色或者是黑色。一棵紅黑樹應該知足一下的性質:c++

  1. 每一個節點是紅色或者黑色的;
  2. 根節點是黑色的;
  3. 每一個葉節點nil是黑色的(使用哨兵節點在刪除調整時能夠方便很多);
  4. 若是一個節點是紅色的,那麼它的兩個子節點是黑色的;
  5. 對於每個節點,到後代全部葉節點所通過的黑色節點數目相同。

根據定義,能夠寫出紅黑樹節點的數據結構:git

struct Node {
    enum Color { RED, BLACK };
    Color _color;
    Key _key;
    Value _value;
    Node *_parent, *_left, *_right;
    Node(): _color(BLACK) {}
    Node(const Key &key, const Value &value, Node *left, Node *right, Node *parent):
        _key(key), _value(value), _color(RED), _left(left), _right(right), _parent(parent) {}
};
複製代碼

本文省略了拷貝、析構、賦值等操做,完整源代碼放在Gist上。github

旋轉

想要紅黑樹維持着平衡,就須要在插入元素和刪除元素的過程當中不斷對結構進行調整。其中,最基礎的操做就是左旋右旋數據結構

以左旋爲例,操做能夠分爲三步:ui

  1. 將Q的左子節點變成P的右子節點;
  2. 將P變成Q的左子節點;
  3. 將Q變成當前子樹的根節點。
void leftRotate(Node *x) {
    Node *y = x->_right;
    // remove y->left to x->right
    x->_right = y->_left;
    if (x->_right != nil)
        x->_right->_parent = x;
    // remove y up
    y->_parent = x->_parent;
    if (x->_parent == nil)
        root = y;
    else if (x->_parent->_left == x)
        x->_parent->_left = y;
    else
        x->_parent->_right = y;
    // remove x down
    x->_parent = y;
    y->_left = x;
}
複製代碼

插入

先污染,後治理。首選按照二叉樹的插入方法先將節點插入二叉樹,而後再對紅黑樹進行調整。spa

void insert(Node *nptr) {
    Node *it = root, *p = root;
    // find insert position
    while (it != nil) {
        p = it;
        if (nptr->_key < it->_key)
            it = it->_left;
        else if (nptr->_key > it->_key)
            it = it->_right;
        else {
            // find target key-value
            it->_value = nptr->_value;
            return;
        }
    }
    // insert
    nptr->_parent = p;
    if (p == nil)
        root = nptr;
    else if (nptr->_key < p->_key)
        p->_left = nptr;
    else
        p->_right = nptr;
    // fixup
    insertFixup(nptr);
}
複製代碼

在完成插入以後,將面臨六種狀況,因爲存在左右對稱的狀況,實際上只須要考慮三種狀況。3d

狀況1時,調整目標節點B的父節點和叔節點都是紅節點,祖父節點爲黑節點,咱們須要將父節點和叔節點的顏色改爲黑色,祖父節點設爲紅節點,並將調整目標設爲祖父節點。code

狀況2狀況3中,新插入節點的父節點爲紅節點,祖父節點爲黑節點,而且叔節點爲黑節點。首選須要把狀況2轉換成狀況3,讓B成爲黑節點,A和C爲B的紅色子節點,並將調整目標設爲A。每每在處理完這兩種狀況後,紅黑樹完成了調整。cdn

因爲NIL也算是黑色節點,因此還須要定義一個獲取節點顏色的宏。blog

調整過程是自下而上的,一個插入的新節點的顏色是紅色的,每次調整的目標是使每一個子樹保持紅黑樹的性質。對於狀況2和3來講,調整結束後子樹樹根爲黑色,對總體的性質不會形成影響。而在狀況1中,子樹樹根爲紅色,對總體的性質形成了影響,須要繼續調整,這就是循環條件的意義

void insertFixup(Node *ptr) {
    while (ptr->_parent->_color == Node::RED) {
        if (ptr->_parent == ptr->_parent->_parent->_left) {
            Node *y = ptr->_parent->_parent->_right;
            // case 1
            if (y->_color == Node::RED) {
                ptr->_parent->_color = Node::BLACK;
                y->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                ptr = ptr->_parent->_parent;
            } else {
                // case 2: switch case 2 to case 3
                if (ptr == ptr->_parent->_right) {
                    ptr = ptr->_parent;
                    leftRotate(ptr);
                }
                // case 3
                ptr->_parent->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                rightRotate(ptr->_parent->_parent);
            }
        } else {
            // with 'left' and 'right' exchanged
            Node *y = ptr->_parent->_parent->_left;
            if (y->_color == Node::RED) {
                ptr->_parent->_color = Node::BLACK;
                y->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                ptr = ptr->_parent->_parent;
            } else {
                if (ptr == ptr->_parent->_left) {
                    ptr = ptr->_parent;
                    rightRotate(ptr);
                }
                ptr->_parent->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                leftRotate(ptr->_parent->_parent);
            }
        }
    }
    root->_color = Node::BLACK;
}
複製代碼

刪除

刪除的過程比插入過程複雜不少。與二叉樹同樣,咱們須要一個替換操做,將子樹u替換成子樹v。

void transplant(shared_ptr<Node> u, shared_ptr<Node> v) {
    if (u->_parent == nil)
        root = v;
    else if (u == u->_parent->_left)
        u->_parent->_left = v;
    else
        u->_parent->_right = v;
    v->_parent = u->_parent;
}
複製代碼

刪除的過程和二叉樹相似,多了一些處理哨兵、記錄顏色的過程。

void remove(Node *ptr) {
        Node *y = ptr, *x;
        int y_original_color = y->_color;
        if (y->_left == nil) {
            x = ptr->_right;
            transplant(ptr, ptr->_right);
        } else if (y->_right == nil) {
            x = ptr->_left;
            transplant(ptr, ptr->_left);
        } else {
            y = min(ptr->_right);
            y_original_color = y->_color;
            x = y->_right;
            if (y->_parent == ptr)
                x->_parent = y; // change nil->_parent
            else {
                transplant(y, y->_right);
                y->_right = ptr->_right;
                y->_right->_parent = y;
            }
            transplant(ptr, y);
            y->_left = ptr->_left;
            y->_left->_parent = y;
            y->_color = ptr->_color;
        }
        if (y_original_color == Node::BLACK)
            deleteFixup(x);
    }
複製代碼

首先考慮一下刪除節點的時候,咱們對紅黑樹形成了什麼樣的影響。**若是刪除一個紅色節點,那麼紅黑樹的性質並不會收到任何影響;若是刪除的是一個黑色節點,那麼意味着黑高相等的性質將不復存在,刪除一個黑色節點。**那麼和插入調整的思路相似,每次調整的目標是保持子樹內的性質。

調整過程須要分左右對稱的四種狀況:

  • 狀況1:有一個紅色的兄弟節點,經過旋轉和顏色調換,使紅色節點到調整目標節點一側來,變成狀況二、三、4中的一種;
  • 狀況2:兄弟節點的兩個子節點全爲黑,因而把兄弟節點設爲紅節點,這樣一來,子樹中的黑高是相等了,可是刪除的黑節點並無被彌補,還須要繼續往上調整;
  • 狀況3:兄弟節點左紅右黑,這個時候須要把紅色節點調整到叔節點的右側,變成狀況4;
  • 狀況4:兄弟節點右節點爲紅,這個時候須要把紅節點調整到調整目標節點一側來,用這個紅色節點彌補刪除的黑色節點。調整結束後,子樹知足紅黑樹性質,結束調整過程。

而對於循環條件。應該怎麼理解呢?若是調整過程到了根節點,那麼就不存在某子樹內黑高缺一的狀況,能夠結束循環。若是遇到了紅節點,那麼把這個紅節點變成黑節點就能夠解決黑高不等的狀況。

void removeFixup(Node *ptr) {
    while (ptr != root && ptr->_color == Node::BLACK) {
        if (ptr == ptr->_parent->_left) {
            Node *w = ptr->_parent->_right;
            // case 1
            if (w->_color == Node::RED) {
                w->_color = Node::BLACK;
                ptr->_parent->_color = Node::RED;
                leftRotate(ptr->_parent);
                w = ptr->_parent->_right;
            }
            // case 2
            if (w->_left->_color == Node::BLACK && w->_right->_color == Node::BLACK) {
                w->_color = Node::RED;
                ptr = ptr->_parent;
            } else {
                // case 3
                if (w->_right->_color == Node::BLACK) {
                    w->_left->_color = Node::BLACK;
                    w->_color = Node::RED;
                    rightRotate(w);
                    w = ptr->_parent->_right;
                }
                // case 4
                w->_color = ptr->_parent->_color;
                ptr->_parent->_color = Node::BLACK;
                w->_right->_color = Node::BLACK;
                leftRotate(ptr->_parent);
                ptr = root;
            }
        } else {
            // with 'left' and 'right' exchanged
            Node *w = ptr->_parent->_left;
            if (w->_color == Node::RED) {
                w->_color = ptr->_parent->_color;
                ptr->_parent->_color = Node::RED;
                rightRotate(ptr->_parent);
                w = ptr->_parent->_left;
            }
            if (w->_left->_color == Node::BLACK && w->_right->_color == Node::BLACK) {
                w->_color = Node::RED;
                ptr = ptr->_parent;
            } else {
                if (w->_left->_color == Node::BLACK) {
                    w->_color = Node::RED;
                    w->_right->_color = Node::BLACK;
                    leftRotate(w);
                    w = ptr->_parent->_left;
                }
                w->_color = ptr->_parent->_color;
                w->_left->_color = Node::BLACK;
                ptr->_parent->_color = Node::BLACK;
                rightRotate(ptr->_parent);
                ptr = root;
            }
        }
    }
    ptr->_color = Node::BLACK;
}
複製代碼

查找

節點顏色對於查找過程沒有意義,紅黑樹查找過程和二叉樹是同樣的。

相關文章
相關標籤/搜索