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


193

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

そこにはいくつかのバランスのとれた二分探索木(BST)があります。赤黒木を選択する際の設計上のトレードオフは何でしたか?


26
私が見たすべての実装はRBツリーを使用していますが、これはまだ実装に依存していることに注意してください。
トーマス

3
@トーマス。それは実装に依存していますが、なぜすべての実装がRBツリーを使用するようになっているのですか?
Denis Gorodetskiy

1
スキップリストの使用を検討しているSTL実装者がいるかどうか、本当に知りたいです。
Matthieu

2
C ++のマップとセットは、実際には順序付きマップと順序付きセットです。ハッシュ関数を使用して実装されていません。すべてのクエリはを受け取りO(logn)、受け取りませんO(1)が、値は常にソートされます。C ++ 11から(と思います)、ハッシュ関数を使用して実装されているunordered_mapとがありunordered_set、それらは並べ替えられていませんが、ほとんどのクエリと操作はO(1)(平均)で可能です
SomethingSomething

@Thomasは真実ですが、実際にはそれほど興味深いものではありません。この規格では、特定のアルゴリズムまたは一連のアルゴリズムを念頭に置いて、複雑さを保証しています。
Justin Meiners、2018

回答:


125

おそらく最も一般的な2つの自己平衡ツリーアルゴリズムは、Red-BlackツリーAVLツリーです。挿入/更新後にツリーのバランスを取るには、両方のアルゴリズムで回転の概念を使用して、ツリーのノードを回転させて再バランスを実行します。

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

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


54
赤黒木がO(1)時間でツリーの変更を行えるように聞こえますが、これは正しくありません。ツリーの変更は、赤黒とAVLの両方のツリーでO(log n)です。これは、主な操作がすでにO(log n)であるため、ツリー変更のバランス調整部分がO(1)またはO(log n)であるかどうかを疑わしくします。AVLツリーが行う少し余分な作業をすべて行った後でも、ツリーのバランスがより緊密になり、ルックアップがわずかに速くなります。したがって、これは完全に有効なトレードオフであり、AVLツリーが赤黒ツリーより劣ることはありません。
ネクロマンサー2011年

35
違いを確認するには、実際のランタイムの複雑さを超えて検討する必要があります。通常、AVLツリーの総ランタイムは、挿入/削除よりもルックアップの方が多い場合に低くなります。RBツリーでは、挿入/削除の数が多いほど、実行時間が短くなります。ブレークが発生する正確な割合は、実装、ハードウェア、および正確な使用法の多くの詳細に依存しますが、ライブラリの作成者は幅広い使用パターンをサポートする必要があるため、知識に基づいた推測を行う必要があります。また、AVLの実装は少し難しいので、AVLを使用すると実績のあるメリットが必要になる場合があります。
スティーブジェソップ

6
RBツリーは「デフォルトの実装」ではありません。各実装者は実装を選択します。私たちの知る限りでは、それらはすべてRBツリーを選択しているためこれはおそらくパフォーマンスのためか、実装/保守を容易にするためのものです。先ほど述べたように、パフォーマンスのブレークポイントは、ルックアップよりも挿入/削除のほうが多いと考えているわけではなく、2つの間の比率が、RBがAVLよりも優れていると考えるレベルを上回っているだけです。
Steve Jessop

9
@Denis:残念ながら、数値を取得する唯一の方法は、std::map実装のリストを作成し、開発者を追跡して、彼らが決定を下すのに使用した基準を尋ねることです。これは推測にとどまります。
Steve Jessop

4
これらすべてに欠けているのは、バランスの決定を行うために必要な補助情報を保存するためのノードごとのコストです。赤黒の木は、色を表すために1ビットを必要とします。AVLツリーには少なくとも2ビットが必要です(-1、0、または1を表すため)。
SJHowe 2017

46

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

std::map ノードの挿入/削除の速度と検索の間で妥当なトレードオフが得られるため、Red-Blackツリーを使用します。


1
よろしいですか??? 私は個人的に、赤黒の木はどちらかまたはより複雑で、決して単純ではないと思います。唯一のことは、Rd-Blackツリーにあり、再バランスが発生する頻度はAVLよりも低くなります。
エリックウエレット2017年

1
@Eric理論的には、R / BツリーとAVLツリーはどちらも挿入と削除に関して複雑度O(log n))を持っています。しかし、運用コストの1つの大きな部分はローテーションです。これは、これら2つのツリー間で異なります。参照してくださいdiscuss.fogcreek.com/joelonsoftware/...引用:「O(ログn)の回転を必要とすることができますAVL木のバランスを取る、赤、黒の木ながら、それが持っているかもしれませんが(バランスにそれを持って来るために、最大で2頭の回転がかかりますO(log n)ノードを調べて、回転が必要な場所を決定します。」それに応じて私のコメントを編集しました。
webbertiger 2017年

26

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

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

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

これらのすべてのツリーでは、ノードが高さ、重量、色などの情報を運ぶ必要があります。このような追加情報が含まれないのはSplayツリーだけです。しかし、ほとんどの人はその構造の乱雑さのため、スプレーの木を恐れています!

最後に、ツリーはノードで重み情報を運ぶこともでき、重みのバランスをとることができます。さまざまなスキームを適用できます。サブツリーに他のサブツリーの要素数の3倍を超える数が含まれている場合は、バランスを再調整する必要があります。リバランスは、1回転または2回転のいずれかで再度実行されます。これは、2.4lognの最悪のケースを意味します。3の代わりに2倍でより良い比率を得ることができますが、それは、サブツリーの1%弱をあちこちで不均衡にしておくことを意味する場合があります。トリッキー!

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

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


2
いい答えだ。しかし、AVLが最高の場合、標準ライブラリがstd :: mapをRBツリーとして実装するのはなぜですか?
Denis Gorodetskiy、2011年

13
AVLの木が間違いなく最高であることには同意しません。高さは低くなりますが、赤/黒の木よりも(合計で)再調整に多くの作業が必要です(O(log n)再調整作業とO(1)償却済み再調整作業)。スプレーの木ははるかに良くなる可能性があり、人々がそれらを恐れているというあなたの主張は根拠がありません。そこに1つの普遍的な「最良の」ツリーバランシング方式はありません。
templatetypedef

ほぼ完璧な答えです。なぜAVLが最高だと言ったのですか?それは単に間違っているため、最も一般的な実装では赤黒ツリーを使用します。AVLを選択するには、読み取り操作の比率をかなり高くする必要があります。また、AVLはRBよりもメモリフットプリントが少し少なくなっています。
エリックウエレット2017年

通常、ツリーは挿入されるよりも頻繁に検索されるため、ほとんどの場合AVLの方が優れている傾向があることに同意します。RBツリーは、書き込みが大部分の場合にわずかな利点があり、さらに重要なことに、大部分が読み取りの場合にわずかな欠点があるツリーであると、なぜ広く広く考えられているのですか?あなたが見つけるよりも多くを挿入すると本当に信じられていますか?
doug65536

25

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

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

タイプでは、<演算子(比較)をツリーのキーとして使用するだけで済みます。ただし、ハッシュテーブルでは、各キータイプにhash関数が定義されている必要があります。型の要件を最小限に抑えることは、一般的なプログラミングにとって非常に重要であり、さまざまな型やアルゴリズムで使用できるようになります。

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

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

(C ++ 11には、とのハッシュテーブルを追加しましたunordered_map。あなたが見ることができるからドキュメント、それはこれらのオプションの多くのconfigureにポリシーを設定する必要があります。)

他の木はどうですか?

赤黒木は高速検索を提供し、BSTとは異なり、自己バランスをとります。別のユーザーは、自己バランスAVLツリーに比べてその利点を指摘しました。

アレクサンダーステパノフ(STLの作成者)はstd::map、最新のメモリキャッシュに対してより友好的であるため、もう一度書いた場合、Red-Blackツリーの代わりにB *ツリーを使用すると述べました。

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

マップは常に木を使用する必要がありますか?

別の可能なマップ実装は、ソートされたベクトル(挿入ソート)とバイナリ検索です。これは、頻繁には変更されないが頻繁に照会されるコンテナに適しています。私はしばしばこれをCでqsort行いbsearch、組み込まれています。

マップを使用する必要さえありますか?

キャッシュに関する考慮事項は、学校で教えられた状況(リストの中央から要素を削除するなど)であっても、それを使用することstd::listやそれをstd::deque超えることはほとんど意味がないことを意味しますstd:vector。同じ理由で、forループを使用してリストを線形検索する方が、いくつかのルックアップ用のマップを作成するよりも効率的でクリーンです。

もちろん、読み取り可能なコンテナを選択することは、通常、パフォーマンスよりも重要です。


3

2017-06-14の更新:コメントを書き込んだ後、webbertigerが回答を編集します。私はその答えが私の目にははるかに良くなっていることを指摘しておきます。しかし、私は追加情報として私の答えを保持しました...

最初の答えは間違っている(訂正:両方ではない)と思い、3番目の答えは誤った肯定を持っているためです。はっきりさせないといけないと思います...

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

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

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

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


2

これは、実装の選択にすぎません。これらは、バランスのとれたツリーとして実装できます。さまざまな選択肢はすべて、わずかな違いはありますが比較可能です。したがって、どれもどれも同じです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.