古典的な赤黒木についての深い理解 | JD Logistics Technical Team

この記事では、赤黒ツリーの古典的な実装について説明します。Java での赤黒ツリーの実装では、古典的な赤黒ツリーが使用されます。前回の記事では、左傾の赤黒木について紹介しましたが、比較的単純ですが、今回は回転などの基礎知識は省略しますので、前回の記事を読んでから読んでください。この記事の内容のほとんどは「アルゴリズム入門」と Java で赤黒ツリーを実装するためのソース コードに言及していますので、辛抱強く読んでいただければ幸いです。

本文を始める前に、次の質問を見てみましょう。

  • 赤黒ツリーが AVL ツリーよりも広く使用されているのはなぜですか?

赤黒ツリーと AVL ツリーについては、「最悪の場合、AVL ツリーと赤黒ツリーの検索時間は対数的になります。係数は赤黒ツリーの方が高くなりますが、本質的な違いはありません。 AVL ツリーの最も致命的な点は、ノードを削除するときの回転数が対数であるのに対し、赤黒ツリーでは最大 3 回転しか必要ないため、赤黒ツリーが AVL ツリーよりも広く使用されることです。 「より多く」の観点から見ると、実際にはこれが根本的な理由ではありません。根本的な理由は、挿入操作と削除操作が任意のシーケンスで混在している場合、赤黒ツリーの償却時間計算量が O(1) に留まるということです。 、AVL ツリーの償却時間計算量は O(logn) です

古典的な赤黒ツリーは2-3-4 検索ツリーと同型であり、左寄りの赤黒ツリー (2-3 ツリー) の実装と比較して、赤黒ツリーのバランスを維持する際のオーバーヘッドが少なくなります。黒い木。以下では、古典的な赤黒ツリーを略して赤黒ツリーと呼びます。 

2-3-4 検索ツリー

2-3-4 検索ツリーは 2-3 検索ツリーに 4 つのノードを追加します。4 つのノードは前の記事で紹介されています。まず 2-3-4 検索ツリーがどのようなものかを見てみましょう:

2-3-4の木.jpg

2ノードや3ノードに新規ノードを挿入する場合についてはここでは詳しく説明しませんので、4ノードに新規ノードを挿入する場合に注目してみましょう。新しいノードを 4 ノードに挿入する場合、以下に示すように、まず 4 ノードを 3 つの 2 ノードに変換してから、挿入操作が 2 ノードの 1 つで実行されます。黄色のノードは、は新しく挿入されたノードです。ノードは、4 ノードへの挿入の 4 つの状況に対応します。

2-3-4の木2.jpg

再びケース (4) を例として取り上げ、値 34 のノードを 2-3-4 検索ツリーに挿入してみましょう。

2-3-4 木 3.jpg

4 ノードを 3 つの 2 ノードに変換し、新しいノード 34 の挿入が完了したら、上の図に示すように、「ルート ノード 25」を親ノードに挿入する必要があります。これは、前回の記事の2-3探索木の話と基本的に同じで、仮の5ノードを継続的に分解し、元の4ノードを3つの2ノードのルートノードに分解して挿入する必要があります。ノード内で2ノードや3ノードに出会うまでは分解する必要のないノードに変換し、最終的にルートノードに挿入して5ノードにすると、ノードも分解して挿入する必要があります。完了後は、ツリーの高さに 1 を加えたものになります

2-3-4 ツリーの挿入操作により、ツリー自体への変更がローカルに行われます。関連するノードと参照に加えて、ツリーの他の部分を変更したりチェックしたりする必要はありません。これらのローカル変換は、グローバルな順序付けとツリーの順序付け バランス: 挿入プロセス中、2-3-4 ツリーは常に完全にバランスのとれた 2 分木になります

古典的な赤黒の木

古典的な赤黒ツリーは 2-3-4 探索ツリーと同型であり、2-3-4 探索ツリーの例を赤黒ツリーに変換すると、下図のようになります。赤いノードも赤く染まります):

古典的な赤黒の木.jpg

次の特性を満たします。

  • ノードの色は赤または黒です

  • ルートノードは黒です

  • 葉のノード(ヌルノード)は黒になります(図ではヌルノードは描画されません)

  • 赤いノードの 2 つの子ノードは黒です (連続した赤いノードは表示できません)

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

黒バランスの性質については、左傾の赤黒木の説明でお話しましたが、ここでもう一度説明します、2-3-4 の木は常に完璧なバランスを保つことができるので、どの葉からの距離もノードとルート ノードは等しい。赤黒ツリーも2-3-4 検索ツリーであり、その中の黒いリンクは 2-3-4 検索ツリーの通常のリンクなので、赤黒ツリー内の黒リンクも完全にバランスが取れている必要があるため、リーフ ノードからルート ノードまでのパス上の黒ノードの数は同じでなければなりません。

以下では、図と Java のソース コードを組み合わせて、赤黒ツリーでのノードの挿入と削除を説明します。 TreeMap 

ノードの定義

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;

        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
        // ...
    }

色情報と左右の子ノードへの参照に加えて、ノード定義では親ノードへの参照も追加していることがわかります。 parent 

ノードの挿入

2ノードの挿入

ノードを 2 ノードに直接挿入するのは非常に簡単で、次の図に示す 2 つのケースでは、2 ノードが 3 ノードに変換されます。

古典的な赤黒の木 2.jpg

3 ノードの挿入

3 ノードを挿入するには、左スキュー 3 ノードと右スキュー 3 ノードの 2 つのケースで説明する必要があります。

  1. 左に歪んだ 3 ノードの左または右のノードを挿入するには、次の図に示すように、それを 4 ノードに変換する必要があります。

古典的な赤黒の木 3.jpg

  1. 右に歪んだ 3 ノードの左側または右側のノードを挿入するには、次の図に示すように、それを 4 ノードに変換する必要があります。

古典的な赤黒の木 4.jpg

4 ノードの挿入

上記 2 つのケースでは、「上向きのマージ」は発生しません。4 ノードが挿入された場合は、3 つの 2 ノードに分解し、2 ノードの「ルート ノード」をその親にマージする必要があります。ノード (その場合)、ケースバイケースで議論する必要もあります。

  1. 赤いノードを 4 つのノードの左側に挿入します。

古典的な赤黒の木 5.jpg

  1. 赤いノードを 4 つのノードの右側に挿入します。

古典的な赤黒の木 6.jpg

現時点では、新しいノードを挿入するすべての状況について説明しました。ソース コードを見て、コメントに注目してみましょう。 TreeMap 

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 插入第一个节点
        if (t == null) {
            compare(key, key);

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // 根据比较器找到插入节点的位置
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 根据大小关系添加新的节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        // *插入之后修复平衡操作*
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

重点を置く必要がある方法: fixAfterInsertion 

    private void fixAfterInsertion(Entry<K,V> x) {
        // 新插入的节点指定为红色
        x.color = RED;

        // 如果非空非根节点且有连续的红色节点出现,需要不断地修复平衡
        while (x != null && x != root && x.parent.color == RED) {
            // 插入节点后,出现连续红色的节点的位置在左侧
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                // 插入的是4-节点,对应插入4-节点的情况1
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    // 反色处理
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    // 处理父节点的父节点(因为该节点为红色,可能会发生向上合并的操作)
                    x = parentOf(parentOf(x));
                } else {
                    // 如下步骤对应插入3-节点的情况1
                    // 插入的是3-节点的右节点
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        // 左旋父节点
                        rotateLeft(x);
                    }
                    // 现在转换成了插入位置为3-节点的左节点,父节点染成黑色
                    setColor(parentOf(x), BLACK);
                    // 父节点的父节点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 右旋父节点的父节点,转换成4-节点
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                // 插入节点后,出现连续红色的节点的位置在右侧
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                // 插入的是4-节点,对应插入4-节点的情况2
                if (colorOf(y) == RED) {
                    // 反色处理
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    // 处理父节点的父节点(因为该节点为红色,可能会发生向上合并的操作)
                    x = parentOf(parentOf(x));
                } else {
                    // 如下步骤对应插入3-节点的情况2
                    // 插入的是3-节点的左节点
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        // 右旋父节点
                        rotateRight(x);
                    }
                    // 转换成了插入位置为3-节点的右节点,父节点为黑色
                    setColor(parentOf(x), BLACK);
                    // 父节点的父节点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 左旋父节点的父节点,转换成4-节点
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        // 根节点始终为黑色
        root.color = BLACK;
    }

*ノードの削除

ノードの削除は、赤黒ツリーで最も複雑な実装です。3 ノードまたは 4 ノードを削除する場合 (赤ノードが削除されます)、ノードを削除しても赤黒ツリーのバランスには影響しません。 ; 2ノードのノードを削除する場合は、ノードを削除した後に赤黒ツリーのバランスを上方向に修正する必要があります。これが最も複雑な部分でもあります。「アルゴリズム入門」で説明した状況に従って分析してみましょう"。削除プロセスを理解するには、赤黒ツリーのプロパティをよく理解しておく必要があります。(特にプロパティ 5: 赤黒ツリーは常に黒のバランスが保たれています)、この本では 2 つの要素を削除した後の 4 つの状況について説明しています。 -node. 赤黒ツリーのリバランスが開始されるノードは x であり (x ノードは、特定の 2 ノードが削除された後、この位置にノードが接続されます)、その兄弟ノードは次のように w であることに注意してください。

ケース 1: x の兄弟ノード w は赤です

削除.jpg

上の図に示されているローカルの赤黒ツリーは、ノードが削除される前に黒バランスがとられているため、ルート ノードからサブツリーまでの黒ノードの数は 3 になるはずです。ノードが削除されると、ルート ノードから a サブツリーまでの黒いノードの数は 2 になり (図に示すように)、ルート ノードから c サブツリーまでのパス上の黒いノードの数は 2 になります。

反転・左回転処理後、元のルートノードは赤く染まりますが、「新しいルートノード」からサブツリーまでの黒いノードは2個のままで3個に増えていないので、黒いノードはまだアンバランス; c サブツリーへ パス上の黒ノードの数は 2 で、操作前と同じであり、赤黒ツリーのプロパティには影響しません。

色を反転して左折すると、ケース 1 がケース 2、ケース 3、ケース 4 となって処理が継続されますが、これは実際には理解するのが難しくありません。赤黒木の性質 5 と組み合わせると、反転と左折操作により、ルート ノードは特定のサブツリー パスに移動します。黒いノードの数は変更されておらず、削除操作を実行したときにパスから 1 つの黒いノードが削除されているため、「ルート ノード」からのパスは" 特定のサブツリーには、ブラック バランスを満たすために黒いノードを追加する必要があります。重要な点は、ルート ノードから各サブツリーまでのパス上の黒いノードの数は変更できず、プロパティ 5 が true である必要があるということです。ノードの削除と変換の前後

ケース 2: x の兄弟ノード w は黒で、w の 2 つの子ノードは両方とも黒です。

削除2.jpg

図中の赤と黒のグラデーション色は、ノードが黒ノードまたは赤ノードである可能性があることを示していることに注意してください。この場合、兄弟ノードの子ノードは両方とも黒で、ノードが削除される前のルート ノードからサブツリー a およびサブツリー c へのパス上のルート ノードの数は両方とも 2 です (不確実な色のルート ノードは無視します)。 。ノードを削除した後、ルート ノードから a サブツリーまでのパス上の黒いノードの数は 1 になります。w ノードを赤に染めると、ルート ノードから c サブツリーまでのパス上の黒いノードの数は次のようになります。 1 に減らされ、ルート ノードからピクチャまで図のすべてのサブツリー パス上の黒ノードの数は同じです。これは、図のローカルの赤黒ツリーがブラック バランスの条件を満たしていることを意味します。現時点では、x をその親ノードにポイントし、修復プロセスを (その間) 繰り返す必要があります。

ケース 3: x の兄弟ノード w は黒、w の左ノードは赤、w の右ノードは黒です。

削除3.jpg

この場合、ルート ノードからサブツリー e までのパス上の黒いノードの数は 2 です。w と w の色を交換した後、左回転と右回転の操作は、赤黒ツリーのプロパティには影響しません。このようにして、状況 3 はシナリオ 4 に変換されます。

ケース 4: x の兄弟ノード w は黒で、w の右側のノードは赤です

削除4.jpg

ケース 4 では、ルート ノードからサブツリーまでのパス上の黒ノードの数は 1 ですが、黒のバランスを達成するには 2 に増やす必要があります。w ノードと x.parent ノードの色を交換し、w.right ノードを黒に染めてから、x.parent ノードに対して左折操作を実行すると、ノードからのパス上の黒いノードの数が減ります。 a サブツリーへのルート ノードは 2 に増加し、他の c、d、e、f サブツリーへのパス上の黒ノードの数は変更されず、赤黒ツリーのプロパティは破壊されません。操作が完了すると、 x をルート ノードにポイントし、ループを終了します。これは、この時点で赤黒ツリーがすべてのプロパティを満たしているためです。

2 ノードを削除した後の 4 つの異なる状況を分析したので、染色と回転操作を通じて赤黒ツリーの黒バランスを再度調整する必要があります。以下のメソッドの実装を見てみましょ。注釈情報に注意してください。 TreeMap  remove 

    /**
     * 删除节点
     */
    public V remove(Object key) {
        // 找到要删除的节点
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        // 执行删除操作
        deleteEntry(p);
        return oldValue;
    }

次に、削除操作の実行ロジックを見てみましょう。

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // 如果该节点有左右子节点,则它是一个要被删除的内部节点
        // 那么需要找到该节点的“后继节点”,找到之后删除任务则变为了删除该后继节点
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        }

        // 执行完上述代码块后,被删除节点要么为叶子节点,要么为只有左子树或右子树的节点
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        // 被删除节点有左子树或右子树
        if (replacement != null) {
            // 修正节点的父节点引用关系
            replacement.parent = p.parent;
            // 被删除节点为根节点的关系
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                // 被删除的节点是左节点
                p.parent.left  = replacement;
            else
                // 被删除的节点是右节点
                p.parent.right = replacement;

            // 断开被删除节点的所有引用关系
            p.left = p.right = p.parent = null;

            // 如果被删除节点为2-节点,需要执行再平衡操作
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) {
            // 红黑树只有一个节点的情况
            root = null;
        } else {
            // 删除叶子节点,如果该节点为2-节点那么需要再平衡修复
            if (p.color == BLACK)
                fixAfterDeletion(p);

            // 断开引用关系
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

    // 获取 t 节点的后继节点
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            // 右子树不为空,则右子树中最小的节点为该节点的后继节点
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            // ...
        }
    }

fixAfterDeletion 削除後にメソッドのバランスを再調整するには、全員がアノテーション情報に注意を払う必要があります。

    private void fixAfterDeletion(Entry<K,V> x) {
        // 非根节点和当前节点是黑色的才需要修复平衡
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));

                // 情况 1
                if (colorOf(sib) == RED) {
                    // 反色处理:兄弟节点染黑,父节点染红
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    // 左旋父节点
                    rotateLeft(parentOf(x));
                    // 新的兄弟节点
                    sib = rightOf(parentOf(x));
                }

                // 情况 2
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    // 将兄弟节点染红后,x指向父节点引用,继续修复平衡
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    // 情况 3
                    if (colorOf(rightOf(sib)) == BLACK) {
                        // 交换颜色:兄弟节点左节点染黑,兄弟节点染红
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        // 右旋兄弟节点
                        rotateRight(sib);
                        // 新的兄弟节点
                        sib = rightOf(parentOf(x));
                    }
                    // 情况 4
                    // 交换兄弟节点和父节点的颜色
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    // 将兄弟节点的右节点由红染黑
                    setColor(rightOf(sib), BLACK);
                    // 左旋父节点
                    rotateLeft(parentOf(x));
                    // 满足红黑树的性质,指向根节点循环结束
                    x = root;
                }
            } else {
                // 以下是镜像操作
                Entry<K,V> sib = leftOf(parentOf(x));

                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib = leftOf(parentOf(x));
                }

                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib = leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(sib), BLACK);
                    rotateRight(parentOf(x));
                    x = root;
                }
            }
        }

        // 情况 2,x 为红色节点时跳出循环,需要将 x 节点染黑
        // 因为红黑树中不存在连续的红色节点
        setColor(x, BLACK);
    }

赤黒ツリーで delete メソッドを実行する場合の時間計算量はどれくらいですか? n 個のノードを持つ赤黒ツリーの高さは logn です。fixAfterDeletion メソッドが呼び出されない場合、複雑さは O(logn) です。ケース 1、3、および 4 では、それぞれが一定数の色の変更と最大 3 回の回転を実行します。終了します。ケース 2 の場合のみ、バランスを繰り返し修復できます。ポインタも最大 O(logn) 回上昇し、回転操作がないため、複雑さはO(logn) で、最大 3 回転です。 , したがって、赤黒ツリーの削除方法の時間計算量は O(logn) です。 fixAfterDeletion  fixAfterDeletion 


巨人の肩

  • 「アルゴリズム入門」: 第 13 章 赤黒ツリー

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

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

  • Blog Garden - 赤と黒の木の削除

  • 著者: 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/10584600
おすすめ