紅黑樹

前言

本篇文章我將向大家介紹最負盛名的自平衡二叉查找樹——紅黑樹。

Robert Sedgewick 與其著作《算法》

紅黑樹由魯?shù)婪?貝爾——一位慕尼黑科技大學信息技術教授發(fā)明。1978 年,在羅伯特.塞奇威克發(fā)表的一篇論文中為這種數(shù)據(jù)結(jié)構正式命名為紅黑樹,并提供了完整的復雜度證明。

紅黑樹雖然繁瑣,但是它在實踐中有著非常高的效率,因為紅黑樹是自平衡的(紅黑樹實現(xiàn)的其實是一種偽平衡,在后文中我們會提到),所以它不會出現(xiàn)像二分搜索樹在極端的情況下退化為鏈表的這種情況,可以在 O(logN) 的時間做元素的查找,插入以及刪除操作。因為其優(yōu)秀的性能,在一些底層數(shù)據(jù)結(jié)構設計中被廣泛使用——譬如 Java 語言中的 TreeMap 與 TreeSet 就是紅黑樹。

接下來,我們就一起探索,紅黑樹究竟是怎樣的一種數(shù)據(jù)結(jié)構。

紅黑樹與 2-3 樹

在介紹大名鼎鼎的紅黑樹之前,我們先來看一下 2-3 樹這種數(shù)據(jù)結(jié)構。提前了解 2-3 樹對我們接下來學習紅黑樹有很大的幫助,因為紅黑樹與 2-3 樹的原理是相通的,并且 2-3 樹要比復雜的紅黑樹更容易理解。

什么是 2-3 樹呢?首先,2-3 樹是一種絕對平衡的多叉樹。

我們都知道平衡二叉樹的概念——平衡二叉樹是一棵二分搜索樹。它或者是一顆空樹,或它的左子樹與右子樹的高度之差(左子樹的高度 - 右子樹的高度,其別名叫平衡因子:balance factor)的絕對值不超過 1,且一棵平衡二叉樹的左右子樹均是一棵平衡二叉樹。

如下圖所示,該樹就是一棵平衡二叉樹:

image

而所謂的絕對平衡則比平衡二叉樹里面的平衡性要更加嚴格,絕對平衡的定義是:對于樹中任意一個節(jié)點,左右子樹高度相同。

image

如上圖所示,該示意圖表示一棵 2-3 樹。這棵樹滿足了絕對平衡的定義。

除此之外, 2-3 樹并不是一棵標準的二叉樹,而是一棵多叉樹。在上圖的 2-3 樹中,我們可以看到,有的節(jié)點存放了一個元素,有的節(jié)點存放了兩個元素。存放了一個元素的節(jié)點我們稱之為 2-節(jié)點,而存放了兩個元素的節(jié)點我們則稱之為 3-節(jié)點,2-3 樹中只具有這兩種類型的節(jié)點,這也是 2-3 樹其名字的由來。

接下來,我將演示向 2-3 樹插入元素的操作,讓我們一起來看一下 2-3 樹究竟是通過什么神奇的操作來維持整棵樹是絕對平衡的。

首先,我們向一棵空的 2-3 樹中插入一個節(jié)點 “42”,然后再插入節(jié)點 “37”:

image

向 2-3 樹插入節(jié)點的原則與二分搜索樹是一樣的,不過它還會遵循另外一個原則,那就是:新的節(jié)點永遠不會插入到一個空的位置上

如果這棵樹是二分搜索樹,那么節(jié)點 “37” 將會插入到節(jié)點 “42” 的左孩子的位置上,不過這樣一來,就破壞了 2-3 樹的絕對平衡性。所以,節(jié)點 “37” 并不會插入到一個空的位置,而是與節(jié)點 “42” 融合成為一個新的 3-節(jié)點:

image

接下來,我們向 2-3 樹中插入元素 “12”:

image

依據(jù)我們的原則“新的節(jié)點永遠不會插入到一個空的位置上”,節(jié)點 “12” 將會和當前的 3-節(jié)點融合成為一個 “4-節(jié)點”:

image

然后,這個 4-節(jié)點會分裂成 3 個 2-節(jié)點,來維持絕對平衡的狀態(tài)并保證遵循 2-3 樹的定義:

image

我們繼續(xù)向當前的 2-3 樹中插入元素 ”18“:

image
image

向當前的 2-3 樹中插入元素 ”6“:

image
image
image
image

向當前的 2-3 樹中插入元素 ”11“:

image
image

向當前的 2-3 樹中插入元素 ”5“:

image
image
image
image
image

通過上面幾張模擬圖的演示,想必大家已經(jīng)能夠初步地掌握 2-3 樹的特點以及在向 2-3 樹添加元素時,2-3 樹如何維護絕對平衡。

我們之前說過,紅黑樹和 2-3 樹的本質(zhì)是相同的,如果大家理解了 2-3 樹,那么接下來我要介紹的紅黑樹也就不難理解了。2-3 樹中有兩種節(jié)點:2-節(jié)點與 3-節(jié)點,2-節(jié)點存儲一個元素,3-節(jié)點存儲兩個元素。而紅黑樹則是一種“二叉樹形式的 2-3 樹”,它使用單個黑色的節(jié)點來表示 2-節(jié)點,使用 “紅-黑” 這兩個節(jié)點表示一個三節(jié)點:

image

這樣一來,紅黑樹中所有的紅色節(jié)點必然為左孩子節(jié)點,所以我們也將這種紅黑樹稱為“左傾紅黑樹”(Left-Leaning Red-Black Tree)。

譬如這樣的一棵 2-3 樹:


image

轉(zhuǎn)換為紅黑樹之后就是這個樣子:

image

這也就是為什么我們會說紅黑樹與 2-3 樹是等價的,對于任意的一棵 2-3 樹都可以通過這種方式轉(zhuǎn)換為一棵紅黑樹。

紅黑樹的實現(xiàn)

紅黑樹的基本性質(zhì)

image

紅黑樹具有以下五條基本性質(zhì):

1. 每個節(jié)點或者為紅色,或者為黑色

這一點沒什么可說的,畢竟叫做紅黑樹嘛,節(jié)點非紅即黑。

2. 根節(jié)點的顏色一定是黑色

我們可以類比考慮一下 2-3 樹,2-3 樹的節(jié)點不是 2-節(jié)點就是 3-節(jié)點,而無論是哪一種節(jié)點作為紅黑樹的根節(jié)點,都會使得根節(jié)點的顏色必然是黑色的。

3. 每一個葉子節(jié)點(最后的空節(jié)點)一定是黑色的

如果紅黑樹為空,那么根節(jié)點也為空,這一點和第二點可以相互對應起來,因為根節(jié)點的顏色必然是黑色的。

4. 如果一個節(jié)點是紅色的,那么它的孩子節(jié)點都是黑色的。

這一點大家同樣可以類比下 2-3 樹,紅黑樹中紅色節(jié)點的左右節(jié)點不是 2-節(jié)點就是 3-節(jié)點,無論是哪一種節(jié)點,與紅色節(jié)點相連的必然都是黑色節(jié)點。

5. 從任意一個節(jié)點到葉子節(jié)點,經(jīng)過的黑色節(jié)點的數(shù)量一樣多。

image

上面這棵 2-3 樹轉(zhuǎn)換為紅黑樹之后,是這樣的:

image

從這張圖里,我們可以清晰地看出,從任意一個節(jié)點到葉子節(jié)點,經(jīng)過黑色節(jié)點的數(shù)量是一樣多的。

這也就是我們所說的——紅黑樹是一棵保持“黑平衡”的二叉樹(黑色節(jié)點是絕對平衡的),而嚴格意義上來講紅黑樹并不是一棵平衡二叉樹,但是因為紅黑樹具有黑平衡的特性,所以可以認為紅黑樹保持的是一種近似的平衡。

從這里我們也可以分析出向紅黑樹添加,查找,刪除元素的時間復雜度。在最差的情況下,從根節(jié)點到葉子節(jié)點的路徑上,紅色節(jié)點與黑色節(jié)點交替出現(xiàn),其復雜度為 O(2logN),忽略掉常數(shù)項系數(shù),我們可以得到紅黑樹的增刪改查的時間復雜度為 O(logN)。

向紅黑樹中添加元素的代碼實現(xiàn)

接下來我們來看一下如何向紅黑樹中添加元素。

紅黑樹中節(jié)點的定義與實現(xiàn)并不難。因為紅黑樹本身是一棵二分搜索樹,所以節(jié)點可以存儲元素值,并且內(nèi)部有指向左,右孩子的指針,這些和二分搜索樹中節(jié)點的實現(xiàn)是一樣的。不過,紅黑樹的節(jié)點有顏色之分,且顏色非紅即黑,我們就額外使用一個 boolean 變量表示節(jié)點的顏色:

public class Node<E> {

    public E e;
    public Node left;
    public Node right;
    public boolean color;


    public Node(E e) {
        this.e = e;
        left = null;
        right = null;
        color = true; // true 表示紅色,false 表示黑色
    }
}

可以看到,我們設定初始化一個節(jié)點的顏色為紅色。其實不難理解,在介紹 2-3 樹時我們就提到了,向 2-3 樹中添加節(jié)點的原則是新的節(jié)點永遠不會插入到一個空的位置上。而我們在添加節(jié)點時,首先要將待添加的節(jié)點與其他節(jié)點進行融合,如果初始化節(jié)點的顏色為紅色,那么就表示新添加的節(jié)點與其他節(jié)點進行融合這一步驟。

在捋清向紅黑樹添加一個節(jié)點的邏輯之前,我們首先回顧一下向二分搜索樹中插入一個節(jié)點的邏輯:

如果當前二分搜索樹的根節(jié)點為空,那么新插入的節(jié)點就會成為根節(jié)點。

如果當前二分搜索樹的根節(jié)點不為空,就讓根作為當前比較的節(jié)點:新插入的節(jié)點與當前節(jié)點進行比較;如果值比當前節(jié)點小就要“向左走”,如果值比當前節(jié)點大就要“向右走”,然后讓下一層的節(jié)點繼續(xù)作為當前比較的節(jié)點,直至走到應該插入的位置。

下圖為在向當前的二分搜索樹中添加節(jié)點“28”的流程:

image

Java 代碼:

/**
 * @param e 向二分搜索樹中添加新的元素
 */
public void add(E e) {
    root = add(e, root);
}

/**
 * @param e    向二分搜索樹中新插入的節(jié)點
 * @param node 當前比較的節(jié)點
 * @return 返回二分搜索樹的根節(jié)點
 */
private Node add(E e, Node node) {
    if (node == null) {
        size++;
        return new Node(e);
    }
    if (e.compareTo((E) node.e) < 0) {
        node.left = add(e, node.left);
    } else if (e.compareTo((E) node.e) > 0) {
        node.right = add(e, node.right);
    }
    return node;
}

我們要滿足紅黑樹的根節(jié)點始終為黑色,所以每次插入完一個節(jié)點,我們都手動將根節(jié)點置為黑色:

public void add(E e) {
    root = add(e, root);
    root.color = BLACK; // false
}

基于向二分搜索樹中插入一個節(jié)點的邏輯,我們向紅黑樹中新插入一個節(jié)點有以下幾種情況:

  • 當前插入的節(jié)點與 2-節(jié)點進行融合
  • 當前插入的節(jié)點與 3-節(jié)點進行融合

如果我們插入到一個 2-節(jié)點上,則具有以下兩種情況:

第一種情況為,插入的節(jié)點比當前的 2-節(jié)點的元素值小。

image

如果是這種情況,我們直接將新的節(jié)點與 2-節(jié)點融合為一個 3-節(jié)點即可:

image

第二種情況為,插入的節(jié)點比當前的 2-節(jié)點的元素值大。

image

將元素 “42” 插入到 “37” 的右孩子的位置上:

image

此時,當前并不滿足左傾紅黑樹的定義,我們需要以節(jié)點 ”37“ 進行一次 “左旋轉(zhuǎn)” 操作(左旋轉(zhuǎn)為逆時針,右旋轉(zhuǎn)為順時針),并將顏色重置:

image

為了不失一般性,我在節(jié)點 “37” 與節(jié)點 “42” 的孩子節(jié)點上分別添加了對應的子樹,通過上圖中偽代碼的邏輯,我們看到“左旋轉(zhuǎn)”后的樹既保持了紅黑樹的特性,也保持了二分搜索樹的特性。

左旋轉(zhuǎn)操作的 Java 代碼如下:

/**
 * 左旋轉(zhuǎn):
 *
 *        node                      x
 *       /   \                    /  \
 *     T1     x    =========>   node T3
 *          /  \               /   \
 *         T2  T3             T1   T2
 *
 */
private Node leftRotate(Node node){
    Node x = node.right;

    // 左旋轉(zhuǎn)
    node.right = x.left;
    x.left = node;

    x.color = node.color;
    node.color = RED;

    return x;
}

在討論了新插入的節(jié)點與 2-節(jié)點進行融合的所有情況后,我們來看一下新插入的節(jié)點與 3-節(jié)點進行融合的情況。

如果我們插入到一個 3-節(jié)點上,則具有以下幾種情況:

第一種情況為,新的節(jié)點比 3-節(jié)點上的黑色節(jié)點的值還要大:

image

插入節(jié)點 “66” 后:

image

這一步操作相當于我們新插入的節(jié)點與 3-節(jié)點融合成為了一個 4-節(jié)點,對應到 2-3 樹上,我們的操作是這樣的:

image

接下來,我們需要將當前的 4-節(jié)點變成 3 個 2-節(jié)點,并讓 “42” 繼續(xù)向上融合:

image

所以,對應地,在紅黑樹中,我們應該將節(jié)點 “37” 與 “66” 變?yōu)楹谏?2-節(jié)點,將節(jié)點 “42” 置為繼續(xù)與上一個節(jié)點融合的紅色節(jié)點:

image

這一步驟的操作叫做 flipColors,顧名思義就是將這三個節(jié)點的顏色全部進行翻轉(zhuǎn),其代碼對應如下:

// flipColors
private void flipColors(Node node){
    node.color = RED;
    node.left.color = BLACK;
    node.right.color = BLACK;
}

第二種情況為,新的節(jié)點比 3-節(jié)點上的紅色節(jié)點的值還要?。?/p>

image

節(jié)點 “12” 插入后:

image

此時,我們需要對節(jié)點 “42” 進行一次右旋轉(zhuǎn):

image

再進行一次 flipColors:

image

右旋轉(zhuǎn)的 Java 代碼如下:

/**
 * 右旋轉(zhuǎn):
 *
 *        node                       x
 *       /   \                     /  \
 *     x      T2    ===========>  y   node
 *   /  \                            /   \
 *  y   T1                          T1   T2
 *
 */
private Node rightRotate(Node node) {
    Node x = node.left;

    // 右旋轉(zhuǎn)
    node.left = x.right;
    x.right = node;

    x.color = node.color;
    node.color = RED;
    return x;
}

第三種情況為,新的節(jié)點比 3-節(jié)點上的紅色節(jié)點的值要大,但是比黑色節(jié)點的值要?。?/p>

image

節(jié)點 “40” 插入后:

image

此時,我們首先對節(jié)點 “37” 進行一次左旋轉(zhuǎn),然后對 "42" 節(jié)點進行一次右旋轉(zhuǎn),最后再執(zhí)行一次 flipColors 操作即可:

image

總的來看,向紅黑樹中添加元素的操作有如下的邏輯:

image

向紅黑樹中添加節(jié)點的完整 Java 代碼如下:

public class RBTree<E extends Comparable<E>> {

    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private Node root;
    private int size;

    public RBTree() {
        root = null;
        size = 0;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 判斷節(jié)點 node 的顏色
     *
     * @param node
     * @return
     */
    public boolean isRed(Node node) {
        if (node == null)
            return false;
        return node.color;
    }

    /**
     * @return 返回紅黑樹的根節(jié)點
     */
    public Node getRoot() {
        return root;
    }

    /**
     * 左旋轉(zhuǎn):
     *
     *        node                      x
     *       /   \                    /  \
     *     T1     x    ===========> node T3
     *          /  \               /   \
     *         T2  T3             T1   T2
     *
     */
    private Node leftRotate(Node node){
        Node x = node.right;

        // 左旋轉(zhuǎn)
        node.right = x.left;
        x.left = node;

        x.color = node.color;
        node.color = RED;

        return x;
    }

    /**
     * 右旋轉(zhuǎn):
     *
     *        node                       x
     *       /   \                     /  \
     *     x      T2    ===========>  y   node
     *   /  \                            /   \
     *  y   T1                          T1   T2
     *
     */
    private Node rightRotate(Node node) {
        Node x = node.left;

        // 右旋轉(zhuǎn)
        node.left = x.right;
        x.right = node;

        x.color = node.color;
        node.color = RED;
        return x;
    }

    // 顏色翻轉(zhuǎn)
    private void flipColors(Node node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    /**
     * @param e 向紅黑樹中添加新的元素
     */
    public void add(E e) {
        root = add(e, root);
        root.color = BLACK;
    }

    /**
     * @param e    向紅黑樹中新插入的節(jié)點
     * @param node 當前比較的節(jié)點
     * @return 返回紅黑樹的根節(jié)點
     */
    private Node add(E e, Node node) {
        if (node == null) {
            size++;
            return new Node(e); // 默認插入的是紅色節(jié)點
        }
        if (e.compareTo((E) node.e) < 0) {
            node.left = add(e, node.left);
        } else if (e.compareTo((E) node.e) > 0) {
            node.right = add(e, node.right);
        }

        if(isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);
        if(isRed(node.left) && isRed(node.left.left))
            rightRotate(node);
        if(isRed(node.left) && isRed(node.right))
            flipColors(node);
        return node;
    }
}

那么至此,向紅黑樹中插入一個節(jié)點的操作就已經(jīng)介紹完畢了。

以上內(nèi)容就是我對紅黑樹的學習總結(jié)。關于向紅黑樹中刪除節(jié)點的操作,在本篇文章中將不會涉及。

總結(jié)

今天我主要向大家分享了紅黑樹這種數(shù)據(jù)結(jié)構。

我們在介紹紅黑樹之前首先介紹了 2-3 樹。如果理解了 2-3 樹,那么理解紅黑樹其實并不難。

然后我們介紹了紅黑樹的五大基本特性:

  1. 每個節(jié)點或者為紅色,或者為黑色
  2. 根節(jié)點的顏色一定是黑色
  3. 每一個葉子節(jié)點(最后的空節(jié)點)一定是黑色的
  4. 如果一個節(jié)點是紅色的,那么它的孩子節(jié)點都是黑色的
  5. 從任意一個節(jié)點到葉子節(jié)點,經(jīng)過的黑色節(jié)點的數(shù)量一樣多

并且從 2-3 樹的原理上對紅黑樹這 5 條基本特性進行了分析。

最后,我們介紹了如何向紅黑樹中添加一個節(jié)點及它的代碼實現(xiàn)。

好啦,至此為止,這篇文章就到這里了~歡迎大家關注我的公眾號,在這里希望你可以收獲更多的知識,我們下一期再見!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容