| 赤黒の木 | |||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| タイプ | 木 | ||||||||||||||||||||||||||||
| 発明した | 1978 | ||||||||||||||||||||||||||||
| 発明者 | レオニダス・J・ギバスとロバート・セジウィック | ||||||||||||||||||||||||||||
| ビッグオー記法による計算量の表し方 | |||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||

コンピュータサイエンスにおいて、赤黒木は、順序付けられた情報の高速な格納と取得で知られる自己平衡二分探索木データ構造です。赤黒木のノードは、通常赤と黒で描かれる追加の「色」ビットを保持しており、これにより木が常にほぼ平衡していることが保証されます。[ 1 ]
ツリーが変更されると、新しいツリーは再配置され、「再塗装」されて、最悪の場合にツリーのバランスが崩れる度合いを制限する色付け特性が復元されます。これらの特性は、再配置と再塗装を効率的に実行できるように設計されています。
(再)バランス調整は完璧ではないが、検索を保証する時間、はツリー内のエントリ数です。挿入と削除の操作、ツリーの再配置と色の変更も実行されます。時間。[ 2 ] [ 3 ]
各ノードの色を追跡するには、ノードごとに1ビットの情報しか必要ありません。これは、色は2色しかないためです(一部のプログラミング言語に存在するメモリアライメントにより、実際のメモリ使用量は異なる場合があります)。このツリーには、赤黒木であることを示すその他のデータは含まれていないため、メモリ使用量は従来の(色付けされていない)二分探索木とほぼ同じです。場合によっては、追加された1ビットの情報をメモリコストを追加することなく保存できます。
1972年、ルドルフ・バイヤー[ 4 ]は、Bツリーの4次ケースであるデータ構造を発明しました。これらのツリーは、ルートからリーフまでのすべてのパスを同じ数のノードで保持し、完全にバランスの取れたツリーを作成します。ただし、これらは二分探索木ではありませんでした。バイヤーは論文の中でこれらを「対称二分Bツリー」と呼び、後に2-3-4ツリーまたは2-3ツリーとして普及しました。[ 5 ]
1978年の論文「バランスのとれたツリーのための二色フレームワーク」[ 6 ]で、レオニダス・J・ギバスとロバート・セジウィックは、対称二分Bツリーから赤黒ツリーを導出した。[ 7 ] 「赤」という色は、著者らがゼロックスPARCで働いていたときに利用できたカラーレーザープリンターで生成される色の中で最も見栄えの良い色だったため選ばれた。[ 8 ]ギバスの別の回答では、ツリーを描くために利用できる赤と黒のペンがあったためだと述べている。[ 9 ]
1993年、アルネ・アンダーソンは挿入と削除の操作を簡素化するために右傾斜ツリーの概念を導入した。[ 10 ]
1999年、クリス・オカサキは挿入操作を純粋関数型にする方法を示した。そのバランス関数は、4つの不均衡なケースと1つのデフォルトの均衡ケースのみを処理する必要があった。[ 11 ]
元のアルゴリズムでは 8 つの不均衡なケースが使用されていましたが、Cormen ら (2001) はそれを 6 つの不均衡なケースに減らしました。[ 1 ] Sedgewick は、挿入操作をJavaの 46 行だけで実装できることを示しました。[ 12 ] [ 13 ] 2008 年に、Sedgewick は、挿入および削除操作を簡素化する Andersson のアイデアを活用して、左寄りの赤黒木を提案しました。Sedgewick は当初、2 つの子が赤であるノードを許可し、彼の木を 2-3-4 木に似せていましたが、後にこの制限が追加され、新しい木は 2-3 木に似せられるようになりました。Sedgewick は挿入アルゴリズムをわずか 33 行で実装し、元の 46 行のコードを大幅に短縮しました。[ 14 ] [ 15 ]
ノードの黒深度は、ルートからそのノードまでの黒ノードの数(つまり、黒祖先の数)として定義されます。赤黒木の黒高は、ルートから葉までの任意のパスにある黒ノードの数であり、要件 4により一定です(または、任意の葉ノードの黒深度として定義することもできます)。[ 16 ]: 154-165 ノードの黒高は、そのノードをルートとする部分木の黒高です。この記事では、ヌルノードの黒高は 0 に設定する必要があります。これは、例の図が示すように、その部分木が空であり、その木の高さも 0 であるためです。
二分探索木に課せられる要件に加えて、赤黒木は以下を満たさなければならない。 [ 17 ]
Cormen ら[ 17 ]は「ルートが黒であること」を 5 番目の要件として主張していますが、Mehlhorn と Sanders [ 16 ]や Sedgewick と Wayne [ 15 ]はそう主張していません。 : 432–447ルートは常に赤から黒に変更できるため、このルールは解析にほとんど影響を与えません。また、再帰アルゴリズムと証明をわずかに妨げるため、この記事でもこのルールは省略しています。
例えば、黒ノードのみで構成される完全二分木はすべて赤黒木である。
検索やツリー走査などの読み取り専用操作は、いずれの要件にも影響を与えません。対照的に、変更操作の挿入と削除は要件1と2を容易に維持しますが、他の要件に関しては、要件3の違反(赤違反、または要件4の違反と呼ばれる黒人に対する暴力。
この要件は、赤黒木の重要な特性、すなわち根から最も遠い葉までのパスの長さが根から最も近い葉までのパスの長さの2倍以下であることを強制します。その結果、木は高さバランスが取れます。値の挿入、削除、検索などの操作には、高さに比例する最悪の場合の時間が必要となるため、木の高さの上限により、赤黒木は最悪の場合、つまり木の数の対数で効率的になります。エントリ、つまり(これは、 AVL ツリーやB ツリーなど、すべての自己平衡木に共通する性質ですが、通常の二分探索木には当てはまりません。)数学的な証明については、「境界の証明」のセクションを参照してください。
赤黒木は、すべての二分探索木と同様に、要素への非常に効率的な逐次アクセス(例えば、左-根-右の順での順走査)を可能にします。しかし、根から葉への走査による漸近的に最適な直接アクセスもサポートしており、結果として、検索時間。

赤黒木は、次数4のB木である2-3-4木と構造が似ています。 [ 18 ] 2-3-4木では、各ノードは1~3個の値を持ち、2~4個の子を持つことができます。これらの2-3-4ノードは、図1に示すように、赤黒木の黒ノードと赤子グループに対応します。3ノードには2つの同等の表現があるため、これは1対1の対応ではありません。赤子は左または右のどちらかに位置する可能性があります。左寄りの赤黒木バリアントは、左子表現のみを許可することで、この関係を正確に1対1にします。すべての2-3-4ノードには対応する黒ノードがあるため、赤黒木の不変量4は、2-3-4木の葉がすべて同じレベルにあると言うことと同等です。
構造的な類似性にもかかわらず、赤黒木に対する操作はB木よりも経済的である。B木は可変長のベクトルの管理を必要とするが、赤黒木は単純な二分木である。[ 19 ]
赤黒木は、挿入時間、削除時間、検索時間に関して最悪ケース保証を提供します。このため、リアルタイムアプリケーションなどの時間制約のあるアプリケーションで価値があるだけでなく、最悪ケース保証を提供する他のデータ構造の構成要素としても価値があります。たとえば、計算幾何学で使用される多くのデータ構造は赤黒木に基づいており、Linuxカーネルの完全公平スケジューラとepollシステムコールは赤黒木を使用しています。[ 20 ] [ 21 ] AVL木は、これをサポートする別の構造です。検索、挿入、削除。AVL ツリーは赤黒色にすることができ、したがって赤黒ツリーのサブセットです。AVL の最悪ケースの高さは赤黒ツリーの最悪ケースの高さの 0.720 倍なので、AVL ツリーはより厳密にバランスが取れています。Ben Pfaff による 79 回の実行で現実的なテスト ケースを使用したパフォーマンス測定では、AVL と RB の比率は 0.677 ~ 1.077、中央値は 0.947、幾何平均は0.910 であることがわかりました。[ 22 ] WAVL ツリーのパフォーマンスは、AVL ツリーと赤黒ツリーの中間に位置します。
赤黒木は関数型プログラミングにおいても特に有用であり、最も一般的な永続データ構造の1つとして、変異後も以前のバージョンを保持できる連想配列やセットの構築に使用されます。赤黒木の永続バージョンには時間に加えて、挿入または削除ごとにスペースが必要になります。
2-3-4木には必ず、同じ順序のデータ要素を持つ対応する赤黒木が存在します。2-3-4木における挿入と削除の操作は、赤黒木における色反転と回転に相当します。このため、2-3-4木は赤黒木の背後にある論理を理解する上で重要なツールとなります。実際、2-3-4木は実用上あまり使われないにもかかわらず、多くの入門アルゴリズムの教科書では、赤黒木の直前に2-3-4木を紹介しています。
2008 年、セジウィックは、実装における以前は指定されていなかった自由度を排除することにより、左傾斜赤黒木[ 23 ]と呼ばれる、より単純なバージョンの赤黒木を導入しました。LLRB は、挿入および削除時を除き、すべての赤いリンクが左に傾かなければならないという追加の不変条件を維持します。赤黒木は、任意の操作シーケンスに対して、2-3 木[ 24 ]または 2-3-4 木[ 23 ]のいずれかに等長にすることができます。2-3-4 木の等長性は、1978 年にセジウィックによって記述されました。[ 6 ] 2-3-4 木では、等長性は、2 つの子ノードの赤色が子ノードから親ノードに移動することに対応する分割に対応する「色の反転」によって解決されます。
高速検索に最適化されたツリーの一種であるタンゴツリーの元の説明では、データ構造の一部として赤黒木が具体的に使用されています。[ 25 ]
Java 8以降、 HashMapは変更され、ハッシュコードが衝突する異なる要素を格納するためにLinkedListを使用する代わりに、赤黒木が使用されるようになりました。これにより、そのような要素を検索する時間計算量が改善されます。にどこはハッシュが衝突する要素の数です。[ 26 ]
赤黒木に対する検索やツリー走査などの読み取り専用操作は、二分探索木で使用される操作と何ら変わりません。なぜなら、すべての赤黒木は単純な二分探索木の特殊なケースだからです。ただし、挿入や削除の直後の結果は、赤黒木の特性を損なう可能性があります。その特性を回復させる操作は再平衡化と呼ばれ、赤黒木が自己平衡化するようにします。 再調整(つまり色の変更)の最悪ケースの時間計算量はそして平均, [ 27 ] : 310 [ 16 ] : 158ただし、これらは実際には非常に高速です。さらに、再バランスには 3 つのツリー回転(挿入の場合は 2 つ)を超えることはありません[ 28 ]。
これはC言語における挿入と削除の実装例です。以下にrotate_subtree、挿入と削除の例で使用されるデータ構造とヘルパー関数を示します。
typedef enum Color : char {黒、赤}色;typedef enum Direction : char {左、右}方向;// 赤黒木のノードtypedef struct Node {struct Node * parent ; // ルートノードの場合は nullユニオン{// 共用体なので、->left/->right または ->child[0]/->child[1] を使用できます構造体{struct Node * left ;struct Node * right ;};struct Node * child [ 2 ];};色色;整数キー;}ノード;typedef struct {struct Node * root ;}木;static Direction direction ( const Node * N ) {return N == N -> parent -> right ? RIGHT : LEFT ;}Node * rotate_subtree ( Tree * tree , Node * sub , Direction dir ) {ノード* sub_parent = sub -> parent ;Node * new_root = sub -> child [ 1 - dir ]; // 1 - dir は逆方向Node * new_child = new_root -> child [ dir ];sub -> child [ 1 - dir ] = new_child ;if ( new_child ) {new_child -> parent = sub ;}new_root -> child [ dir ] = sub ;new_root -> parent = sub_parent ;sub -> parent = new_root ;if ( sub_parent ) {sub_parent -> child [ sub == sub_parent -> right ] = new_root ;}それ以外{tree -> root = new_root ;}return new_root ;}
この提案では、挿入と削除の両方(ごく単純なケースは除く)を、ノード、エッジ、色の6つの組み合わせ(ケース)に分類しています。挿入と削除の両方について、ルートとループに黒レベルを1つ近づけるケースが正確に1つ含まれており、残りの5つのケースはツリーのバランスを独自に調整します。より複雑なケースは図に示されています。
U == NULL || U->color == BLACK // considered blackU != NULL && U->color == RED // not considered blackU == NULL。この場合、どちらの場合もU->colorは変更されません(短絡評価を参照)。(このコメントは要件2considered blackに準拠しています。)if提案[ 29 ]が実現すれば、関連する声明の発生頻度ははるかに少なくなるはずです。挿入は、新しい (NULL ではない) ノード、たとえばN を、順序付けされた先行ノードのキーが新しいノードのキーよりも小さく、さらにその新しいノードのキーが順序付けされた後続ノードのキーよりも小さい NULL ノードの二分探索木内の位置に配置することから始まります。 (多くの場合、この配置は挿入操作の直前の木内での検索の結果であり、ノードと方向 で構成されます。) 新しく挿入されたノードは一時的に赤色に着色され、すべてのパスに以前と同じ数の黒いノードが含まれるようになります。 しかし、その親、たとえばPも赤色の場合、この操作によって赤色違反が発生します。PdirP->child[dir] == NULL
// 親は省略可能ですvoid insert ( Tree * tree , Node * node , Node * parent , Direction dir ) {ノード->色=赤;ノード->親=親;if ( ! parent ) {ツリー->ルート=ノード;戻る;}親->子[ dir ] =ノード;// ツリーのバランスを再調整するする{// ケース1if ( parent -> color == BLACK ) {戻る;}ノード* grandparent = parent -> parent ;if ( ! grandparent ) {// ケース4親->色=黒;戻る;}dir = direction ( parent );ノード* uncle = grandparent -> child [ 1 - dir ];if ( ! uncle || uncle -> color == BLACK ) {if ( node == parent -> child [ 1 - dir ]) {// ケース5rotate_subtree ( tree , parent , dir );ノード=親;親=祖父母->子[ dir ];}// ケース6rotate_subtree ( tree , grandparent , 1 - dir );親->色=黒;祖父母->色=赤;戻る;}// ケース2親->色=黒;叔父->色=黒;祖父母->色=赤;ノード=祖父母;} while (( parent = node -> parent ));// ケース3戻る;}挿入操作の再バランスループには、以下の不変条件があります。
dir。現在のノードの親Pは黒なので、要件 3が満たされます。ループ不変条件に従って、要件 4 も満たされます。
親ノードPと叔父ノードUの両方が赤色の場合、両方を黒色に塗り替えることができ、要件 4 を維持するために祖父母ノードG が赤色になります。親ノードまたは叔父ノードを通るパスはすべて祖父母ノードを通る必要があるため、これらのパス上の黒ノードの数は変わりません。ただし、祖父母ノード G に赤色の親ノードがある場合、祖父母ノードGは要件 3 に違反する可能性があります。G をNに再ラベル付けすると、ループ不変条件が満たされるため、1 つの黒レベル (= 2 つのツリーレベル) 上で再バランスを反復できます。
挿入ケース2が実行されました回実行され、木の高さは 1 増えてhになりました。現在のノードNは木の (赤い) 根であり、すべての RB 特性が満たされています。
親ノードPは赤色で、ルートノードです。ノードNも赤色なので、要件3は満たされません。しかし、 Pの色を変更すると、ツリーはRB型になります。ツリーの黒い部分の高さは1増加します。
親ノードPは赤色ですが、叔父ノードUは黒色です。最終的な目標は、親ノードP を祖父母の位置に回転することですが、NがGの「内部」孫(つまり、NがGの右子の左子またはGの左子の右子である場合) には、これは機能しません。Pでのdir-回転は、現在のノードNとその親ノードPの役割を入れ替えます。回転により、Nを通るパス (図の2とラベル付けされたサブツリー内のパス) が追加され、 Pを通るパス( 4とラベル付けされたサブツリー内のパス) が削除されます。しかし、 PとN の両方が赤色なので、要件 4は維持されます。要件 3 はケース 6 で復元されます。
現在のノードN は、 Gの「外側」の孫(左の子の左または右の子の右) であることが確実です。次に、 Gで(1-dir)-回転を行い、 Gの代わりにPを配置して、 P をNとGの親にします。要件 3が違反されたため、 Gは黒色で、以前の子であるPは赤色です。PとGの色を入れ替えると、結果として得られる木は要件 3 を満たします。要件 4も満たされたままです。これは、黒色のGを通っていたすべてのパスが、黒色のPを通るようになったためです。
このアルゴリズムは補助データ構造を使用せずに入力を変換し、補助変数用に少量の追加ストレージ領域しか使用しないため、インプレース変換です。
複雑なケースは、Nがルートではなく、黒色で表示され、適切な子を持たない場合(⇔ NULLの子のみを持つ場合)です。最初のイテレーションでは、NはNULLに置き換えられます。
void remove ( Tree * tree , Node * node ) {ノード*親=ノード->親;ノード*兄弟;ノード* close_nephew ;ノード*遠い甥;方向dir = direction ( node );親->子[ dir ] = NULL ;start_balanceへ移動;する{dir = direction ( node );開始残高:兄弟=親->子[ 1 - dir ];distant_nephew = sibling -> child [ 1 - dir ];close_nephew = sibling -> child [ dir ];if ( sibling -> color == RED ) {// ケース3rotate_subtree ( tree , parent , dir );親要素->色=赤;兄弟->色=黒;兄弟姉妹=近しい甥;distant_nephew = sibling -> child [ 1 - dir ];if ( distant_nephew && distant_nephew -> color == RED ) {case_6へ移動;}close_nephew = sibling -> child [ dir ];if ( close_nephew && close_nephew -> color == RED ) {case_5へ移動;}// ケース4兄弟->色=赤;親->色=黒;戻る;}if ( distant_nephew && distant_nephew -> color == RED ) {case_6へ移動;}if ( close_nephew && close_nephew -> color == RED ) {case_5へ移動;}if ( parent -> color == RED ) {// ケース4兄弟->色=赤;親->色=黒;戻る;}// ケース2兄弟->色=赤;ノード=親;} while ( parent = node -> parent );// ケース1戻る;ケース5 :rotate_subtree ( tree , sibling , 1 - dir );兄弟->色=赤;close_nephew -> color = BLACK ;遠い甥=兄弟姉妹;兄弟姉妹=近しい甥;ケース6 :rotate_subtree ( tree , parent , dir );兄弟->色=親->色;親->色=黒;distant_nephew -> color = BLACK ;戻る;}削除操作の再バランスループには、次の不変条件があります。
dir。現在のノードNが新しいルートです。各パスから黒ノードが1つ削除されたため、RB特性は維持されます。木の黒ノードの高さは1減少します。
P、S、およびSの子は黒です。Sを赤に塗ると、 Sを通過するすべてのパス( Nを通過しないパスと正確に一致する)の黒ノードが1つ減ります。これで、 Pを根とするサブツリー内のすべてのパスの黒ノードの数は同じになりますが、 Pを通過しないパスより1つ少ないため、要件4はまだ満たされない可能性があります。PをNに再ラベル付けすると、ループ不変条件が満たされるため、1つ上の黒レベル(= 1ツリーレベル)で再バランスを反復できます。
兄弟ノードSは赤色なので、Pと甥ノードCおよびDは黒色でなければなりません。PでAdir回転を行うと、SはNの祖父母になります。その後、 PとSの色を反転させた後も、 Nを通る経路は黒色のノードが1つ不足しています。しかし、Nには赤色の親ノードPと、再割り当て後の黒色の兄弟ノードSが存在するため、ケース4、5、または6の変換によってRB形状を復元することができます。
兄弟SとSの子供は黒色だが、Pは赤色である。SとPの色を交換しても、Sを通る経路上の黒色のノード数には影響しないが、 Nを通る経路上の黒色のノード数は1つ増え、それらの経路で削除された黒色のノードを補うことになる。
兄弟Sは黒、Sの近しい子Cは赤、Sの遠い子Dは黒です。Sで(1-dir)-回転を行うと、甥C がSの親となり、Nの新しい兄弟になります。SとCの色は交換されます。すべてのパスには依然として同じ数の黒いノードがありますが、N には遠い子が赤である黒い兄弟がいるので、この配置はケース D6 に適合します。Nもその親Pもこの変換の影響を受けず、P は赤または黒になる可能性があります (図中)。![]()
兄弟Sは黒で、Sの遠い子Dは赤です。Pでdir-回転を行うと、兄弟SはPとSの遠い子Dの親になります。PとSの色が交換され、Dは黒になります。サブツリー全体は、ルートSで依然として同じ色、つまり赤または黒 (図中) を持ち、これは変換前と変換後で同じ色を指します。このようにして要件 3が維持されます。サブツリー内のNを通らないパス(つまり、図中のDとノード3を通らないパス) は、以前と同じ数の黒いノードを通りますが、Nには新たに 1 つの黒い祖先が加わります。P が黒になったか、 P が黒でS が黒い祖父母として追加されたかのいずれかです。したがって、 Nを通すパスは新たに 1 つの黒いノードを通り、要件 4が回復され、ツリー全体が RB 型になります。![]()
このアルゴリズムは補助データ構造を使用せずに入力を変換し、補助変数用に少量の追加ストレージ領域しか使用しないため、インプレース変換です。

のために高さのある赤黒い木がありますと
ノード(は床関数であり、この木の高さでノード数が少ない赤黒木は存在しないため、最小です。その黒の高さは(黒い根付き)または奇数の場合(それから赤い根で)
ある高さの赤黒木が最小数のノードを持つためには、最小の黒の高さで最大の木の高さを達成するために、最大の数の赤ノードを持つ最長のパスがちょうど1つ存在しなければならない。このパスの他に、他のすべてのノードは黒でなければならない。[ 15 ]: 444 証明の概略この木からノードを取り除くと、高さが失われるか、何らかのRB特性が失われる。
高さのRBツリー赤根の場合、その影響は最小限です。これは、
高さが最小のRBツリー(図2のRB h )根には、高さの異なる2つの子サブツリーが存在する。高さの高い子サブツリーは最小RBツリーRB h –1でもあり、その高さを定義する最長パスも含まれている。;それはノードと黒の高さもう一方のサブツリーは、(黒)の高さの完全二分木です。持っている黒ノードのみで、赤ノードは存在しない。ノードの数は帰納法によって求められる。
関数のグラフ凸であり、ブレークポイントを持つ区分的線形である。どこ関数は以下のように表にまとめられています。A027383( h –1)( OEISの配列A027383)。
不平等につながる奇数の場合につながる
偶数の場合も奇数の場合も、区間内
とノードの数である。[ 33 ]
赤黒の木ノード(キー)はツリーの高さを持つ
単一要素の挿入、削除、および検索操作に加えて、赤黒木には、和集合、積集合、差集合など、いくつかの集合操作が定義されています。これらの集合関数に基づいて、挿入または削除の高速一括操作を実装できます。これらの集合操作は、分割と結合という2 つのヘルパー操作に依存しています。新しい操作により、赤黒木の実装はより効率的で高度に並列化できます。[ 34 ]この実装の時間計算量を達成するには、ルートが赤または黒のどちらかである必要があり、すべてのノードが独自の黒の高さを保持する必要があります。
結合アルゴリズムは以下のとおりです。
function joinRightRB(T L , k, T R ): if (T L .color=black) and (T L .blackHeight=T R .blackHeight): return Node(T L ,⟨k,red⟩,T R ) T'=Node(T L .left,⟨T L .key,T L .color⟩,joinRightRB(T L .right,k,T R )) if (T L .color=black) and (T'.right.color=T'.right.right.color=red): T'.right.right.color=black; return rotateLeft(T') return T' /* T ' ' [recte T'] */ function joinLeftRB(T L , k, T R ): /* joinRightRB と対称的 */ function join(T L , k, T R ): if T L .blackHeight>T R .blackHeight: T'=joinRightRB(T L ,k,T R ) if (T'.color=red) and (T'.right.color=red): T'.color=black T R .blackHeight > T L .blackHeightの場合、 T' を返します。 /* 対称 */ if (T L .color=black) and (T R .color=black): return Node(T L ,⟨k,red⟩,T R ) return Node(T L ,⟨k,black⟩,T R )
分割アルゴリズムは以下のとおりです。
function split(T, k): if (T = NULL) return (NULL, false, NULL) if (k = T.key) return (T.left, true, T.right) if (k < T.key): (L',b,R') = split(T.left, k) return (L',b,join(R',T.key,T.right)) (L',b,R') = split(T.right, k) return (join(T.left,T.key,L'),b,R')
集合AとBを表す2 つの赤黒木t 1とt 2の和集合は、 A ∪ Bを表す赤黒木tです。次の再帰関数はこの和集合を計算します。
function union(t 1 , t 2 ): if t 1 = NULL return t 2 if t 2 = NULL return t 1 (L 1 ,b,R 1 )=split(t 1 ,t 2 .key) proc1=開始: T L = union(L 1 ,t 2 .left) proc2=開始: T R = union(R 1 ,t 2 .right) wait all proc1,proc2 return join(T L , t 2 .key, T R )
ここでは、split関数は2つのツリーを返すものと想定されています。1つは入力キーを除いたキーを保持するツリー、もう1つは入力キーより大きいキーを保持するツリーです。(このアルゴリズムは非破壊的ですが、インプレース破壊バージョンも存在します。)
交差または差のアルゴリズムは似ていますが、Joinと同じだが中間キーがないJoin2ヘルパールーチンが必要です。和集合、交差、差の新しい関数に基づいて、1 つのキーまたは複数のキーを赤黒木に挿入または削除できます。SplitはJoinを呼び出しますが、赤黒木のバランス基準を直接処理しないため、このような実装は通常「結合ベース」の実装と呼ばれます。
和集合、交差集合、差集合のそれぞれの複雑さは2本の赤黒木のサイズそしてこの複雑さは、比較回数の観点から最適です。さらに重要なのは、和集合、積集合、差集合の再帰呼び出しは互いに独立しているため、並列深度で並列実行できることです。[ 34 ]の場合結合ベースの実装では、より大きなツリーのルートを使用してより小さなツリーを分割する場合、単一要素の挿入と削除と同じ計算上の有向非巡回グラフ(DAG)になります。
項目のソート済みリストから赤黒木を構築するための並列アルゴリズムは定数時間で実行できます。時間は、コンピュータのモデルによって異なりますが、利用可能なプロセッサの数が漸近的に数に比例する場合アイテムのうち高速な検索、挿入、削除の並列アルゴリズムも知られている。[ 35 ]
赤黒木のための結合ベースのアルゴリズムは、和集合、積集合、構築、フィルタリング、マップリデュースなど、一括操作に対して並列処理が可能です。
挿入、削除、更新といった基本的な操作は、複数の要素をまとめて処理する操作を定義することで並列化できます。また、複数の基本操作を組み合わせて一括処理することも可能です。例えば、一括処理では、ツリーに挿入する要素と削除する要素の両方をまとめて処理できます。
一括操作のアルゴリズムは、赤黒木だけでなく、2-3木、2-3-4木、(a,b)木などの他のソート済みシーケンスデータ構造にも適用できます。以下では、一括挿入のためのさまざまなアルゴリズムについて説明しますが、同じアルゴリズムは削除や更新にも適用できます。一括挿入とは、シーケンスの各要素を挿入する操作です。木の中に。
このアプローチは、効率的な結合および分割操作をサポートするすべてのソート済みシーケンスデータ構造に適用できます。[ 36 ] 基本的な考え方は、IとTを複数の部分に分割し、これらの部分に対して並列に挿入を実行することです。
ステップ3で私が設定した分割の制約により、ステップ5でツリーを再び結合し、結果として得られるシーケンスがソートされることが保証されます。
この擬似コードは、一括挿入のための結合ベースアルゴリズムの単純な分割統治実装を示しています。どちらの再帰呼び出しも並列実行可能です。ここで使用されている結合操作は、この記事で説明されているバージョンとは異なり、代わりに2番目のパラメータkが欠落しているjoin2が使用されています。
bulkInsert (T, I, k): I.sort() bulklInsertRec(T, I, k) bulkInsertRec (T, I, k): if k = 1: forall e in I: T.insert(e) else m := ⌊size(I) / 2⌋ (T 1 , _, T 2 ) := split(T, I[m]) bulkInsertRec(T 1 , I[0 .. m], ⌈k / 2⌉) || bulkInsertRec(T 2 , I[m + 1 .. size(I) - 1], ⌊k / 2⌋) T ← join2(T 1 , T 2 )
ソートIは、この分析では考慮されていません。
バルク操作を並列化するもう1つの方法は、パイプライン方式を使用することです。[ 38 ] これは、基本操作の処理タスクを一連のサブタスクに分割することによって実現できます。複数の基本操作の場合、各サブタスクを個別のプロセッサに割り当てることで、サブタスクを並列に処理できます。
ソートIはこの分析では考慮されていません。また、より小さいと想定されるそうでなければ、結果として得られるツリーを最初から構築する方が効率的だろう。
多くの人が、なぜ赤黒という名前を使ったのかと尋ねます。私たちは、このデータ構造、つまりバランスの取れたツリーの見方を、パーソナルコンピュータや、グラフィカルユーザーインターフェース、イーサネット、オブジェクト指向プログラミングなど、今日私たちが使っている多くのイノベーションの本拠地であるゼロックスPARCで発明しました。しかし、そこで発明されたものの1つがレーザー印刷で、私たちは近くにカラーレーザープリンターがあり、色で印刷できるようになったことに非常に興奮しました。そして、色の中で赤が一番見栄えが良かったのです。そのため、3つのノードで赤いリンク、つまりリンクの種類を区別するために、赤という色を選びました。これが、これまで質問してきた人たちへの答えです。
ソートされたリストから赤黒木を構築するための並列アルゴリズム
アイテムは一緒に過ごす時間CRCW PRAM 上のプロセッサで実行され、一緒に過ごす時間EREW PRAM上のプロセッサ。