左傾の赤黒木についての深い理解 | JD Logistics Technical Team

バランスの取れた二分探索木

バランス二分探索ツリーの各ノードの左サブツリーと右サブツリーの高さの差は 1 を超えません。挿入、検索、削除操作を O(logn) 時間以内に完了できます。提案された最初の自己平衡二分探索です。ツリーは AVL ツリーです。

挿入または削除操作を実行した後、AVL ツリーはノードのバランス係数に基づいてバランスが取れているかどうかを判断します。バランスが取れていない場合は、ツリーのバランスを維持するために回転操作が実行されます。この記事では主に赤について説明します-blacktree. 興味があれば、AVL ツリー関連の知識について学ぶことができますが、ここでは詳しく説明しません。

2-3 検索ツリー

標準二分木のノードは 2 ノード (1 つのキーと 2 つのポインタを含む) と呼ばれますが、二分探索木のバランスを確保するには、3 ノード (2 つのキーと 3 つのポインタを含む) を追加することで柔軟性を加える必要があります。ポインタ)。2 ノード ポインタと 3 ノード ポインタに対応する間隔サイズの関係は次のとおりです。

  • 2 ノード: 左ポインタが指す左サブツリーのすべてのキー値が現在のノード キー値より小さく、右ポインタが指す右サブツリーのすべてのキー値が現在のノードより大きいキーの値。

  • 3 ノード: 左ポインタが指す左サブツリー内のすべてのキー値は現在のノードのキー値より小さく、中間ポインタが指す中性子ツリー内のすべてのキー値は 2 つのキーの間にあります。現在のノードの値、および右ポインタが指すすべてのキー値は、右サブツリー内のすべてのキー値が現在のノードのキー値より大きい

まずは 2-3 検索ツリーの例を見てみましょう。

画像.png

ここで注意すべき点は、2-3 の探索ツリーでノードがどのように追加または削除されても、常に完全なバランスを維持できることです。以下を読む際には、この点に留意してください。 

3-ノードの導入によりツリーのバランスがどのように確保されるのですか?

例として、キー値 11 が挿入されたノードを考えます。ノード 12 のキーに対応する左側のサブツリーが検索されます。3 ノードを導入しない場合、図の (1) に示すように、ツリーの高さは増加します。下の図のように、3 ノードを導入した後は、ツリーの高さを増加させることなく 2 ノードを 3 ノードに置き換えるだけで済みます。図の (2) に示すように、2-3 探索ツリーは依然として完全にバランスが取れています。次の図:

画像.png

次に、挿入したキー値が 26 の場合、キーが 19 と 24 にある 3 つのノードが検索されます。このノードには冗長なキーの位置がないため、26 のキーは次の場所にのみ配置できます。右側のサブツリーの位置により、ツリーの高さに 1 が加えられます。ただし、賢い方法を使用して、最初にノードを 4 ノードに変換し、次に 4 ノードを 3 つの 2 ノードに変換することができます。これら 3 つの 2 ノードのルート ノード (ノードの中間キー) を変換します。次の図に示すように、4 ノードの値) が元の親ノードに挿入されると、木の高さは変更されないままになります (完全なバランス)。

画像.png

一時的に 4 ノードを使用しましたが、最終的な変換後、ツリー全体は依然として 2 ノードと 3 ノードで構成されています。

キー値 4 のノードを挿入するとどうなるでしょうか? 3 ノード 1 と 3 の位置を検出し、同じ方法を使用して 4 ノードに変換しますが、変換後に元の親ノードに挿入する準備ができたときに、次のことがわかります。親ノードも 3 ノードであるため、同じ方法を再度使用する必要があり、これを一般的なケースに一般化すると、一時的な 4 ノードを常に分解して、上位の親にキーを挿入する必要があります。以下に示すように、2 ノードに遭遇し、それを分解を必要としない 3 ノードに変換するか、2 ノードとしてルート ノードに到達するまで、ノードを作成します。

2-3の木4.jpg

ルート ノードが処理されるときに、ルート ノードがまだ 3 ノードである場合はどうなるでしょうか? 実際、原理は同じですが、ルート ノードには親ノードがないため、4 ノードの中間キーを上にマージする必要はなく、それらを 3 つの 2 ノードに変換するだけで、ツリーの高さは次のようになります。それに応じて 1 つ増加します。

上記の状況では、2-3 ツリー挿入アルゴリズムにより、ツリー自体の構造がローカルに変更されます。関連するノードと参照に加えて、ツリーの他の部分を変更したりチェックしたりする必要はありません。これらのローカル変換挿入プロセス中、ツリーの全体的な順序とバランスには影響しません。2-3ツリーは常に完全にバランスの取れたバイナリ ツリーになります  

左傾赤黒木 (LLRBT)

2 ~ 3 個のツリーを学習した後、最初に単純な赤黒ツリーである左傾赤黒ツリーを学習しましょう。これは古典的な赤黒ツリーの変形であり、比較的実装が簡単です。その基本的な考え方は、標準の二分探索ツリーと「色情報」を使用して 2 ~ 3 ツリーを表すことです。リンクは 2 つのタイプに分けられ、赤いリンクは 2 つの 2 ノードを接続して 3 ノードを形成し、黒いリンクは 2 つのタイプに分けられます。 link 以下の図に示すように、2-3 ツリー内の通常のリンクです。

LLRB.jpg

赤いリンクが指すノードは赤いノード、黒いリンクが指すノードは黒いノードであると規定します。上の図から、赤いリンクを「ひっくり返す」と、 3 ノードの表現と同じです。これは、左傾の赤黒ツリーの 3 ノードを表現するために色情報がどのように使用されるかです。正確には、3 ノードは 2 つの 2 ノードと 1 つの 2 ノードで構成されます。 2 つのノードのうちの 2 つのノードは、他の 2 つのノードによって赤色に色付けされます。リンク左参照 

左傾の赤黒木は、次の条件を満たす 2-3 探索木であると言えます。

  • ルートノードは黒です

  • 赤いリンクはすべて左の参照です

  • 2 つの赤いリンクに同時に接続されているノードはありません

  • リーフ ノードからルート ノードまでのパス上の黒いノードの数は同じです。つまり、ツリーは黒のバランスが取れています。

2-3 探索木は常に完全なバランスを維持できるため、どの葉ノードからルートノードまでの距離も等しくなります。左傾の赤黒ツリーも 2-3 探索ツリーであり、その中の黒いリンクは 2-3 探索ツリーの通常のリンクです。そして、左傾の赤黒ツリーの黒いリンクによって参照される黒いノードは、ブラック ツリーも完全にバランスが取れている必要があるため、リーフ ノードからルート ノードまでのパス上の黒いノードの数は同じでなければなりません。

プロパティを理解しました。次に、左傾の赤黒ツリーのコード実装を見てみましょう。

ノードの定義

上で、赤いリンクによって参照されるノードは赤、黒のリンクによって参照されるノードは黒であると規定しました。色をマークするフィールドをノードに追加します:赤、黒、次のように定義されます。 color True False 

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    /**
     * 判断节点是否为红色
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return false;
        } else {
            return node.color == RED;
        }
    }
}

回転させる

挿入および削除操作により、左寄りの赤黒ツリーに赤のリンクと右の参照が含まれたり、左右の参照の両方が赤のリンクになったりして、黒の不均衡が生じる可能性があります。左寄りの赤黒ツリーの特性を常に満たすために、ツリーがある場合は、回転操作を通じて修正する必要があります。

左利き

赤リンクの右基準がある場合、左折操作により赤リンクの左基準に調整することができます。そのプロセスは次の図に示すようになります。  

LLRB2.jpg

コードは以下のように表示されます。

    /**
     * 左旋
     */
    private Node rotateLeft(Node node) {
        Node newRoot = node.right;
        node.right = newRoot.left;
        newRoot.left = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

回転が完了すると、左回転により、ルート ノードとしての 2 つの結合のうち小さい方の結合が、ルート ノードとしての大きい方の結合に変更されることがわかります。

右回転

右回転は左回転の逆で、赤いリンクの左の基準を赤いリンクの右の基準に回転させることができます。プロセスは次の図に示すとおりです。

LLRB3.jpg

コードは以下のように表示されます。

    /**
     * 右旋
     */
    private Node rotateRight(Node node) {
        Node newRoot = node.left;
        node.left = newRoot.right;
        newRoot.right = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

右回転により、2 つの結合のうち大きい方の結合がルート ノードとして、小さい方の結合がルート ノードとして変更されます。

左手メソッドと右手メソッドの戻り値は同じです。回転が完了すると、回転されたノードへの親ノードの参照が変更されます。ノードを挿入するとき、回転操作は順序を維持できます。赤と黒の木の左傾の完璧なバランス。つまり、赤と黒の木の中で回転するとき、順序や完璧なバランスを心配する必要はありません。 Node   

ノードの挿入

挿入された各ノードはデフォルトでは赤ですが、挿入後のノードの色に応じて回転操作を使用して修復されます。状況に応じて説明します。

2ノードに挿入

2 ノードへのノードの挿入は非常に簡単です。新しいキーの値が古いキーより小さい場合、ノードは古いノードの赤いリンクによって参照されたままにする必要があります。新しいキーの値が古いキーより大きい場合は、 , その場合、ノードは古いノードの赤いリンクによって左に参照される必要があります。左に傾いた赤黒ツリーの赤いリンクは左から参照できないため、リンクは右に参照されます。 -ターン調整プロセス図は次のとおりです。

LLRB4.jpg

3ノードに挿入

3 ノードへの挿入には 3 つのケースがあり、それぞれについて説明します。

  • 新しいノードは 3 より大きい - ノード内の 2 つのキー値

この場合、ノードは 3 ノードによって右参照されます。前にも述べたように、3 ノードに新しいノードを挿入すると、一時的に 4 ノードに変換されて処理され、4 ノードは変換できます。 3 つの 2 ノードにすると、木の高さ 2 のバランスのとれた木が得られます。このとき、ルート ノードの左右の参照は両方とも赤いリンクです。これらをすべて黒のリンク (色を反転) に変換できます。次の図に示すように、処理)。

LLRB5.jpg

  • 新しいノードは 3 未満です - ノード内の 2 つのキー値

この場合、ノードは 3 ノードによって参照されたままになっているため、2 つの連続する赤いリンクが存在します。必要なのは、上の赤いリンクを右に回転するだけで、上で説明した最初の状況に変換されます。プロセスは次のとおりです。以下に示されています:

LLRB6.jpg

  • 新しいノードは、3 ノードの 2 つのキー値の間にあります。

この状況では、まだ 2 つの連続した赤いリンクが存在します。下の図に示すように、下の赤いリンクを左回りに変換して、上記の 2 番目の状況に変換するだけです。

LLRB7.jpg

色変換

ノードの 2 つの赤いリンクを黒いリンクに変換する上記のアクションのメソッドを定義する必要があります。この図では、左右のノードが黒に変換されることに加えて、ノードが赤に変換されることがわかります。カラー変換は基本的に回転操作と同様にローカルであり、ツリー全体のブラック バランスには影響しませんノードの色が赤に変換された後は、それを親ノードに送信することと同じであり、親ノードに新しいキーが挿入されることを意味します。上記の挿入されたノードの対応状況に応じて処理する必要があります。 flipColors  

    /**
     * 颜色转换
     */
    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

ただし、ルート ノードの場合、色変換はこのルールに完全には従いません。ルート ノードが赤の場合、ルート ノードは 3 つのノードの中で小さいノードであるためですが、実際にはそうではありません (ルート ノードノードは常に大きい値です)。したがって、ノードを挿入するたびに、ルート ノードが black であることを確認する必要がありますルート ノードが赤から黒に変化するたびに、木の高さが 1 ずつ増加することになります  

3 ノードにノードを挿入した後、一時的に 4 ノードに変換され、4 ノードの中央のキーがその親ノード (左参照か右参照かに関係なく) に挿入されます。赤いノード。中央のキーが赤いノードです。親ノードの挿入は、新しく挿入された赤いノードを処理するのと同じです。ルート ノードまたは 2 ノードが見つかるまでこのプロセスを繰り返す必要があるため、常に維持できます左傾の赤黒木と 2-3 探索木。

LLRB8.jpg

上の図は、ノード挿入後に回転と色変換が実行されるすべての状況に対応しています。ツリーのバランスを保つためのこれらの操作は、挿入ノードからルート ノードまでのパス上のすべてのノードで実行する必要があります (バックトラッキング)。ルールは次のとおりです。

  • ノードの右側のノードが赤で、左側のノードが空または黒の場合、左回転が必要です。

  • ノードの左ノードが赤の場合、左ノードの左ノードはまだ赤なので、右に回転する必要があります。

  • ノードの左ノードと右ノードが両方とも赤の場合、色変換を実行する必要があります

ノードを挿入するときに考えられるすべての状況について説明したので、左傾の赤黒ツリーの挿入メソッドを実装します。実装は通常の二分探索木の挿入方法を拡張し、ツリーのバランスを保つためのロジックを追加したもので、下からため、回転と色変換のロジックを追加する必要があります。再帰ロジック、つまりバックトラック中にツリーのバランスを修復する場合、コードは次のとおりです。  

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key > node.key) {
            node.right = put(node.right, key, value);
        } else if (key < node.key) {
            node.left = put(node.left, key, value);
        } else {
            node.value = value;
        }
        // 将3-节点的红链接右引用左旋
        if (isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        // 将4-节点两条连续的红链接左引用左旋
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        // 进行颜色转换并将红链接在树中向上传递
        if (isRed(node.left) && isRed(node.right)) {
            flipColor(node);
        }

        return node;
    }

ノードの削除

ノードの削除方法は、左傾赤黒ツリーで最も複雑な実装であるため、以下の内容は少し難しく感じるかもしれませんが、がっかりしないでください。私がこのプロセスを理解するまでに何度も繰り返しました。勉強中です。あなたももっと辛抱してください。

左傾の赤黒ツリーの削除プロセス中に、2 つのノードが直接削除されると、次の図に示すように、空のリンクが残り、ツリーの完全なバランスが崩れてしまいます。 
LLRB_DELETE.jpg

したがって、削除を実行する前に、削除されたノードを 3 ノードまたは一時的な 4 ノードの一部に変換して、ノードの削除の前後で左傾の赤黒ツリーが常に完全にバランスが保たれるようにする必要があります。同様に、削除されたノードが 3 ノードまたは 4 ノードの場合、それを直接削除してもツリーの完全なバランスには影響しません。

次に、最小のノードを削除する場合を例にして、ノードの削除プロセスを説明します。

最小のノードを削除します

削除した2ノードを3ノードまたは4ノードの一部に変換する必要があるのですが、どうやって変換すればよいのでしょうか?

まず最初のケースを見てみましょう. 削除されるノードとその兄弟ノードは両方とも 2 ノードです。上記のノード挿入ロジックによれば、4 ノードの色を変換して左右 2 つの黒い子ノードが得られるので、色を変換して再度 4 ノードにすると、削除したノードが Part になります。 4 ノードの場合、この時点で削除してもツリーのバランスには影響しません。ただし、削除後はツリーのバランスには影響しませんが、赤リンクの右参照が左傾した赤黒ツリーの性質に適合しないため、修復するには左折操作が必要になります。プロセス図は次のとおりです。

LLRB_DELETE3.jpg

図ではルートノードを(1)から(2)に赤色に変換していますが、ノードを挿入する際には左右だけでなく、4ノードが3つの2ノードに変換されるのを覚えているでしょうか。ノードは黒に変換されますが、左右のノードも黒に変換されます。ノードも赤に染まり、親ノードに挿入されることを示します。ノードはルート ノードであるため、黒で表現されます(1) の図 (2) は、4 ノードを 3 つの 2 ノードに変換する一般的な状況に対応して、ルート ノードを赤に戻します。図 (3) では、色を反転して 4 ノードが得られます。これら 3 つのノードのうち。

2 番目のケースを見てみましょう。削除されるノードは 2 ノードであり、その兄弟ノードは 3 ノードです。「キー借用法」は「アルゴリズム」で導入されています。左側の子ノードが小さいノードと置き換えられます。ルート ノードを借用して 3 ノードを形成すると、ルート ノードは右側のノードから小さいノードを借用して元のノード スタイルを変更せずに維持し、右側のノードは 2 ノードになります。借用が完了すると、左側の子ノードになります。ノード (3- ノード内の小さいキー (赤いノード) を削除しても、以下に示すように、ツリーの完全なバランスには影響しません。

LLRB_DELETE2.jpg

最小のノードを削除するプロセスを整理したので、コードを書いてみます。

    /**
     * 删除最小节点
     */
    public void deleteMin() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMin(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最小节点
     */
    private Node deleteMin(Node node) {
        if (node.left == null) {
            return null;
        }

        // 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
        if (!isRed(node.left) && !isRed(node.left.left)) {
            node = moveRedLeft(node);
        }
        node.left = deleteMin(node.left);

        return balance(node);
    }

    /**
     * 从右向左借键
     */
    private Node moveRedLeft(Node node) {
        flipColors(node);
        // 兄弟节点为3-节点的情况
        if (isRed(node.right.left)) {
            node.right = rotateRight(node.right);
            node = rotateLeft(node);
        }

        return node;
    }

    /**
     * 从下到上再平衡
     */
    private Node balance(Node node) {
        if (isRed(node.right)) {
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

ノードを挿入するときは、次のように定義されていることに注意してください。基本的に、ノードを挿入するときの操作は、現在のノードの色を反転することです。 flipColors 

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

次に、ノード削除メソッドで再利用するために、次のようにこのメソッドを逆カラー処理に変更します。

    private void flipColors(Node node) {
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

基本的に、最小のノードを削除するプロセスは、現在のノードが決して 2 ノードにならないように、またノードを削除した後のツリーの不均衡を避けるために、上から下まで継続的に現在のノードを赤く染めることです。最小のノードが見つかるので、そのパス上のすべてのノードがこの操作を実行する必要があります。 

最大のノードを削除する

最大のノードを削除する場合、削除されたノードが 2 ノードでその兄弟ノードが 3 ノードである場合についてのみ説明しますが、削除されたノードとその兄弟ノードが両方とも 2 ノードである場合は非常に単純です。再度説明する必要はありません。

兄弟ノードが 3 ノードの場合でも、「キーを借用」メソッドを使用する必要があり、そのプロセスは次の図に示すとおりです。

LLRB_DELETE4.jpg

最大のノードを削除するコード ロジックは次のとおりです。

    /**
     * 删除最大节点
     */
    public void deleteMax() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMax(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最大节点
     */
    private Node deleteMax(Node node) {
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        if (node.right == null) {
            return null;
        }

        // 当前节点的右引用不是红链接且右节点不是3-节点
        if (!isRed(node.right) && !isRed(node.right.left)) {
            node = moveRedRight(node);
        }
        node.right = deleteMax(node.right);

        return balance(node);
    }

    /**
     * 从左向右借键
     */
    private Node moveRedRight(Node node) {
        flipColors(node);
        if (isRed(node.left.left)) {
            node = rotateRight(node);
        }
        return node;
    }

このうち、最小のノードを削除するには次のコードを使用します。

    private Node deleteMax(Node node) {
        // 区别于删除最小节点的操作
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        // ...
    }

このロジックを説明しましょう。左傾の赤黒ツリーでは、現在のノードの左ノードが赤の場合、次の状況が存在する可能性があります。

LLRB_DELETE5.jpg

図中の最大のノードを直接削除すると、その左側のノードが失われるため、最初に右回転を実行し、削除するノードを右の参照の位置まで回転させます。

ノードの削除

ノードを削除する方法は、最大のノードと最小のノードを削除する方法を理解すると、非常に理解しやすくなります。実際には、最大のノードを削除する方法と最小のノードを削除する方法を組み合わせたものですが、通過するノードが 3 ノードまたは一時的な 4 ノードであることも確認する必要があります。対応するノードが見つかった場合、ノードの値はノード値を右側のサブツリーの最小値に置き換え、右側のサブツリーの最小ノードを削除します。削除が完了したら、バランス修復を実行します。次のようなアノテーション情報に注意する必要があります。

    /**
     * 删除节点
     */
    public void delete(Integer key) {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = delete(root, key);
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除节点
     */
    private Node delete(Node node, Integer key) {
        if (key < node.key) {
            // 要删除的节点在左子树,保证当前节点为红色
            if (!isRed(node.left) && !isRed(node.left.left)) {
                node = moveRedLeft(node);
            }
            node.left = delete(node.left, key);
        } else {
            if (isRed(node.left)) {
                node = rotateRight(node);
            }
            // node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
            if (key.equals(node.key) && node.right == null) {
                return null;
            }
            // 要删除的节点在右子树
            if (!isRed(node.right) && !isRed(node.right.left)) {
                node = moveRedRight(node);
            }

            if (key.equals(node.key)) {
                // 找到右子树中最小的节点替换当前节点的键和值
                Node min = min(node.right);
                node.key = min.key;
                node.value = min.value;
                // 再将该右子树最小的节点移除
                deleteMin(node.right);
            } else {
                node.right = delete(node.right, key);
            }
        }

        return balance(node);
    }

学習と参照を容易にするために、完全なコードは次のとおりです。

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    private Node root;

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key > node.key) {
            node.right = put(node.right, key, value);
        } else if (key < node.key) {
            node.left = put(node.left, key, value);
        } else {
            node.value = value;
        }
        // 将3-节点的红链接右引用左旋
        if (isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        // 将4-节点两条连续的红链接左引用左旋
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        // 进行颜色转换并将红链接在树中向上传递
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

    /**
     * 删除最小节点
     */
    public void deleteMin() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMin(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最小节点
     */
    private Node deleteMin(Node node) {
        if (node.left == null) {
            return null;
        }

        // 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
        if (!isRed(node.left) && !isRed(node.left.left)) {
            node = moveRedLeft(node);
        }
        node.left = deleteMin(node.left);

        return balance(node);
    }

    /**
     * 从右向左借键
     */
    private Node moveRedLeft(Node node) {
        flipColors(node);
        // 兄弟节点为3-节点的情况
        if (isRed(node.right.left)) {
            node.right = rotateRight(node.right);
            node = rotateLeft(node);
        }

        return node;
    }

    /**
     * 删除最大节点
     */
    public void deleteMax() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMax(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最大节点
     */
    private Node deleteMax(Node node) {
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        if (node.right == null) {
            return null;
        }

        // 当前节点的右引用不是红链接且右节点不是3-节点
        if (!isRed(node.right) && !isRed(node.right.left)) {
            node = moveRedRight(node);
        }
        node.right = deleteMax(node.right);

        return balance(node);
    }

    /**
     * 从左向右借键
     */
    private Node moveRedRight(Node node) {
        flipColors(node);
        if (isRed(node.left.left)) {
            node = rotateRight(node);
        }
        return node;
    }

    /**
     * 删除节点
     */
    public void delete(Integer key) {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = delete(root, key);
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除节点
     */
    private Node delete(Node node, Integer key) {
        if (key < node.key) {
            // 要删除的节点在左子树,保证当前节点为红色
            if (!isRed(node.left) && !isRed(node.left.left)) {
                node = moveRedLeft(node);
            }
            node.left = delete(node.left, key);
        } else {
            if (isRed(node.left)) {
                node = rotateRight(node);
            }
            // node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
            if (key.equals(node.key) && node.right == null) {
                return null;
            }
            // 要删除的节点在右子树
            if (!isRed(node.right) && !isRed(node.right.left)) {
                node = moveRedRight(node);
            }

            if (key.equals(node.key)) {
                // 找到右子树中最小的节点替换当前节点的键和值
                Node min = min(node.right);
                node.key = min.key;
                node.value = min.value;
                // 再将该右子树最小的节点移除
                deleteMin(node.right);
            } else {
                node.right = delete(node.right, key);
            }
        }

        return balance(node);
    }

    /**
     * 获取最小节点
     */
    private Node min(Node node) {
        if (node.left == null) {
            return node;
        }
        return min(node.left);
    }

    /**
     * 从下到上再平衡
     */
    private Node balance(Node node) {
        if (isRed(node.right)) {
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

    /**
     * 左旋
     */
    private Node rotateLeft(Node node) {
        Node newNode = node.right;
        node.right = newNode.left;
        newNode.left = node;

        newNode.color = node.color;
        node.color = RED;

        return newNode;
    }

    /**
     * 右旋
     */
    private Node rotateRight(Node node) {
        Node newNode = node.left;
        node.left = newNode.right;
        newNode.right = node;

        newNode.color = node.color;
        node.color = RED;

        return newNode;
    }

    /**
     * 颜色转换
     */
    private void flipColors(Node node) {
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

    /**
     * 判断节点是否为红色
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return false;
        } else {
            return node.color == RED;
        }
    }
}

巨人の肩

  • 『アルゴリズム 第 4 版』: 第 3.3 章 バランス検索ツリー

  • ウィキペディア - バランス二分探索ツリー

  • ウィキペディア - AVL ツリー

  • Zhihu - AVL の木と赤黒の木についての考え

  • ウィキペディア - 左に傾いた赤黒の木

  • LeetCode - エントリーから導入までの赤黒ツリー

  • Zhihu - 「アルゴリズム」の左傾赤黒ツリー (LLRB) 削除操作のコードの各行を誰かが説明してもらえますか?

著者: JD Logistics 王一龍

出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください

Bilibiliは2度クラッシュ、テンセントの「3.29」第1レベル事故…2023年のダウンタイム事故トップ10を振り返る Vue 3.4「スラムダンク」リリース MySQL 5.7、莫曲、李条条…2023年の「停止」を振り返る 続き” (オープンソース) プロジェクトと Web サイトが 30 年前の IDE を振り返る: TUI のみ、明るい背景色... Vim 9.1 がリリース、 Redis の父 Bram Moolenaar に捧げ、「ラピッド レビュー」LLM プログラミング: Omniscient 全能&&愚かな 「ポスト・オープンソースの時代が来た。ライセンスの有効期限が切れ、一般ユーザーにサービスを提供できなくなった。チャイナ ユニコムブロードバンドが突然アップロード速度を制限し、多くのユーザーが苦情を申し立てた。Windows 幹部は改善を約束した: Make the Start」メニューもまた素晴らしいです。 パスカルの父、ニクラス・ヴィルトが亡くなりました。
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10575882
おすすめ