セットまたは連想配列を実装するためにハッシュテーブルまたはバランスの取れたバイナリツリーを選択する必要がある場合、どの要素を考慮する必要がありますか?
一般的に、この質問には答えられません。
問題は、多くの種類のハッシュテーブルとバランスの取れたバイナリツリーがあり、それらのパフォーマンスが大きく異なることです。
したがって、素朴な答えは、必要な機能に依存します。順序付けが不要な場合はハッシュテーブルを使用し、そうでない場合はバランスの取れたバイナリツリーを使用します。
より詳細な答えを得るために、いくつかの代替案を検討しましょう。
ハッシュテーブル (いくつかの基本については、Wikipediaのエントリを参照してください)
二分木
O(1)は漸近的な複雑さであることを忘れないでください。要素が少ない場合、係数は通常(パフォーマンス面で)より重要です。これはハッシュ関数が遅い場合に特に当てはまります。 。
最後に、セットの場合、 Bloom Filters のような確率論的なデータ構造を考慮することもできます。
ハッシュテーブルは、通常、データを何らかの順序で保持する必要がない場合に適しています。データを並べ替えておく必要がある場合は、バイナリツリーの方が適しています。
近代的なアーキテクチャの価値ある点:ハッシュテーブルは、通常、負荷係数が低い場合、バイナリツリーよりも少ないメモリ読み取りを持ちます。メモリアクセスは、CPUサイクルの書き込みに比べてコストが高くなる傾向があるため、ハッシュテーブルは多くの場合より高速です。
次のバイナリツリーは、赤黒木、AVL木、またはトレジャーのように、自己バランスであると想定されています。
一方、ハッシュテーブルを拡張するときにハッシュテーブルのすべてを再ハッシュする必要がある場合、これは発生する(償却される)コストのかかる操作になる可能性があります。バイナリツリーにはこの制限はありません。
バイナリツリーは、純粋に機能的な言語で実装する方が簡単です。
バイナリツリーには、自然な並べ替え順序と、すべての要素に対してツリーを歩く自然な方法があります。
ハッシュテーブルの負荷係数が低い場合、多くのメモリ領域を無駄にしている可能性がありますが、2つのポインタを使用すると、バイナリツリーはより多くの領域を占有する傾向があります。
ハッシュテーブルは、ほぼO(1)(負荷係数の処理方法によって異なります)対ビンツリーO(lg n)です。
木は「平均的な実行者」になる傾向があります。彼らが特にうまくやることは何もありませんが、彼らは特に悪いことは何もしません。
バイナリ検索ツリーでは、キー間の完全な順序関係が必要です。ハッシュテーブルに必要なのは、一貫性のあるハッシュ関数との等価関係または同一関係のみです。
完全な順序関係が利用可能な場合、ソートされた配列は、バイナリツリーに匹敵するルックアップパフォーマンス、ハッシュテーブルの順序での最悪の場合の挿入パフォーマンス、および両方よりも複雑さとメモリ使用量が少なくなります。
最悪の場合のルックアップの複雑度をO(K)またはO(log K)要素をソートできる場合。
ツリーとハッシュテーブルの両方の不変式は、キーが変更された場合の復元にコストがかかりますが、ソートされた配列の場合はO(n log N)未満です。
これらは、使用する実装を決定する際に考慮すべき要素です。
ハッシュテーブルは検索が高速です。
二分木:
単一の要素にのみアクセスする必要がある場合は、ハッシュテーブルの方が優れています。ある範囲の要素が必要な場合、バイナリツリー以外のオプションはありません。
上記の他の素晴らしい答えに追加するには、私は言うだろう:
データ量が変わらない場合は、ハッシュテーブルを使用します(定数の保存など)。ただし、データ量が変化する場合は、ツリーを使用してください。これは、ハッシュテーブルで負荷係数に達すると、ハッシュテーブルのサイズを変更する必要があるという事実によるものです。サイズ変更操作は非常に遅くなる可能性があります。
対処していないと思う点の1つは、データ構造がpersistentの場合にツリーがはるかに優れていることです。つまり、不変の構造です。標準のハッシュテーブル(つまり、リンクリストの単一の配列を使用するもの)は、テーブル全体を変更しない限り変更できません。これに関連する状況の1つは、2つの同時実行関数の両方にハッシュテーブルのコピーがあり、そのうちの1つがテーブルを変更する場合です(テーブルが変更可能な場合、その変更は他のテーブルにも表示されます)。別の状況は次のようなものです。
def bar(table):
# some intern stuck this line of code in
table["hello"] = "world"
return table["the answer"]
def foo(x, y, table):
z = bar(table)
if "hello" in table:
raise Exception("failed catastrophically!")
return x + y + z
important_result = foo(1, 2, {
"the answer": 5,
"this table": "doesn't contain hello",
"so it should": "be ok"
})
# catastrophic failure occurs
可変テーブルでは、関数呼び出しが受け取るテーブルが実行中そのテーブルにとどまることを保証できません。他の関数呼び出しがテーブルを変更する可能性があるためです。
したがって、可変性は時には楽しいことではありません。現在、これを回避する方法は、テーブルを不変に保ち、更新が古いテーブルを変更せずにnewテーブルを返すようにすることです。しかし、ハッシュテーブルを使用すると、多くの場合、コストのかかるO(n)操作になります。基になる配列全体をコピーする必要があるためです。一方、バランスのとれたツリーでは、O(log n)ノードのみを作成する必要のある新しいツリーを生成できます(ツリーの残りの部分は同一です)。
これは、不変のマップが必要な場合に効率的なツリーが非常に便利になることを意味します。
セットの多くのわずかに異なるインスタンスがある場合は、おそらく構造を共有する必要があります。これはツリーを使用すると簡単です(ツリーが不変またはコピーオンライトの場合)。ハッシュテーブルでどれだけうまくできるかわかりません。少なくともそれほど明白ではありません。
私の経験では、ツリーはキャッシュの影響をあまりにも受けやすいため、hastableは常に高速です。
実際のデータを確認するには、TommyDSライブラリのベンチマークページを確認してください http://tommyds.sourceforge.net/
ここでは、最も一般的なハッシュテーブル、ツリー、トライライブラリのパフォーマンスを比較できます。
注意すべき1つのポイントは、トラバース、最小および最大アイテムについてです。ハッシュテーブルは、あらゆる種類の順序付きトラバース、または最小または最大アイテムへのアクセスをサポートしていません。これらの機能が重要な場合、バイナリツリーの方が適しています。