この記事では、赤黒ツリーの古典的な実装について説明します。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ノードに新規ノードを挿入する場合に注目してみましょう。新しいノードを 4 ノードに挿入する場合、以下に示すように、まず 4 ノードを 3 つの 2 ノードに変換してから、挿入操作が 2 ノードの 1 つで実行されます。黄色のノードは、は新しく挿入されたノードです。ノードは、4 ノードへの挿入の 4 つの状況に対応します。
再びケース (4) を例として取り上げ、値 34 のノードを 2-3-4 検索ツリーに挿入してみましょう。
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 探索ツリーの例を赤黒ツリーに変換すると、下図のようになります。赤いノードも赤く染まります):
次の特性を満たします。
-
ノードの色は赤または黒です
-
ルートノードは黒です
-
葉のノード(ヌルノード)は黒になります(図ではヌルノードは描画されません)
-
赤いノードの 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 ノードに変換されます。
3 ノードの挿入
3 ノードを挿入するには、左スキュー 3 ノードと右スキュー 3 ノードの 2 つのケースで説明する必要があります。
-
左に歪んだ 3 ノードの左または右のノードを挿入するには、次の図に示すように、それを 4 ノードに変換する必要があります。
-
右に歪んだ 3 ノードの左側または右側のノードを挿入するには、次の図に示すように、それを 4 ノードに変換する必要があります。
4 ノードの挿入
上記 2 つのケースでは、「上向きのマージ」は発生しません。4 ノードが挿入された場合は、3 つの 2 ノードに分解し、2 ノードの「ルート ノード」をその親にマージする必要があります。ノード (その場合)、ケースバイケースで議論する必要もあります。
-
赤いノードを 4 つのノードの左側に挿入します。
-
赤いノードを 4 つのノードの右側に挿入します。
現時点では、新しいノードを挿入するすべての状況について説明しました。ソース コードを見て、コメントに注目してみましょう。 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 は赤です
上の図に示されているローカルの赤黒ツリーは、ノードが削除される前に黒バランスがとられているため、ルート ノードからサブツリーまでの黒ノードの数は 3 になるはずです。ノードが削除されると、ルート ノードから a サブツリーまでの黒いノードの数は 2 になり (図に示すように)、ルート ノードから c サブツリーまでのパス上の黒いノードの数は 2 になります。
反転・左回転処理後、元のルートノードは赤く染まりますが、「新しいルートノード」からサブツリーまでの黒いノードは2個のままで3個に増えていないので、黒いノードはまだアンバランス; c サブツリーへ パス上の黒ノードの数は 2 で、操作前と同じであり、赤黒ツリーのプロパティには影響しません。
色を反転して左折すると、ケース 1 がケース 2、ケース 3、ケース 4 となって処理が継続されますが、これは実際には理解するのが難しくありません。赤黒木の性質 5 と組み合わせると、反転と左折操作により、ルート ノードは特定のサブツリー パスに移動します。黒いノードの数は変更されておらず、削除操作を実行したときにパスから 1 つの黒いノードが削除されているため、「ルート ノード」からのパスは" 特定のサブツリーには、ブラック バランスを満たすために黒いノードを追加する必要があります。重要な点は、ルート ノードから各サブツリーまでのパス上の黒いノードの数は変更できず、プロパティ 5 が true である必要があるということです。ノードの削除と変換の前後。
ケース 2: x の兄弟ノード w は黒で、w の 2 つの子ノードは両方とも黒です。
図中の赤と黒のグラデーション色は、ノードが黒ノードまたは赤ノードである可能性があることを示していることに注意してください。この場合、兄弟ノードの子ノードは両方とも黒で、ノードが削除される前のルート ノードからサブツリー a およびサブツリー c へのパス上のルート ノードの数は両方とも 2 です (不確実な色のルート ノードは無視します)。 。ノードを削除した後、ルート ノードから a サブツリーまでのパス上の黒いノードの数は 1 になります。w ノードを赤に染めると、ルート ノードから c サブツリーまでのパス上の黒いノードの数は次のようになります。 1 に減らされ、ルート ノードからピクチャまで図のすべてのサブツリー パス上の黒ノードの数は同じです。これは、図のローカルの赤黒ツリーがブラック バランスの条件を満たしていることを意味します。現時点では、x をその親ノードにポイントし、修復プロセスを (その間) 繰り返す必要があります。
ケース 3: x の兄弟ノード w は黒、w の左ノードは赤、w の右ノードは黒です。
この場合、ルート ノードからサブツリー e までのパス上の黒いノードの数は 2 です。w と w の色を交換した後、左回転と右回転の操作は、赤黒ツリーのプロパティには影響しません。このようにして、状況 3 はシナリオ 4 に変換されます。
ケース 4: x の兄弟ノード w は黒で、w の右側のノードは赤です
ケース 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 転載の際は出典を明記してください