私はアルゴリズムに磨きをかけ、ハッシュテーブルを実装するこれらの2つの方法を確認しました。彼らは主に同様のパフォーマンス特性とメモリ要件を持っているようです。
線形プローブのいくつかの欠点を考えることができます-つまり、配列の拡張にはコストがかかる可能性があります(ただし、これは、最大で2 log N回行われますか?おそらく大したことではありません)。削除の管理は少し難しいです。 。しかし、私には利点もあると思います。そうしないと、より明白な実装の次にある実行可能な実装方法として、教科書に記載されないでしょう。
なぜどちらを選ぶのですか?
線形プローブ(または実際のプローブ)では、削除は「ソフト」でなければなりません。つまり、ユーザーが検索できるものと一致しないダミー値(トゥームストーンと呼ばれることが多い)を入力する必要があります。または、毎回リハッシュする必要があります。墓石が多すぎる場合は、ハッシュを再構築するか、墓地をデフラグするための戦略が推奨されます。
個別のチェーン(各バケットはリンクされた値のリストへのポインター)には、キャッシュに関連するすべての問題があるリンクリストを検索してしまうという欠点があります。
調査方法のもう1つの利点は、値がすべて同じ配列に存在することです。これにより、配列のみをコピーするだけで、コピーオンライトが非常に簡単になります。元のクラスが不変クラスによって変更されないことが保証できる場合、スナップショットの作成はO(1)であり、ロックせずに実行できます。
この素晴らしい答えをチェックしてください:
ここで引用:
チェーンハッシュが線形プローブよりも高速であることに気付いたのには驚きました。実際、線形プローブは通常、チェーンよりも大幅に高速です。実際、それが主な理由です。
チェーンハッシュは理論的には優れており、線形プローブにはいくつかの既知の理論的な弱点があります(たとえば、O(1)期待されるルックアップを保証するためにハッシュ関数に5方向の独立性が必要であるなど))。線形プローブは、参照の局所性により、通常は大幅に高速です。具体的には、リンクリスト内のポインタを追跡するよりも、配列内の一連の要素にアクセスする方が高速であるため、調査が必要な場合でも、線形プローブは連鎖ハッシュよりも優れています。より多くの要素。
連鎖ハッシュには他にもメリットがあります。たとえば、線形プローブハッシュテーブルへの挿入では、新しい割り当ては必要ありません(テーブルを再ハッシュしない限り)。そのため、メモリが不足しているネットワークルーターなどのアプリケーションでは、テーブルが設定されたら、それを知っておくと便利です。要素は、mallocが失敗するリスクなしで、その中に配置できます。
私は実際にバイアスされた答えでジャンプしますprefer個別にリンクされたリストとの個別のチェーンを見つけてそれを見つけますeasierそれらでパフォーマンスを達成します(私はそれらを言っていません「最適です-私のユースケースではeasier)、それは矛盾するように矛盾しています。
もちろん、理論的な最適値は、衝突のないハッシュテーブル、または最小限のクラスタリングによるプロービングテクニックです。ただし、個別のチェーンソリューションは、クラスタリングの問題にまったく対処する必要はありません。
とはいえ、私が使用するデータ表現は、ノードごとに個別のメモリ割り当てを呼び出しません。ここではCです:
struct Bucket
{
int head;
};
struct BucketNode
{
int next;
int element;
};
struct HashTable
{
// Array of buckets, pre-allocated in advance.
struct Bucket* buckets;
// Array of nodes, pre-allocated assuming the client knows
// how many nodes he's going to insert in advance. Otherwise
// realloc using a similar strategy as std::vector in C++.
struct BucketNode* nodes;
// Number of bucket heads.
int num_buckets;
// Number of nodes inserted so far.
int num_nodes;
};
バケットは単なる32ビットのインデックスであり(実際には構造体も使用していません)、ノードは2つの32ビットのインデックスにすぎません。多くの場合、ノードはテーブルに挿入される要素の配列と並行して格納され、ハッシュテーブルのオーバーヘッドをバケットあたり32ビットおよび32ビットに削減するため、element
インデックスさえ必要としません挿入された要素あたりのビット数。私がより頻繁に使用する実際のバージョンは次のようになります。
struct HashTable
{
// Array of head indices. The indices point to entries in the
// second array below.
int* buckets;
// Array of next indices parallel to the elements to insert.
int* next_indices;
// Number of bucket heads.
int num_buckets;
};
また、空間的な局所性が低下した場合、後処理パスを簡単に実行できます。ここで、各バケットノードが他のバケットテーブルと隣接している新しいハッシュテーブルを作成します(単純なコピー関数は、ハッシュテーブルを直線的に通過して新しいハッシュテーブルを作成します) -ハッシュテーブルを走査する性質上、コピーは互いに隣接するバケット内のすべての隣接ノードで終了します)。
プローブ手法に関しては、空間ローカリティが最初からすでに使用されているため、メモリプールやバッキングアレイがなく、バケットとノードごとに32ビットのオーバーヘッドがないという利点がありますが、多くの衝突により悪質な方法で蓄積し始める可能性のあるクラスタリングの問題に対処する必要があるかもしれません。
クラスタリングの本質は頭痛の種であり、多くの衝突が発生した場合には多くの分析が必要です。このソリューションの利点は、そのような詳細な分析とテストをしなくても、最初からまともな結果をしばしば達成できることです。また、テーブルが暗黙的にサイズを変更する場合、そのような設計が、バケットあたり32ビットとノードあたり32ビットを必要とするこの基本的なソリューションの実行をはるかに超える方法でメモリ使用量を爆発させてしまう場合に遭遇しました最悪のシナリオでもです。これは、多数の衝突があったとしても、悪くなりすぎないようにするソリューションです。
私のコードベースのほとんどは、インデックスを格納するデータ構造を中心に展開しており、挿入される要素の配列と並行してインデックスを格納することがよくあります。これにより、メモリサイズが削減され、挿入する要素の余分な深いコピーが回避され、メモリの使用を簡単に推論できます。それとは別に、私の場合、予測可能なパフォーマンスから最適なパフォーマンスを得ることができます。多くの一般的なケースのシナリオで最適ですが、最悪のシナリオでひどく実行できるアルゴリズムは、常に適切に実行され、予測できない時間にフレームレートが途切れないアルゴリズムよりも、多くの場合私にとってあまり好ましくありません。これらの種類のソリューションを支持する傾向があります。