web-dev-qa-db-ja.com

std :: mapが赤黒ツリーとして実装されているのはなぜですか?

std::map赤黒木 として実装されているのはなぜですか?

バランスの取れた複数の バイナリ検索ツリー (BST)があります。赤黒木を選択する際の設計上のトレードオフは何ですか?

173

おそらく、最も一般的な2つの自己分散ツリーアルゴリズムは、 赤黒木AVL木 です。挿入/更新後にツリーのバランスをとるには、両方のアルゴリズムで、ツリーのノードが回転する回転の概念を使用して、再バランスを実行します。

どちらのアルゴリズムでも挿入/削除操作はO(log n)ですが、Red-Blackツリーの場合、再分散回転は O(1) 操作ですが、AVLではこれは- O(log n) 操作。再バランスステージのこの側面で赤黒ツリーをより効率的にし、より一般的に使用される理由の1つです。

赤黒ツリーは、JavaおよびMicrosoft .NET Frameworkの製品を含むほとんどのコレクションライブラリで使用されます。

112
Chris Taylor

それは本当に使い方に依存します。 AVLツリーには通常、リバランスの回転が多くあります。したがって、アプリケーションの挿入操作と削除操作が多すぎず、検索に重点を置いている場合は、おそらくAVLツリーが適切な選択です。

std::mapは、ノードの挿入/削除の速度と検索の間の合理的なトレードオフを得るため、赤黒ツリーを使用します。

43
webbertiger

AVLツリーの最大高さは1.44lognですが、RBツリーの最大高さは2lognです。 AVLに要素を挿入すると、ツリーの1つのポイントで再バランスがとられる可能性があります。リバランスにより挿入が終了します。新しいリーフの挿入後、そのリーフの祖先の更新は、ルートまで、または2つのサブツリーが同じ深さになるまで実行する必要があります。 k個のノードを更新する必要がある確率は1/3 ^ kです。リバランスはO(1)です。要素を削除すると、複数のリバランスが発生する可能性があります(ツリーの深さの半分まで)。

RBツリーは、バイナリ検索ツリーとして表される4次のBツリーです。 Bツリーの4ノードは、同等のBSTで2つのレベルになります。最悪の場合、ツリーのすべてのノードは2ノードで、リーフに至る3ノードのチェーンは1つだけです。その葉は根から2lognの距離にあります。

ルートから挿入ポイントに下がって、4ノードを2ノードに変更して、挿入がリーフを飽和させないようにする必要があります。挿入から戻って、これらすべてのノードを分析して、4ノードを正しく表していることを確認する必要があります。これは、ツリー内を下ることもできます。グローバルコストは同じになります。無料のランチはありません!ツリーから要素を削除する順序は同じです。

これらのすべてのツリーでは、ノードが高さ、重量、色などの情報を保持する必要があります。そのような追加情報がないのはスプレイツリーだけです。しかし、ほとんどの人は、構造の不自由さのために、スプレイツリーを恐れています!

最後に、ツリーはノード内の重み情報も保持できるため、重みのバランスを取ることができます。さまざまなスキームを適用できます。サブツリーに他のサブツリーの要素の数が3倍以上含まれる場合は、リバランスする必要があります。リバランスは、1回または2回の回転で再度行われます。これは、最悪の場合の2.4lognを意味します。 3の代わりに2倍ではるかに良い比率で逃げることができますが、それはサブツリーの1%をあちこちで不均衡のままにすることを意味するかもしれません。トリッキー!

どのタイプのツリーが最適ですか?確かにAVL。それらはコーディングが最も簡単で、lognに最も近い高さを持っています。 1000000要素のツリーの場合、AVLは最大で高さ29、RB 40、および比率に応じて36または50の重量バランスが取られます。

他にも多くの変数があります:ランダム性、追加の比率、削除、検索など。

24
user847376

以前の回答はアドレスツリーの代替案のみであり、赤黒はおそらく歴史的な理由のためにのみ残っています。

ハッシュテーブルではないのはなぜですか?

型は、ツリーのキーとして使用される<比較のみを必要とします。ただし、ハッシュテーブルでは、各キータイプにhash関数が定義されている必要があります。型の要件を最小限に保つことは、汎用プログラミングにとって非常に重要です。

適切なハッシュテーブルを設計するには、使用するコンテキストを熟知している必要があります。オープンアドレッシングまたはリンクチェーンを使用する必要がありますか?サイズ変更する前にどのレベルの負荷を受け入れる必要がありますか?衝突を回避する高価なハッシュを使用する必要がありますか、それともラフで高速なハッシュを使用する必要がありますか?

STLはアプリケーションに最適な選択肢を予測できないため、デフォルトはより柔軟である必要があります。木は「機能する」だけでうまくスケールします。

(C++ 11はunordered_mapでハッシュテーブルを追加しました。 documentation からわかるように、これらのオプションの多くを構成するにはポリシーを設定する必要があります。)

他の木はどうですか?

レッドブラックツリーは、BSTとは異なり、高速なルックアップを提供し、自己バランスをとっています。別のユーザーは、自己バランスAVLツリーを超える利点を指摘しました。

アレクサンダーステパノフ(STLの作成者)は、std::mapを再度記述した場合、赤黒ツリーの代わりにB *ツリーを使用すると言った。

それ以降の最大の変更点の1つは、キャッシュの増加です。キャッシュミスは非常にコストがかかるため、現在では参照の局所性がより重要になっています。参照の局所性が低いノードベースのデータ構造は、あまり意味がありません。今日STLを設計している場合、別のコンテナセットがあります。たとえば、連想コンテナを実装するには、メモリ内のB *ツリーが赤黒ツリーよりもはるかに優れた選択肢です。 - Alexander Stepanov

常に赤黒木またはB *木を使用する必要がありますか?

他の機会に、Alexはstd::vectorが同様の理由でほとんど常に最高のリストコンテナであると述べました。 std::listまたはstd::dequeを学校で教えられたような状況(リストの中央から要素を削除するなど)で使用することはほとんど意味がありません。 std::vectorは非常に高速であるため、大規模なN以外のすべての構造に勝っています。

std::vectorと線形検索を使用して要素数が少ない(数百?)場合、その推論を適用すると、std::mapのツリー実装よりも効率的です。挿入頻度に応じて、std::vectorと組み合わせたソート済みstd::binary_searchが最速の選択肢です。

17
Justin Meiners

更新2017年6月14日:webbertigerはコメントした後、その回答を編集します。私は、その答えが私の目にはるかに良くなっていることを指摘する必要があります。しかし、私は追加情報として答えを保持しました...

私は最初の答えが間違っていると思うという事実のために(修正:もう両方ではない)、3番目の答えは間違った肯定を持っています。私は物事を明確にしなければならないと感じています...

最も人気のある2つのツリーは、AVLとRed Black(RB)です。主な違いは、使用率にあります。

  • AVL:コンサルテーション(読み取り)の比率が操作(変更)よりも大きい場合に適しています。メモリフットプリントはRBよりも少し小さくなります(カラーリングに必要なビットのため)。
  • RB:相談(読み取り)と操作(変更)のバランスが取れている一般的な場合、または相談よりも変更が多い場合に適しています。赤黒フラグの格納によるわずかに大きなメモリフットプリント。

主な違いはカラーリングにあります。 RBツリーでは、AVLよりもリバランスアクションが少なくなります。これは、カラーリングにより、相対的に高いコストのリバランスアクションをスキップまたは短縮できる場合があるためです。 RBツリーには色付けのため、黒いノード(〜2倍のレベルの可能性がある)の間に赤いノードを受け入れることができるため、ノードのレベルが高くなり、検索(読み取り)の効率が少し低下します...定数(2x)、O(log n)のままです。

ツリーの変更のパフォーマンスヒット(有意)とツリーのコンサルテーションのパフォーマンスヒット(ほとんど重要ではない)を考慮すると、一般的なケースではAVLよりもRBを好むことが自然になります。

3
Eric Ouellet

それはあなたの実装の選択です-それらはバランスのとれたツリーとして実装できます。さまざまな選択肢はすべて、わずかな違いはありますが比較可能です。したがって、anyはanyと同じくらい優れています。

2
necromancer