手撕紅黑樹

一、概述

紅黑樹是一種自平衡二叉查找樹,最早由一位名叫Rudolf Bayer的德國計算機(jī)科學(xué)家于1972年發(fā)明。然而,最初的樹形結(jié)構(gòu)不是現(xiàn)在的紅黑樹,而是一種稱為B樹的結(jié)構(gòu),它是一種多叉樹,可用于在磁盤上存儲大量數(shù)據(jù)。

在1980年代早期,計算機(jī)科學(xué)家Leonard Adleman和Daniel Sleator推廣了紅黑樹,并證明了它的自平衡性和高效性。從那時起,紅黑樹成為了最流行的自平衡二叉查找樹之一,并被廣泛應(yīng)用于許多領(lǐng)域,如編譯器、操作系統(tǒng)、數(shù)據(jù)庫等。

紅黑樹的名字來源于紅色節(jié)點(diǎn)和黑色節(jié)點(diǎn)的交替出現(xiàn),它們的顏色是用來維護(hù)樹的平衡性的關(guān)鍵。它們的顏色具有特殊的意義:

  • 黑色節(jié)點(diǎn)代表普通節(jié)點(diǎn),
  • 紅色節(jié)點(diǎn)代表一個新添加的節(jié)點(diǎn),

它們必須滿足一些特定的規(guī)則才能維持樹的平衡性。

紅黑樹也是一種自平衡的二叉搜索樹,較之 AVL,插入和刪除時旋轉(zhuǎn)次數(shù)更少

二、紅黑樹的特性

  • 每個節(jié)點(diǎn)要么是紅色,要么是黑色。
  • 根節(jié)點(diǎn)是黑色。
  • 所有葉子節(jié)點(diǎn)(NIL節(jié)點(diǎn))都是黑色。這些葉子節(jié)點(diǎn)通常表示空節(jié)點(diǎn)或沒有實(shí)際存儲值的節(jié)點(diǎn)。
  • 紅色節(jié)點(diǎn)不能相連:如果一個節(jié)點(diǎn)是紅色,它的兩個子節(jié)點(diǎn)必須是黑色。
  • 黑色完美平衡:從任何節(jié)點(diǎn)到其所有后代葉子節(jié)點(diǎn)的簡單路徑上,必須包含相同數(shù)量的黑色節(jié)點(diǎn)。這個屬性確保了紅黑樹的平衡性,避免了樹變得過高。

三、實(shí)現(xiàn)紅黑樹

3.1 節(jié)點(diǎn)定義

public class RedBlackTree {

    /**
     * 根節(jié)點(diǎn)
     */
    private Node root;

    /**
     * 顏色枚舉
     */
    enum Color {
        RED, BLACK;
    }

    private static class Node {
        int key;
        Object value;
        Node left;
        Node right;

        // 父節(jié)點(diǎn)
        Node parent;
        // 顏色 默認(rèn)為紅色
        Color color = Color.RED;

        /**
         * 判斷節(jié)點(diǎn)是否是左孩子
         *
         * @return
         */
        boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        /**
         * 獲取當(dāng)前節(jié)點(diǎn)的叔叔:爸爸的兄弟(前提必須有爺爺存在)
         *
         * @return
         */
        Node getUncle() {
            if (parent == null || parent.parent == null) {
                return null;
            }
            // 如果當(dāng)前節(jié)點(diǎn)的父親是屬于左孩子,那么爺爺?shù)挠液⒆泳褪鞘迨?            if (parent == parent.parent.left) {
                return parent.parent.right;
            } else {
                return parent.parent.left;
            }
        }

        /**
         * 獲取當(dāng)前節(jié)點(diǎn)的兄弟
         *
         * @return
         */
        Node getBrother() {
            if (parent == null) {
                return null;
            }
            // 當(dāng)前節(jié)點(diǎn)是屬于左孩子,那么父親的右孩子就是兄弟
            if (this.isLeftChild()) {
                return parent.right;
            } else {
                return parent.left;
            }
        }
    }
}

判斷節(jié)點(diǎn)顏色:

/**
 * 判斷節(jié)點(diǎn)是否是紅色
 *
 * @param node
 * @return
 */
boolean isRed(Node node) {
    return node != null && node.color == Color.RED;
}

/**
 * 判斷節(jié)點(diǎn)是否是黑色
 * @param node
 * @return
 */
boolean isBlack(Node node) {
    return !isRed(node);
    //return node == null || node.color == Color.BLACK;
}

3.2 左右旋代碼

這里的思路與AVL樹的相同,只是多了parent屬性要維護(hù),所以對每一個移動了的節(jié)點(diǎn),都需要更新parent屬性。(關(guān)于AVL樹旋轉(zhuǎn)的詳細(xì)邏輯可以看我上一篇文章)
右旋:

/**
 * 右旋:
 * 1.旋轉(zhuǎn)本身的邏輯:失衡節(jié)點(diǎn)左孩子上位 ,處理失衡節(jié)點(diǎn)的后事
 * 2.處理 待處理后事節(jié)點(diǎn),上位節(jié)點(diǎn),失衡節(jié)點(diǎn) 的父親
 * 3.處理旋轉(zhuǎn)節(jié)點(diǎn)父親的孩子
 * @param node 失衡的節(jié)點(diǎn)
 */
private void rightRotate(Node node) {
    // 失衡的節(jié)點(diǎn) node
    // 失衡節(jié)點(diǎn)的左孩子:要上位的節(jié)點(diǎn)
    Node upNode = node.left;
    // 待處理的上位節(jié)點(diǎn)的后代:上位節(jié)點(diǎn)的右孩子
    Node toChangeParent = upNode.right;

    // 1.正常的右旋處理:
    // 1.1上位
    upNode.right = node;
    // 1.2處理后事
    node.left = toChangeParent;

    // 2.更新相關(guān)移動節(jié)點(diǎn)的父親
    // 2.1 更新上位節(jié)點(diǎn)后代的父親 為 要失衡的節(jié)點(diǎn)
    if (toChangeParent != null) {
        toChangeParent.parent = node;
    }
    // 2.2 更新上位節(jié)點(diǎn)的父親:為 失衡節(jié)點(diǎn)的父親
    upNode.parent = node.parent;
    // 2.3 更新失衡節(jié)點(diǎn)的父親
    node.parent = upNode;

    // 3.處理上位節(jié)點(diǎn)父親的孩子(因?yàn)槭请p向的,步驟二只處理了節(jié)點(diǎn)的父親,針對父親還要更新父親的孩子)
    // 判斷旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于左還是還是右孩子
    if (node.parent == null) {
        // 只需要把上位節(jié)點(diǎn)更新為根節(jié)點(diǎn)
        root = upNode;
    }
    // 旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于左孩子
    else if (node.parent.left == node) {
        node.parent.left = upNode;
    }
    // 旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于右孩子
    else {
        node.parent.right = upNode;
    }
}

左旋:

/**
 * 左旋:
 * 1.旋轉(zhuǎn)本身的邏輯:失衡節(jié)點(diǎn)右孩子上位 ,處理失衡節(jié)點(diǎn)的后事
 * 2.處理 待處理后事節(jié)點(diǎn),上位節(jié)點(diǎn),失衡節(jié)點(diǎn) 的父親
 * 3.處理失衡節(jié)點(diǎn)父親的孩子
 * @param node 失衡的節(jié)點(diǎn)
 */
private void leftRotate(Node node) {
    // 失衡節(jié)點(diǎn)node
    // 上位節(jié)點(diǎn):失衡節(jié)點(diǎn)的右孩子
    Node upNode = node.right;
    // 待處理后事的節(jié)點(diǎn):上位節(jié)點(diǎn)的左孩子
    Node toChangeParent = upNode.left;

    // 1.正常的左旋處理
    // 1.1 節(jié)點(diǎn)上位
    upNode.left = node;
    // 1.2 處理后事: 后代toChangeParent父親更換為原本的失衡節(jié)點(diǎn)
    node.right = toChangeParent;

    // 2. 更新相關(guān)移動節(jié)點(diǎn)的父親
    // 2.1 上位節(jié)點(diǎn)后代toChangeParent的父親 更新為:失衡的節(jié)點(diǎn)
    if (toChangeParent != null) {
        toChangeParent.parent = node;
    }
    // 2.2 更新上位節(jié)點(diǎn)的父親 為 失衡節(jié)點(diǎn)的父親
    upNode.parent = node.parent;
    // 2.3 更新失衡節(jié)點(diǎn)的父親 為 上位節(jié)點(diǎn)
    node.parent = upNode;

    // 3.處理失衡節(jié)點(diǎn)的父親的孩子
    // 如果失衡節(jié)點(diǎn)原本是根節(jié)點(diǎn)
    if (node.parent == null) {
        root = upNode;
    }
    // 失衡節(jié)點(diǎn)原本是屬于左孩子
    else if (node.parent.left == node) {
        node.parent.left = upNode;
    } else {
        node.parent.right = upNode;
    }
}

3.3 插入

假設(shè)插入的節(jié)點(diǎn)為紅色,一共包含以下四種情況:

1.插入節(jié)點(diǎn)為根節(jié)點(diǎn)

將根節(jié)點(diǎn)變黑

2.插入節(jié)點(diǎn)的父親為黑色

樹的紅黑性質(zhì)不變,無需調(diào)整

3.插入節(jié)點(diǎn)的父親和叔叔都為紅色

此種情況只需要變色:

  • 父親變?yōu)楹谏ù藭r違反了黑色平衡),為了保證黑色平衡,連帶的叔叔也變?yōu)楹谏?/li>
  • 此時當(dāng)前路徑多了一個黑色,其他路徑?jīng)]有多的黑色,所以需要在當(dāng)前路徑中再減少黑色:爺爺變成紅色。
  • 爺爺變成紅色,可能又會接著觸發(fā)紅紅相鄰,因此對將爺爺進(jìn)行遞歸調(diào)整,最后根節(jié)點(diǎn)需要變黑

在下圖的情況下插入1:


插入1.png

樹的變色過程:
第一輪:對于插入節(jié)點(diǎn)來說:父親和叔叔變黑,爺爺變紅;
第二輪:對于爺爺來說:爺爺?shù)母赣H和爺爺?shù)氖迨遄兒冢瑺敔數(shù)臓敔斪兗t;(與第一步相同,直接遞歸)
最后爺爺?shù)臓敔斒歉?jié)點(diǎn),所以變黑


紅黑樹插入.gif

總結(jié):父親和叔叔變黑,爺爺變紅,遞歸變化直到根節(jié)點(diǎn),最后根節(jié)點(diǎn)變?yōu)楹谏?/p>

4.插入節(jié)點(diǎn)的父親為紅色,叔叔為黑色

此種情況需要變色+旋轉(zhuǎn):

  • ①父親為左孩子,插入節(jié)點(diǎn)也是左孩子,此時即 LL 不平衡

    • 讓父親變黑,為了保證這顆子樹黑色不變,將爺爺變成紅,但叔叔子樹少了一個黑色
    • 爺爺右旋,補(bǔ)齊一個黑色給叔叔,父親旋轉(zhuǎn)上去取代爺爺,由于它是黑色,不會再次觸發(fā)紅紅相鄰


      image.png
  • ②父親為左孩子,插入節(jié)點(diǎn)是右孩子,此時即 LR 不平衡

    • 父親左旋,變成 LL 情況,按 1. 來后續(xù)處理


      image.png
  • ③父親為右孩子,插入節(jié)點(diǎn)也是右孩子,此時即 RR 不平衡

    • 讓父親變黑,為了保證這顆子樹黑色不變,將爺爺變成紅,但叔叔子樹少了一個黑色
    • 爺爺左旋,補(bǔ)齊一個黑色給叔叔,父親旋轉(zhuǎn)上去取代爺爺,由于它是黑色,不會再次觸發(fā)紅紅相鄰


      image.png
  • ④父親為右孩子,插入節(jié)點(diǎn)是左孩子,此時即 RL 不平衡

    • 父親右旋,變成 RR 情況,按 3. 來后續(xù)處理


      image.png

插入方法:尋找位置插入:

/**
 * 插入/更新節(jié)點(diǎn)
 * @param key
 * @param value
 */
public void put(int key,Object value) {
    // 找到插入的位置
    Node pointer = root;
    // 插入位置的父親
    Node parent = null;
    while (pointer != null) {
        parent = pointer;
        if (key < pointer.key) {
            // 往左找
            pointer = pointer.left;
        } else if (key > pointer.key) {
            // 往右找
            pointer = pointer.right;
        } else {
            // 找到了 直接更新值
            pointer.value = value;
            return;
        }
    }
    // 沒有找到,當(dāng)前指針pointer指向的是要插入的位置
    Node added = new Node(key, value);
    // 樹為空,新增節(jié)點(diǎn)作為根節(jié)點(diǎn)
    if (parent == null) {
        root = added;
    } else if (key < parent.key) {
        // 新增節(jié)點(diǎn)作為左孩子
        parent.left = added;
        // 設(shè)置新增節(jié)點(diǎn)的父親
        added.parent = parent;
    } else {
        // 新增節(jié)點(diǎn)作為右孩子
        parent.right = added;
        added.parent = parent;
    }

    // 插入結(jié)束后,不平衡的情況下需要對樹進(jìn)行旋轉(zhuǎn)和變色
    fixRedRed(added);
}

在插入了節(jié)點(diǎn)之后,樹失衡,對樹進(jìn)行旋轉(zhuǎn)變色方法:

/**
 * 當(dāng)出現(xiàn)兩個相鄰紅色節(jié)點(diǎn)的時候?qū)涞恼{(diào)整
 * 1.插入的是根節(jié)點(diǎn)直接變黑
 * 2.插入節(jié)點(diǎn)的父親就是黑色,無需做任何操作
 * 3.插入節(jié)點(diǎn)的父親是紅色,本身也是紅色,觸發(fā)紅紅相連
 *  - 叔叔是紅色:父親和叔叔一起變紅,爺爺變黑;遞歸直到根節(jié)點(diǎn)
 *  - 叔叔是黑色:此種情況,因?yàn)樽兩珪r叔叔跟父親顏色不同,所以變色做不到平衡,需要旋轉(zhuǎn)
 *      - 父親是左孩子,插入節(jié)點(diǎn)也是左孩子 LL
 *      - 父親是左孩子,插入節(jié)點(diǎn)是右孩子  LR
 *      - 父親是右孩子,插入節(jié)點(diǎn)是右孩子  RR
 *      - 父親是右孩子,插入節(jié)點(diǎn)是左孩子  RL
 */
public void fixRedRed(Node node) {
    // 1.插入的是根節(jié)點(diǎn)直接變黑
    if (node == root) {
        node.color = Color.BLACK;
        return;
    }
    // 2.插入節(jié)點(diǎn)的父親就是黑色,無需做任何操作
    else if (isBlack(node.parent)) {
        return;
    }
    //3. 父親和叔叔都是紅色
    // 代碼執(zhí)行到這里 父親一定是紅色
    // 父親
    Node parent = node.parent;
    // 叔叔
    Node uncle = node.getUncle();
    // 爺爺
    Node grandpa = parent.parent;
    // 叔叔是紅色(此時父親是紅色):
    if (isRed(uncle)) {
        // 父親和叔叔變黑 (路徑多了黑,所以爺爺要變紅)
        parent.color = Color.BLACK;
        uncle.color = Color.BLACK;
        // 爺爺變紅 (又可能觸發(fā)紅紅相連)
        grandpa.color = Color.RED;
        // 遞歸執(zhí)行
        fixRedRed(grandpa);
    }
    // 叔叔是黑色(此時父親是紅色)叔叔和父親顏色不同,通過變色做不到平衡,需要旋轉(zhuǎn)
    // 父親是左孩子,插入節(jié)點(diǎn)也是左孩子 LL不平衡
    if (parent.isLeftChild() && node.isLeftChild()) {
        // 1.變色:父親變黑,爺爺變紅
        parent.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.右旋:爺爺右旋
        rightRotate(grandpa);
    }
    // 父親是左孩子,插入節(jié)點(diǎn)是右孩子 LR不平衡
    else if (parent.isLeftChild() && !node.isLeftChild()) {
        // 1.父親左旋 變成了LL
        leftRotate(parent);
        // LL:變色+旋轉(zhuǎn)
        // 變色:父親變黑(在父親左旋之后,此時父親是插入的節(jié)點(diǎn)),爺爺變紅
        node.color = Color.BLACK;
        grandpa.color = Color.RED;
        rightRotate(grandpa);
    }
    // 父親是右孩子,插入節(jié)點(diǎn)也是右孩子 RR不平衡
    else if (!parent.isLeftChild() && !node.isLeftChild()) {
        // 1.變色:父親變黑,爺爺變紅
        parent.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.爺爺左旋
        leftRotate(grandpa);
    }
    // 父親是右孩子,插入節(jié)點(diǎn)是左孩子,RL不平衡
    else {
        // 父親右旋,變成RR場景 此時父親變成了node
        rightRotate(parent);
        // 1.變色:父親變黑,爺爺變紅
        node.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.旋轉(zhuǎn):爺爺左旋
        leftRotate(grandpa);
    }
}

平衡的過程就是變色+旋轉(zhuǎn)

3.4 刪除

1.刪除節(jié)點(diǎn)沒有孩子

  • 刪除的節(jié)點(diǎn)就是根節(jié)點(diǎn):直接刪除
  • 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn):直接把刪除節(jié)點(diǎn)的父親的孩子置空,刪除節(jié)點(diǎn)的父親也置空(有助于垃圾回收)

2.刪除節(jié)點(diǎn)有一個孩子

  • 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn):
    • 這個孩子一定是紅色(如果是黑色的話就不平衡了)。
      • 剩余節(jié)點(diǎn)和根節(jié)點(diǎn)的 key,value交換,再把根節(jié)點(diǎn)孩子設(shè)置為null,顏色保持黑色不變
  • 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn):讓刪除節(jié)點(diǎn)的父親指向刪剩下的節(jié)點(diǎn)(向下的指針),刪剩下的父親指向刪除節(jié)點(diǎn)的父親(向上的指針);本身刪除節(jié)點(diǎn)的左右孩子和父親置空(垃圾回收)

3.刪除節(jié)點(diǎn)有兩個孩子

想辦法把兩個孩子轉(zhuǎn)變成情況二的一個孩子:
交換刪除節(jié)點(diǎn)和后繼節(jié)點(diǎn)的 key,value,這樣就轉(zhuǎn)變成了情況二,遞歸刪除后繼節(jié)點(diǎn),直到該節(jié)點(diǎn)沒有孩子或只剩一個孩子。

失衡情況:

刪黑色節(jié)點(diǎn)需要考慮平衡,刪紅色不需要

  • 紅色是葉子節(jié)點(diǎn),不會失衡
  • 紅色是非葉子節(jié)點(diǎn),他會有兩個孩子,針對兩個孩子的情況,都會被轉(zhuǎn)換為只有一個孩子的情況,通過交換,進(jìn)入上面的刪除情況二中。

失衡情況一
刪的黑色節(jié)點(diǎn),剩下的是一個紅色節(jié)點(diǎn):

  • 刪了黑色之后,把剩下的這個紅節(jié)點(diǎn)變黑(缺少的黑色通過紅孩子變黑補(bǔ)齊了)

失衡情況二
刪的是黑色節(jié)點(diǎn),剩下的也是一個黑色節(jié)點(diǎn)(或者是null): 當(dāng)該節(jié)點(diǎn)刪除了這個黑色之后,整條路徑上對比其他路徑就少了一個黑色。

  • 2.1 刪除節(jié)點(diǎn)的兄弟為紅色,此時兩個侄子一定是黑色(此種情況轉(zhuǎn)為后續(xù)兩種處理)


    image.png
  • 2.2 刪除節(jié)點(diǎn)的兄弟為黑色,侄子都是黑:

    • 有紅色父親的時候:把兄弟變紅(平衡刪掉的黑色節(jié)點(diǎn)),把父親變黑(平衡該子樹與其他子樹對比少的黑色)


      image.png
    • 沒有紅色父親的時候:以父親作為起點(diǎn),觸發(fā)黑黑的方法,把父親的兄弟變成紅色;
      依次遞歸處理到根節(jié)點(diǎn),那么每一條路徑都把一個黑色變成了紅色,實(shí)現(xiàn)黑色平衡(當(dāng)然如果父親已經(jīng)是紅色了,可以直接變父親的顏色為黑色(就是上面一種情況)


      image.png
  • 2.3 刪除節(jié)點(diǎn)的兄弟為黑色,侄子至少有一個紅:

    • 兄弟是左孩子,左侄子是紅色:LL


      image.png

變色邏輯:
1.通過旋轉(zhuǎn)過來的3變成黑色補(bǔ)齊該路徑上少的黑色
2.針對上位節(jié)點(diǎn)2,變成原本父親的顏色,因?yàn)檫@條路徑上本身是平衡的,所以上來的要變成原本父親的顏色
3.針對紅色的侄子1,變成黑色,因?yàn)樵驹撀窂缴希粍h除節(jié)點(diǎn)的兄弟被當(dāng)成了父親,所以原本作為這個路徑的黑色兄弟就走了,少了一個黑色,所以讓紅色侄子變黑。

  • 兄弟是左孩子,右侄子是紅色:LR
    這里代碼層面可以做簡化,直接對比圖中1和6,最后侄子上位變成了父親的顏色,原本的父親變成黑色


    image.png
    • 兄弟是右孩子,右侄子是紅色:RR(此種場景和LL類似)
    • 兄弟是右孩子,左侄子是紅色:RL(此種場景和LR類似)
    • 針對左右侄子都是紅色的場景,走LL或者RR都是行得通的


      image.png

代碼:

/**
 * 刪除節(jié)點(diǎn)
 * @param deleted
 */
public void doRemove(Node deleted) {
    // 刪除節(jié)點(diǎn)的后繼節(jié)點(diǎn)
    Node replace = findReplace(deleted);
    //分三種大情況:沒有孩子 一個孩子 兩個孩子

    // 被刪除節(jié)點(diǎn)的父親
    Node deletedParent = deleted.parent;

    // 一、沒有孩子
    if (replace == null) {
        // 1.1 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn)
        if (deleted == root) {
            root = null;
        }
        // 1.2 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn)
        else {
            // 變色(注意這里要先變色 再刪除)
            if (isBlack(deleted)) { // 被刪除的是黑色 因?yàn)闆]有孩子(孩子為黑色),所以也屬于下面的刪除的是黑,剩下的孩子也是黑
                // 旋轉(zhuǎn) + 變色
                fixBlackBlack(deleted);
            } else { // 被刪除的是紅色 不需要做處理
                // do nothing
            }
            // 刪除操作
            // 刪除節(jié)點(diǎn)屬于左孩子
            if (deleted.isLeftChild()) {
                deletedParent.left = null;
            } else { // 刪除節(jié)點(diǎn)屬于右孩子
                deletedParent.right = null;
            }
            deleted.parent = null;
        }
        return;
    }

    // 二、有一個孩子
    if (deleted.left == null || deleted.right == null) {
        // 2.1 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn) 這個唯一的孩子一定是紅色:根節(jié)點(diǎn)和孩子互換,刪除孩子
        if (deleted == root) {
            // delete和replace交換
            deleted.key = replace.key;
            deleted.value = replace.value;
            deleted.left = null;
            deleted.right = null;
        }
        // 2.2 不是根節(jié)點(diǎn)
        else {
            // 刪除操作
            // 看被刪除的節(jié)點(diǎn)是左孩子還是右孩子
            if (deleted.isLeftChild()) {
                deletedParent.left = replace;
            } else {
                deletedParent.right = replace;
            }
            replace.parent = deletedParent;
            // 被刪除的節(jié)點(diǎn)孩子還父親置空
            deleted.parent = null;
            deleted.left = null;
            deleted.right = null;

            // 變色操作
            // 刪除的是黑色 ,刪剩下的也是黑色
            if (isBlack(deleted) && isBlack(replace)) {
                // 旋轉(zhuǎn) + 變色
                fixBlackBlack(replace);
            } else { // 刪除的是黑色,剩下的是紅色:
                // 少了一個黑色,把剩下的紅色變黑即可
                replace.color = Color.BLACK;
            }
        }
        return;
    }

    // 三、有兩個孩子
    /**
     * 這種情況轉(zhuǎn)為只有一個孩子的情況:
     * 將要刪除的節(jié)點(diǎn)和后繼節(jié)點(diǎn)的鍵值交換,那么要刪除的節(jié)點(diǎn)就變成了后繼節(jié)點(diǎn),此時一直遞歸調(diào)用,直到只有一個孩子的情況,進(jìn)入情況二的分支
     */
    // 交換key
    int tempKey = deleted.key;
    deleted.key = replace.key;
    replace.key = tempKey;

    // 交換value
    Object tempVal = deleted.value;
    deleted.value = replace.value;
    replace.value = tempVal;

    // 要刪的就變成了replace 遞歸直到進(jìn)入條件二
    doRemove(replace);
}

被刪除的節(jié)點(diǎn)是黑色,刪剩下的也是黑色場景的旋轉(zhuǎn)和變色:

/**
 * 被刪除的節(jié)點(diǎn)是黑色,刪剩下的也是黑色(整體這個路徑上少了一個黑色,失衡)
 * - 情況一.被刪節(jié)點(diǎn)的兄弟是紅色,此時侄子一定是黑色:左旋+變色 轉(zhuǎn)為下面兩種情況
 * - 情況二.被刪節(jié)點(diǎn)的兄弟是黑色,侄子(這倆的孩子)都是黑:
 * - 情況三.被刪節(jié)點(diǎn)的兄弟是黑色,侄子(這倆的孩子)中至少一個紅:
 * @param node
 */
private void fixBlackBlack(Node node) {
    if (node == root) {
        //此時處理結(jié)束
        return;
    }
    // 節(jié)點(diǎn)的兄弟
    Node brother = node.getBrother();
    // 節(jié)點(diǎn)的父親
    Node parent = node.parent;
    // 情況一.被刪節(jié)點(diǎn)的兄弟是紅色,此時侄子一定是黑色:左旋+變色
    if (isRed(brother)) {
        if (node.isLeftChild()) {
            // node屬于左節(jié)點(diǎn)  父親左旋
            // 1.1 父親左旋
            leftRotate(parent);
        } else {
            rightRotate(parent);
        }
        // 1.2 調(diào)整平衡
        brother.color = Color.BLACK;
        parent.color = Color.RED;

        // 此時把兄弟變成了黑色了 就符合了情況二和情況三,再次調(diào)用方法進(jìn)行二和三情況的處理
        fixBlackBlack(node);
        return;
    }
    // 情況二:刪除節(jié)點(diǎn)的兄弟為黑色,侄子都是黑:
    assert brother != null;
    if (isBlack(brother.left) && isBlack(brother.right)) {
        // 2.1兄弟變紅
        brother.color = Color.RED;
        // 如果此時父親是紅,則讓父親變黑就維持了平衡了
        if (isRed(parent)) {
            parent.color = Color.BLACK;
        }
        // 如果父親是黑,觸發(fā)黑黑調(diào)整:讓兄弟變紅;依次遞歸,讓每一條路徑都有一個兄弟變紅,就都少了一個黑色,實(shí)現(xiàn)黑色平衡
        else {
            fixBlackBlack(parent);
        }
    } else {
        // 情況三:刪除的節(jié)點(diǎn)兄弟是黑色,侄子至少一個紅色(一個紅色或者兩個都是紅色)
        /**
         *  兄弟是左孩子,左侄子是紅色:LL :父親右旋+變色
         *  兄弟是左孩子,右侄子是紅色:LR
         *  兄弟是右孩子,右侄子是紅色:RR
         *  兄弟是右孩子,左侄子是紅色:RL
         */
        // LL: 兄弟是左孩子,左侄子是紅色
        if (brother.isLeftChild() && isRed(brother.left)) {
            // 父親右旋
            rightRotate(parent);
            /**
             * 變色邏輯:
             * 1.通過旋轉(zhuǎn)過來的節(jié)點(diǎn)變成黑色補(bǔ)齊該路徑上少的黑色
             * 2.針對上位節(jié)點(diǎn),變成原本父親的顏色,因?yàn)檫@條路徑上本身是平衡的,所以上來的要變成原本父親的顏色
             * 3.針對紅色的侄子,變成黑色,因?yàn)樵驹撀窂缴希粍h除節(jié)點(diǎn)的兄弟被當(dāng)成了父親,所以原本作為這個路徑的黑色兄弟就走了,少了一個黑色,所以讓紅色侄子變黑。
             */
            // 代碼變色從后往前,因?yàn)閺那巴髸?dǎo)致還變色的顏色丟失了
            // 侄子變黑
            brother.left.color = Color.BLACK;
            //上位節(jié)點(diǎn)(兄弟節(jié)點(diǎn))變成原來父親的顏色
            brother.color = parent.color;
            // 原本父親節(jié)點(diǎn)旋轉(zhuǎn)下去了 變成黑色
            parent.color = Color.BLACK;
        }
        // LR:兄弟是左孩子,右侄子是紅色
        else if (brother.isLeftChild() && isRed(brother.right)) {
            /**
             * 這里的代碼可以做簡化,旋轉(zhuǎn)之前和之后分別做一次變色即可
             */
            // 侄子上位,變成父親的顏色(這里要先變色,因?yàn)橹蹲邮且氯サ模驼也坏接液⒆恿耍?            brother.right.color = parent.color;
            // 兄弟左旋
            leftRotate(brother);
            // 父親右旋
            rightRotate(parent);
            // 父親最后退位,顏色變黑,補(bǔ)齊刪除的黑色
            parent.color = Color.BLACK;
        }
        // RR
        else if (!brother.isLeftChild() && !isRed(brother.left)) {
            // 父親左旋
            leftRotate(parent);
            // 侄子變黑
            brother.right.color = Color.BLACK;
            //上位節(jié)點(diǎn)(兄弟節(jié)點(diǎn))變成原來父親的顏色
            brother.color = parent.color;
            // 原本父親節(jié)點(diǎn)旋轉(zhuǎn)下去了 變成黑色
            parent.color = Color.BLACK;
        }
        // RL
        else {
            // 侄子上位,變成父親的顏色(這里要先變色,因?yàn)橹蹲邮且氯サ模驼也坏接液⒆恿耍?            brother.left.color = parent.color;
            // 兄弟右旋
            rightRotate(brother);
            // 父親左旋
            leftRotate(parent);
            // 父親最后退位,顏色變黑,補(bǔ)齊刪除的黑色
            parent.color = Color.BLACK;
        }
    }
}

四、總結(jié)

復(fù)雜度分析

操作 普通二叉搜索樹 AVL樹 紅黑樹
查詢 平均O(logn),最壞O(n) O(logn) O(logn)
插入/更新 平均O(logn),最壞O(n) O(logn) O(logn)
刪除 平均O(logn),最壞O(n) O(logn) O(logn)

二叉搜索樹

二叉搜索樹的插入、刪除、查詢的時間復(fù)雜度與樹的高度相關(guān),因此在最壞情況下,時間復(fù)雜度為O(n),而且容易退化成鏈表,查找效率低,不平衡。

AVL樹

AVL樹是一種嚴(yán)格平衡的二叉搜索樹,其左右子樹的高度差不超過1。因此,它能夠在logn的平均時間內(nèi)完成插入、刪除、查詢操作,但是在維護(hù)平衡的過程中,需要頻繁地進(jìn)行旋轉(zhuǎn)操作,導(dǎo)致插入刪除效率較低。

紅黑樹

紅黑樹是一種近似平衡的二叉搜索樹,它在保持高度平衡的同時,又能夠保持較高的插入刪除效率。紅黑樹通過節(jié)點(diǎn)著色和旋轉(zhuǎn)操作來維護(hù)平衡。紅黑樹在維護(hù)平衡的過程中,能夠進(jìn)行較少的節(jié)點(diǎn)旋轉(zhuǎn)操作,因此插入刪除效率較高,并且查詢效率也較高。

綜上所述,紅黑樹具有較高的綜合性能,是一種廣泛應(yīng)用的數(shù)據(jù)結(jié)構(gòu)。

完整代碼鏈接:https://github.com/GitHongcx/algorithm-demo/blob/main/src/main/java/com/hcx/algorithm/tree/RedBlackTree.java

補(bǔ)充:這篇文章對旋轉(zhuǎn)部分沒有描述的很詳細(xì),在我上一篇AVL樹的實(shí)現(xiàn)里有很詳細(xì)的介紹旋轉(zhuǎn)的邏輯,可以參考。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容