ハッシュとは何かを学ぶのに役立つ記事
ハッシュの概念
ハッシュは C++ で広く使用されており、データを迅速に検索して保存するために使用されるデータ構造およびアルゴリズムです。C++ でのハッシュの一般的な応用例を次に示します。
- ハッシュ テーブル: ハッシュ テーブルは、キーと値のペアを格納するために使用される効率的なデータ構造です。C++ では、
std::unordered_map
および はstd::unordered_set
標準ライブラリによって提供されるハッシュ テーブルの実装です。これらのコンテナーは、一定時間の検索、挿入、および削除操作を提供するため、データを迅速に検索して保存するのに最適です。
#include <unordered_map>
#include <unordered_set>
std::unordered_map<std::string, int> hashMap;
hashMap["apple"] = 3;
int value = hashMap["apple"];
std::unordered_set<int> hashSet;
hashSet.insert(42);
bool exists = hashSet.count(42) > 0;
- ハッシュ関数: ハッシュ関数は、入力データを固定サイズのハッシュ コード (ハッシュ値) (通常は整数) にマッピングします。C++ 標準ライブラリは複数のハッシュ関数を提供し、ユーザーがハッシュ関数をカスタマイズすることもできます。
std::hash<int> intHash;
size_t hashCode = intHash(42);
- ハッシュ コレクションとハッシュ マップ: 標準ライブラリのハッシュ テーブルに加えて、C++ は、Google や ハッシュ テーブル など、同様の機能を提供しながらメモリ オーバーヘッドが低い他のサードパーティ ライブラリと実装も提供し
absl::flat_hash_map
ますabsl::flat_hash_set
。 - ハッシュ セットは重複排除に使用されます。要素をハッシュ セットに挿入することで、重複を迅速に削除できます。
std::unordered_set<int> uniqueValues;
uniqueValues.insert(1);
uniqueValues.insert(2);
uniqueValues.insert(1); // 重复元素将被自动去重
- 暗号化におけるハッシュの応用: ハッシュ関数は、パスワードを保存および検証するために暗号化で使用されます。一般的に使用されるパスワード ハッシュ関数には、SHA-256 や bcrypt などがあります。
- キャッシュの実装: ハッシュ テーブルを使用してキャッシュを実装でき、キーと値のペアをハッシュ テーブルに格納することで、キャッシュ内のデータを一定時間で検索できます。
- データの一意性チェック: ハッシュ関数とハッシュ テーブルを使用してデータの一意性をチェックし、重複したデータが保存されていないことを確認できます。
シーケンシャル構造やバランスツリーでは要素キーとその格納場所との間に対応関係がないため、要素を検索する際にはキーを複数回比較する必要があります。逐次検索の時間計算量はO(N)
、バランスの取れたツリーではツリーの高さ、つまりO(log_2 N)
検索の効率は検索プロセス中の要素の比較の数に依存します。
理想的な検索方法: 比較することなく、検索対象の要素をテーブルから一度に直接取得できます。
格納構造を構築し、特定の関数 (hashFunc) を使用して要素の格納場所とそのキー コードの間に 1 対 1 のマッピング関係を確立すると、検索時にこの関数を通じて要素を迅速に見つけることができます。
この構造に入るとき:
要素を挿入します。
挿入する要素のキーコードに従って、この関数を使用して要素の格納場所を計算し、この場所に従って検索要素を格納します
。
要素のキーコードに対して同じ計算を実行し、取得した関数の値を要素の格納場所として使用し、構造体のこの位置にある要素を比較し、キーコードが等しい場合、検索は成功します。
この方式がハッシュ(hash)方式であり、ハッシュ方式で使用される変換関数をハッシュ(hash)関数と呼び、構築された構造をハッシュテーブル(Hash Table)(またはハッシュテーブル)と呼びます
。セット{1、7、6、4、5、9};
ハッシュ関数は次のように設定されます:hash(key) = key % capacity
容量は、要素を格納するための基礎となるスペースの合計サイズです。
この方法で検索すると、複数のキーコードを比較する必要がないので、検索速度が速くなりますが、剰余をとる際に、2つの数値の余りが同じ場合はどうすればよいのかという疑問が生じます。
ハッシュ衝突
ハッシュ テーブル (ハッシュ テーブル) 形式のストレージでは、ハッシュ競合とは、異なるキー (またはデータ) がハッシュ関数によって計算され、同じハッシュ値を取得して同じハッシュ テーブルに格納しようとする場合を指します。 (またはハッシュテーブルの場所)。ハッシュ関数は入力データを限られた数のバケットにマッピングし、入力データの数がバケットの数よりもはるかに大きい場合があるため、ハッシュの衝突はハッシュ テーブルでよくある問題です。
ハッシュの衝突により、次の問題が発生する可能性があります。
- データの上書き: 2 つの異なるキーが同じバケットの場所にマップされている場合、一方のキーのデータがもう一方のキーのデータを上書きし、データ損失が発生します。
- 検索効率の低下: 複数のキーが同じバケットの場所にマップされている場合、正しいキーを見つけるためにバケット内を検索する必要があるため、特定のキーを見つける効率が低下する可能性があります。
ハッシュの衝突を解決するために、ハッシュ テーブルは通常、次のいずれかの方法を使用します。
- 連鎖: この方法では、各バケットにリンクされたリスト、配列、または同じハッシュ値にマップされた複数の要素を格納するその他のデータ構造が格納されます。ハッシュの衝突が発生すると、既存の要素を上書きせずに、バケット内のリンクされたリストまたは配列に新しい要素が追加されます。
- オープン アドレッシング: この方法では、ハッシュの衝突が発生すると、アルゴリズムはハッシュ テーブル内で次に使用可能なバケットを探し、空きバケットが見つかるまでそのバケットにデータを挿入します。このプロセスには通常、線形検出、二次検出などの一連の検出方法が含まれます。
- 適切なハッシュ関数: 適切なハッシュ関数を選択すると、ハッシュ衝突の発生を減らすことができます。優れたハッシュ関数では、データを可能な限り均等にバケットにマッピングし、それによって衝突の可能性を低減する必要があります。
ハッシュの競合の処理は、ハッシュ テーブルの設計および実装時に考慮する必要がある重要な問題であり、アプリケーション シナリオが異なれば、競合解決戦略も異なる場合があります。合理的な競合処理方法により、ハッシュ テーブルのパフォーマンスと効率を向上させることができます。
ハッシュ関数
ハッシュ関数の設計原則:
ハッシュ関数のドメインには、保存する必要があるすべてのキーが含まれている必要があり、ハッシュ テーブルで m 個のアドレスが許可されている場合、その値の範囲は 0 ~ m-1 である必要があります。ハッシュ関数によって計算されたアドレスは、全体に均等に分散されます。空間では
、ハッシュ関数は比較的単純である必要があります
1. 直接アドレス指定方式
「ダイレクト アドレッシング」 (ダイレクト アドレッシング) は、シンプルで効果的なハッシュ (ハッシュ) テクノロジであり、キーと値のペアの保存と取得の問題を解決するためによく使用されます。直接アドレス指定方式では、考えられる各キーがバケット (またはスロット) に対応し、これらのバケットはキー範囲に従って割り当てられ、通常は配列として実装されます。
このアプローチの中心となる考え方は、各キーが特定のバケットに直接マッピングされるため、各キーが一意のバケット インデックスを持つため、理想的にはハッシュの衝突が発生しないということです。
直接アドレス指定方式の主な機能と制限は次のとおりです。
- スペースの複雑さ: この方法では、すべての可能なキーを収容できる十分な大きさのストレージ スペースを割り当てる必要があるため、スペースの複雑さはキーの範囲サイズによって異なります。キーの範囲が広い場合、メモリの消費量が多くなります。
- 競合解決: 各キーには一意のバケット インデックスがあるため、直接アドレス指定では通常、競合解決戦略は必要ありません。これにより、ストレージ操作と取得操作の両方の計算量が O(1) 定数となり、非常に効率的になります。
- 適用性: 直接アドレス指定は通常、キーの範囲が比較的小さく連続している場合に機能します。キーの範囲が非常に大きい場合、または連続していない場合、大量の記憶領域を割り当てる必要があるため、このアプローチは適切ではない可能性があります。
- 例: 一般的な例は、直接アドレス指定を使用して、カウンターなどの整数キーを含むデータ構造を実装することです。多数の整数値の出現を追跡する必要があるカウンターがある場合は、配列のインデックスが整数値であり、配列の値がその整数値の出現数である配列を使用できます。
// 使用直接定址法实现计数器
const int MAX_RANGE = 1000; // 假设整数范围在0到999之间
int counter[MAX_RANGE] = {
0};
// 增加某个整数值的计数
int key = 42;
counter[key]++;
要約すると、直接アドレス指定は、キーの範囲が比較的小さく連続している状況に適した、シンプルだが効果的なハッシュ方法です。これは、一定の時間複雑さで格納および取得操作を提供しますが、キーの範囲とメモリ消費量に注意する必要があり、検索が比較的小規模で継続的である状況に適しています。
2. 剰余による除算法
分割法は、キーをハッシュ テーブル バケット (スロット) または配列インデックスにマップするために使用される一般的なハッシュ手法です。基本的な考え方は、キーを適切な数値で割って余りを取ることでハッシュ値を計算し、そのハッシュ値をバケットのインデックスとして使用することです。適切な分布を確保するには、この余りは小さな正の整数 (通常は素数) にする必要があります。
剰余による除算の手順は次のとおりです。
- M で示される適切な約数 (通常は素数) を選択します。
- キー K のハッシュ計算: ハッシュ値 = K % M。
- ハッシュ値をバケット インデックスとして使用し、対応するバケットにデータを保存します。
この方法の利点は、シンプルで実装が簡単であることです。ただし、いくつかの制限と注意事項もあります。
- 適切な除数を選択する: 剰余による除算法では、適切な除数 M を選択することが重要です。適切な選択により、適切なハッシュ分散が確保され、ハッシュの衝突が回避されます。多くの場合、素数を選択すると、競合の可能性を減らすことができます。
- 均一な分散: 良好なハッシュ分散を実現するには、キーがキー空間全体に均一に分散される必要があります。キーが不均等に分散されている場合、一部のバケットが過密になり、他のバケットにはデータがほとんどまたはまったく存在しない可能性があります。
- 負の数の処理: 除算して剰余を残す方法では、通常、キーが正の整数である必要があります。負の数値をハッシュする必要がある場合は、負の数値を正の数値に変換するなど、いくつかの調整を検討できます。
- ハッシュ衝突の処理: 除算剰余法により衝突の可能性は減りますが、依然として衝突が発生する可能性があります。実際のアプリケーションでは、通常、チェーンやオープン アドレッシングなどの競合解決戦略を使用して競合を処理する必要があります。
3. スクエア・ミディアム法
「ミッドスクエア法」は単純な擬似乱数生成方法であり、通常は擬似乱数整数列を生成するために使用されます。その基本的な考え方は、整数を 2 乗し、中央の数値を次の整数とすることによって、擬似乱数を繰り返し生成することです。その単純さにもかかわらず、その品質は一般に、より複雑な擬似乱数生成アルゴリズムよりも劣ります。
以下は、正方形の中心を見つけるための基本的な手順です。
- 初期シード (または開始値) を選択します。通常は正の整数です。
- シードを二乗して、より大きな整数を取得します。
- この正方形の結果から、次の疑似乱数として中央の桁を取り出します。
- この新しい擬似乱数を次のラウンドのシードとして使用し、上記の手順を繰り返します。
キーワードが 1234 で二乗すると 1522756 になるので、中 3 桁の 227 をハッシュ アドレスとして抽出します。
例えば、キーワードが 4321 の場合、二乗すると 18671041 になります。中位 3 桁の 671 (または 710) をハッシュとして抽出します住所。
この方法の中心となるアイデアは、二乗と中央の数値の取得を繰り返して擬似乱数を生成することです。ただし、正方形の中心法の品質と均一性は、一般に、より複雑な擬似乱数生成アルゴリズムよりも劣るため、主に教育またはいくつかの単純なシミュレーション問題に使用されます。
二乗センタリング法のパフォーマンスと均一性は、初期シードの選択と抽出されるビット数によって決まります。シードの選択が適切でなかったり、抽出されたビット数が不適切な場合、擬似乱数シーケンスが周期的または不均一になる可能性があります。したがって、実際のアプリケーションでは、通常、線形合同生成器やメルセンヌ ツイスターなどのより高度で信頼性の高い擬似乱数生成器を使用して、高品質の擬似乱数を取得します。キーワードの数は不明であり、桁数もそれほど多くありません。
4. 折り方
フォールディング法は、大きな整数または長い文字列キーを小さなハッシュ値にマッピングして、ハッシュ テーブルまたはハッシュ テーブルの中央に格納できるようにするためによく使用されるハッシュ手法です。フォールディングの基本的な考え方は、入力キーを固定長の部分に分割し、その部分を追加するか他の数学的演算を実行してハッシュ値を生成することです。
折り方の一般的な手順は次のとおりです。
- 適切な分割サイズ (通常は正の整数) を選択します。このサイズはアプリケーションのニーズに基づいて選択でき、通常は入力キーを均等に分割できる値です。
- 入力キーを固定サイズの部分 (分割チャンク) に分割します。固定サイズの部分には、連続する文字、数字、またはその他の適切な単位を指定できます。入力が数値の場合、通常は同じ数の部分に分割されます。
- これらの部分に対して数学的演算 (通常は合計) を実行します。状況に応じて、加算、ビット演算などのさまざまな数学演算を選択してハッシュ値を生成できます。
- 最終結果はハッシュ値であり、ハッシュ テーブルまたはハッシュ テーブルのバケット インデックスとして使用できます。
以下は、folding を使用して整数キーをハッシュにマップする方法の簡単な例です。
入力キーが 1234567890 であると仮定すると、分割ブロック サイズ 3 を選択し、この整数を 1、234、567、および 890 に分割します。次に、部分を合計してハッシュ値を取得します: 1 + 234 + 567 + 890 = 1692。このハッシュ値は、データの保存と取得に使用できます。
折り畳み方法のパフォーマンスと均一性は、分割サイズと数学的演算の選択によって決まります。適切に選択すると、より良いハッシュ分布を提供できますが、衝突や不均一なハッシュ分布などの潜在的な問題を回避するには、パラメータを慎重に選択する必要があります。折りたたみ方法は、キーワードの分布が事前に分からないキーワードに適しています。ここで、桁数は比較的大きいです。
5. 乱数法
ランダム関数を選択し、キーワードのランダム関数値をハッシュ アドレスとして取得します。つまり、H(key) = random(key)
random は乱数関数です。
この方法は通常、キーワードの長さが異なる場合に使用されます。
6. 数学的分析
n d 個の数字があるとします。各数字には r 個の異なるシンボルがある可能性があります。各数字に現れるこれら r 個の異なるシンボルの頻度は同じではない可能性があります。それらはいくつかの桁に均等に分布している可能性があります。各シンボルの出現頻度は機会均等です。一部のビットに不均一な分布があり、特定のシンボルのみが頻繁に表示されます。ハッシュテーブルのサイズに応じて、さまざまなシンボルが均等に分散された数ビットをハッシュアドレスとして選択できます。
会社の従業員登録フォームを保存したいとします。携帯電話番号をキーワードとして使用すると、最初の 7 桁が同じである可能性が高く、最後の 4 桁をハッシュ アドレスとして選択できます。このような抽出の場合、作業は簡単です。競合が発生した場合は、抽出した数値を反転し(1234 から 4321 など)、右のリングをシフトし(1234 から 4123 など)、左のリングをシフトして、最初の 2 つの数値と最後の 2 つの数値を重ね合わせることができます。数値 (1234 から 4123 など)、12+34=46) およびその他の方法。
数値解析手法は通常、キーワードの桁数が比較的大きい場合の処理に適していますが、キーワードの分布が事前にわかっており、キーワードの数桁の分布が比較的均一である場合には、
ハッシュ関数がより高度に設計されるほど、ハッシュの衝突の可能性は低くなりますが、ハッシュの衝突を避けることはできません。
ハッシュ競合の解決
ハッシュの衝突を解決する 2 つの一般的な方法は、クローズド ハッシュとオープン ハッシュです。
1. クローズドハッシュ
クローズド ハッシュ: オープン アドレス指定方法とも呼ばれます。ハッシュの競合が発生したとき、ハッシュ テーブルがいっぱいではない場合、ハッシュ テーブルに空の位置が存在する必要があることを意味します。その場合、キーは競合位置に格納できます。空いている位置に
1.1 線形検出
たとえば、上記の概念で説明したシナリオでは、要素 44 を挿入する必要があります。まず、ハッシュ関数を通じてハッシュ アドレスを計算します。hashAddr は 4 なので、理論的にはこの位置に 44 を挿入する必要がありますが、この位置にはすでに 4 の要素が配置されているため、ハッシュの衝突が発生します。
線形検出: 競合が発生した位置から開始して、次の空き位置が見つかるまで逆方向に検出します。
入れる
ハッシュテーブルに挿入する要素の位置をハッシュ関数で取得し、
その位置に要素がない場合は直接新規要素を挿入し、その位置の要素とハッシュ競合がある場合は線形検出を使用して、ハッシュテーブルに挿入する要素の位置を取得します。次の空の位置を見つけて、新しい要素を挿入します。
削除
クローズド ハッシュを使用してハッシュの競合を処理する場合、ハッシュ テーブル内の既存の要素を物理的に削除することはできません。要素を直接削除すると、他の要素の検索に影響します。たとえば、要素 4 を直接削除すると、44 の検索が影響を受ける可能性があります。したがって、線形プローブでは、マークされた擬似削除を使用して要素を削除します。
ハッシュ テーブル内の各スペースには EMPTY マークが付けられます
。この位置は空です。この EXIST 位置にはすでに要素が存在します。DELETE 要素は削除されています。
enum State{
EMPTY, EXIST, DELETE};
線形検出の実装
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
HashData
ハッシュ テーブル内のデータ項目を表すテンプレート構造を定義します。これには 2 つの主要メンバーが含まれます。
_kv
: これはキーと値のペア (pair<K, V>
) であり、キー値に対応するデータを格納するために使用されます。_state
State
: これはデータ項目のステータスを表す 列挙型で、次の 3 つのいずれかになります。EMPTY
:スロットが空、つまりデータがないことを示します。EXIST
: スロットに有効なデータが含まれていることを示します。DELETE
:スロットに削除されたデータが含まれていることを示します。
この構造は、キーと値のペアをハッシュ テーブルに保存し、各スロットの状態を追跡するように設計されています。状態の存在_state
により、ハッシュ テーブルは挿入や検索だけでなく削除も処理できるようになります。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
HashFunc
あらゆるタイプのキーに使用できる汎用ハッシュ関数テンプレートを定義しますK
。このハッシュ関数の実装は非常に単純で、key
入力キーを直接size_t
型に変換して返します。
具体的には、このハッシュ関数の演算手順は次のとおりです。
K
タイプのキーをkey
入力パラメータとして受け入れます。- キー
key
を にキャストしますsize_t
。つまり、さまざまなタイプのキーを符号なし整数にマップします。 - 変換後の
size_t
値をハッシュ結果として返します。
すべてのタイプのキー、特にカスタム データ タイプでは機能しない可能性があることに注意してください。良好なハッシュ パフォーマンスと均一性を確保するには、より複雑なハッシュ関数が必要になる場合があります。
template<>
struct HashFunc<string>//对字符型特化给定值乘以固定质数131降低冲突
{
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
HashFunc
string () 型のキーを処理するためのハッシュ関数の特殊なバージョンを定義しますstring
。このハッシュ関数は、文字列の各文字を対応する整数値に変換し、それらを結合してハッシュ値を生成します。
この特殊バージョンのハッシュ関数がどのように機能するかは次のとおりです。
- 文字列内の各文字を反復処理します。
- 文字ごとに、現在のハッシュ値に固定素数 (131) を乗算し、文字の整数値を加算します。
- 文字列全体が走査されるまで、手順 1 と 2 を繰り返します。
- 最終的なハッシュ値を文字列ハッシュ結果として返します。
このハッシュ関数はシンプルかつ効率的であることが特徴で、文字列内の各文字をハッシュ値に考慮し、素数 131 を使用して混合 (STL ソース コードの実装を参照) することで、ハッシュ値の均一性を高めます。ハッシュ、セックス。この方法では、多くの状況で良好なハッシュ結果が得られます。
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
private:
vector<HashData<K, V>> _tables;
size_t _size = 0; // 存储多少个有效数据
};
HashTable
ハッシュ テーブル データ構造を実装するテンプレート クラスを定義します。このハッシュ テーブルには、キーと値のペアを格納できます。キーは type でK
、値は type でありV
、ハッシュ関数の種類はオプションで指定できます。デフォルトでは、HashFunc<K>
ハッシュ関数として使用されます。
このハッシュ テーブル クラスの主なメンバーと特徴は次のとおりです。
_tables
: これはHashData<K, V>
データ項目を格納するベクトル ( )でありvector
、ハッシュ テーブルの記憶領域を表します。各要素はハッシュ テーブル内のスロットに対応し、キーと値のペアを格納できます。ハッシュ テーブルのサイズは、ベクトルのサイズによって決まります。_size
: これは、有効なデータ項目の数を記録するカウンターで、現在ハッシュ テーブルに格納されている有効なキーと値のペアの数を示します。これは、挿入および削除操作中に更新されます_size
。- デフォルトのハッシュ関数: テンプレート パラメーターを使用して
Hash
、オプションでカスタム ハッシュ関数を指定できます。HashFunc<K>
カスタム ハッシュ関数タイプが指定されていない場合は、デフォルトのハッシュ関数がハッシュ関数として使用されます。これにより、異なる種類のキーに対して異なるハッシュ関数を使用できるようになります。
次のメンバー関数はすべてパブリックです
挿入関数
挿入機能では拡張性の問題を考慮する必要があり、ハッシュテーブル格納形式の拡張では負荷率の問題も考慮する必要があります。
ハッシュ テーブルの負荷係数は次のように定義されます。α=填入表中的元素个数/散列表的长度
α は、ハッシュ テーブルがどの程度満たされているかを示す係数です。テーブルの長さは固定値であるため、αは「テーブルに埋められる要素の数」に比例するため、αが大きいほどテーブルに埋められる要素が多くなり、競合する可能性が高くなります。 α 値が小さいほど、テーブルに埋められる要素が少なくなり、競合が発生する可能性が低くなります。実際、ハッシュ テーブルの平均検索長は負荷係数 α の関数ですが、衝突を処理する方法が異なれば機能も異なります。
オープンアドレッシング方式の場合、負荷率は特に重要な要素であり、0.7-0.8
以下に厳密に制限する必要があります。を超えると0.8
、テーブル ルックアップ中の CPU キャッシュ ミスが指数関数的に増加します。したがって、Java のシステム ライブラリなど、オープン アドレッシング方式を使用する一部のハッシュ ライブラリでは、負荷係数が 0.75 に制限されており、この値を超えると、ハッシュ テーブルのサイズが変更されます。
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
// 负载因子到了就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
// 旧表的数据映射到新表
for (auto e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
while (_tables[hashi]._state == EXIST)
{
hashi++;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
return true;
}
Insert
新しいキーと値のペアをハッシュ テーブルに挿入するために使用されるメソッドの実装。このメソッドの主な手順とロジックは次のとおりです。
- まず、メソッドを呼び出して、
Find
同じキーを持つデータ項目がすでに存在するかどうかを確認します。同じキーが存在する場合、挿入は失敗し、直接返されますfalse
。 - 次にハッシュテーブルの負荷率を確認します。
_size
負荷係数は、現在格納されている有効なデータ項目の数とハッシュ テーブルのスロットの数の比率です_tables.size()
。負荷率が指定されたしきい値 (7/10) を超えた場合、ハッシュ テーブルが過負荷になっており、拡張する必要があることを意味します。拡張の目的は、ハッシュ テーブルのパフォーマンスと均一性を維持することです。 - 拡張が必要な場合は、
newHT
現在のハッシュ テーブルの 2 倍のサイズで新しいハッシュ テーブルを作成します (現在のハッシュ テーブルが空の場合は 10 に初期化します)。次に、有効な各データ項目_tables
を新しいハッシュ テーブルに挿入するnewHT
呼び出しによって、古いハッシュ テーブルのデータがnewHT.Insert(e._kv)
新しいハッシュ テーブルにマッピングされます。 - データ マッピングが完了したら、関数を呼び出して
swap
古いハッシュ テーブル_tables
と新しいハッシュ テーブルを交換しnewHT
、新しいハッシュ テーブルを現在のハッシュ テーブルにします。 - 次に、挿入するキーのハッシュを計算します。挿入されるスロット インデックスは、ハッシュ関数を呼び出して
hash
キーをハッシュし、モジュロ演算を行うことによって決定されます。kv.first
hashi
- リニア プローブを使用して、使用可能なスロットを見つけます。
_tables[hashi]
現在のスロットのステータスが の場合はEXIST
、空のスロットが見つかるまで次のスロットの検索を続けます。 - 空のスロットが見つかると、キーと値のペアが
kv
スロットに保存され、EXIST
スロットに有効なデータが含まれていることを示すステータスがマークされます。次に、有効なデータ項目の数を増やします_size
。 - 最後に、return は
true
挿入が成功したことを示します。
検索機能
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t start = hash(key) % _tables.size();
size_t hashi = start;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
if (hashi == start)
{
break;
}
}
return nullptr;
}
Find
このメソッドの実装は、指定されたキーのデータ項目を検索し、対応するHashData<K, V>
ポインターを返すために使用されます。このメソッドの主な手順とロジックは次のとおりです。
- まず、現在のハッシュテーブルのサイズを確認し、ハッシュテーブルが空(
_tables.size()
0)の場合は検索できず、nullptr
見つからない旨を直接返します。 - ハッシュ関数オブジェクトを作成し
hash
、そのハッシュ関数を使用してkey
指定されたキーのハッシュ値を計算します。ハッシュ値は、モジュロ演算を通じて% _tables.size()
スロット インデックスを取得するために使用されstart
、検索を開始する位置を示します。 - ハッシュインデックスを初期化し、
hashi
slotからstart
検索を開始します。ループに入ります。 - ループ内では、まず
_tables[hashi]
現在のスロットのステータスを確認します。ステータスが の場合はEMPTY
、現在のスロットが空であることを意味し、指定されたキーが見つからず、次のスロットの検索が続行されることを示します。 - ステータスがそうでない場合は
EMPTY
、データ項目のステータスの確認を続けます_tables[hashi]._state
。ステータスが の場合はDELETE
、現在のスロットのデータが削除されたことを意味し、次のスロットの検索が続行されます。 - ステータスが でなく
EMPTY
、 でもない場合はDELETE
、現在のスロットに有効なデータが含まれていることを意味します。データ項目のキーが_tables[hashi]._kv.first
ターゲットのキーと等しいかどうかを引き続き確認しますkey
。等しい場合、指定されたキーが見つかり、データ項目へのポインタが返されます&_tables[hashi]
。 - 上記の条件がいずれも満たされない場合は、現在のスロットのデータがターゲット キーと一致しないことを意味し、次のスロットの検索が続行されます。ループ検索は、インクリメント
hashi
およびモジュロを取ることによって実装されます。_tables.size()
- ループは再び開始位置に到達するまで続きます
start
。これは、ハッシュ テーブル全体が 1 回走査され、一致するキーが見つからなかったことを示します。この時点でループを終了します。 - 最後に、一致するキーが見つからないままループ全体が終了した場合、return は
nullptr
見つからなかったことを意味します。
削除機能
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
Erase
このメソッドの実装は、指定されたキーに対応するデータ項目を削除するために使用されます。このメソッドの主な手順とロジックは次のとおりです。
- まず、
Find
メソッドを呼び出して、key
指定されたキーに対応するデータ項目を検索します。一致するデータ項目が見つかった場合、Find
メソッドはデータ項目へのポインタを返し、それを に保存しますret
。一致するデータ項目が見つからない場合、Find
メソッドは を返しますnullptr
。 - 次に、
ret
が null 以外のポインタかどうかを確認します。が空でない場合はret
、一致するデータ項目が見つかったので、削除操作を実行できることを意味します。 - 削除操作では、一致するデータ項目のステータスを
_state
に設定しDELETE
、データ項目が削除されたことを示します。 - 同時に、ハッシュ テーブル内の有効なデータ項目の数が
_size
削除を反映して減分されます。 - 最後に、return は
true
削除が成功したことを示します。 - が空の場合(つまり、一致するデータ項目が見つからない場合)、削除が失敗したことを示すために
ret
返されます。false
このErase
メソッドは、実際にハッシュ テーブルからデータを削除するのでDELETE
はなく、ステータスを削除ステータスを示すものとしてマークすることによって、ハッシュ テーブル内のデータ項目のトゥームストーン削除を実装します。このアプローチにより、ハッシュ テーブルの整合性を維持しながら、検索中に削除されたデータをスキップできます。
すべてのコード
#pragma once
#include<iostream>
using namespace std;
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>//对字符型特化给定值乘以固定质数131降低冲突
{
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
// 负载因子到了就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
// 旧表的数据映射到新表
for (auto e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
while (_tables[hashi]._state == EXIST)
{
hashi++;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
return true;
}
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t start = hash(key) % _tables.size();
size_t hashi = start;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
if (hashi == start)
{
break;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
void Print()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._state == EXIST)
{
printf("[%d:%d] ", i, _tables[i]._kv.first);
}
else
{
printf("[%d:*] ", i);
}
}
cout << endl;
}
private:
vector<HashData<K, V>> _tables;
size_t _size = 0; // 存储多少个有效数据
};
線形検出の欠点: ハッシュの競合が発生すると、すべての多くの比較が必要となるため、検索効率が低下します。
1.2 二次検出
線形検出の欠点は、空き位置を 1 つずつ見つける方法であるため、次の空き位置を見つけるのに関連して、相反するデータが一緒に蓄積されることです。次の空の位置を検索します。空の位置のメソッドは次のとおりです: H_i = (H_0 + i^2 )% m
、または: H_i = (H_0 - i^2)% m
。このうち、i =1,2,3…, H_0
は要素のキーコードをハッシュ関数Hash(x)で計算して得られる位置、mはテーブルのサイズです。
線形プローブの補間関数を変更します。
Hash hash;
size_t start = hash(kv.first) % _tables.size();
size_t i = 0;
size_t hashi = start;
// 二次探测
while (_tables[hashi]._state == EXIST)
{
++i;
hashi = start + i*i;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
コードの主な手順とロジックは次のとおりです。
- ハッシュ関数オブジェクトを作成します
hash
。 - 指定されたキーのハッシュ値を計算し
kv.first
、モジュロ演算を使用して、検索を開始する位置を示す% _tables.size()
スロット インデックスを取得します。start
i
試行回数を追跡するために整数を初期化し、検索される現在のスロットを表すhashi
ように初期化します。start
- ループに入り、二次プローブを使用して利用可能なスロットを見つけます。反復ごとに、 は増分してから、によって計算される
i
新しいハッシュ インデックスを計算します。次に、モジュロ演算を使用して、ハッシュ インデックスがハッシュ テーブルのサイズを超えないようにします。hashi
start + i*i
% _tables.size()
_tables[hashi]
現在のスロットのステータスを確認します。ステータスが の場合はEXIST
、スロットがすでに占有されていることを意味し、次の繰り返しに進み、次のスロットを試します。_tables[hashi]._state
空のスロットが見つかった場合EXIST
、そのスロットはデータを保存できることを意味します。キーと値のkv
ペアをスロットに保存し、EXIST
スロットに有効なデータが含まれていることを示すステータスをマークします。- 有効なデータ項目の数を増加させ
_size
、データの挿入が成功したことを示します。
二次プローブを使用すると、データ項目がより均等に分散され、線形プローブで見られるクラスタリング効果が軽減されます。
テーブルの長さが素数で、テーブルの読み込み係数 a が 0.5 を超えない場合、位置を 2 回探索することなく新しいエントリを挿入できます。したがって、テーブルに半分の空きポジションがある限り、テーブルがいっぱいになる問題は発生しません。検索時にはテーブルがいっぱいであることを考慮する必要はありませんが、挿入時にはテーブルの負荷率 a が 0.5 を超えないようにする必要があり、超えた場合は容量の増加を検討する必要があります。したがって、クローズド ハッシュの最大の欠点は、スペース使用率が比較的低いことであり、これはハッシュの欠点でもあります。
2. オープンハッシュ
オープンハッシュ方式はチェーンアドレス方式(オープンチェーン方式)とも呼ばれ、まずハッシュ関数を用いてキーコードセットのハッシュアドレスを計算し、同じアドレスを持つキーコードは同じサブセットに属し、それぞれが同じサブセットに属するものとする。サブセットはバケットと呼ばれ、バケット内の要素は単一リンク リストを通じてリンクされ、各リンク リストのヘッド ノードはハッシュ テーブルに格納されます。
オープン ハッシュ内の各バケットには、ハッシュの競合がある要素が含まれています。
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{
}
};
HashNode
ハッシュ テーブル内のノードを表す構造を定義します。このノードには次のメンバーが含まれます。
_kv
: これは、pair<K, V>
ノードにキーと値のデータを格納するために使用されるキーと値のペア ( ) です。_next
: これは次のノードへのポインタであり、リンク リスト構造を構築し、ハッシュの競合を処理するために使用されます。ハッシュの競合が発生した場合、複数のノードが同じハッシュ バケット (スロット) にマッピングされる可能性があり、_next
リンク リストのポインタを使用して同じハッシュ値を持つノードが接続されます。
チェーンアドレス方式では、各ハッシュバケット(スロット)はリンクリストを保持しており、複数のキーが同じスロットにマッピングされる場合、それらは順番にリンクリストに追加され、ポインタを介して接続されます_next
。このようにして、複数のキーと値のペアを同じハッシュ バケットに保存できるため、ハッシュの競合の問題が解決されます。キーと値のペアを検索または削除する必要がある場合は、リンク リストをたどって特定のノードを見つけることができます。このチェーン アドレス方式の実装により、ハッシュ テーブルでデータを効果的に管理し、効率的なパフォーマンスを維持できるようになります。
template<class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};
キーと値のペアのデータを保存するために使用されるハッシュ テーブルを定義するテンプレート クラスHashTable
。このクラスの主なメンバーとプロパティは次のとおりです。
typedef HashNode<K, V> Node;
HashNode<K, V>
: これは、コードの可読性を向上させるために に簡略化された型エイリアス宣言ですNode
。private:
: これはプライベートアクセス識別子であり、以下のメンバー変数およびメソッドがクラスのプライベートメンバーであり、外部から直接アクセスできないことを示します。vector<Node*> _tables;
vector
:ハッシュテーブルのハッシュバケット(スロット)を格納するコンテナです。各要素はHashNode<K, V>
タイプのポインターであり、リンクされたリストのヘッド ノードです。このコンテナにはハッシュ テーブルのすべてのデータが格納されます。size_t _size = 0;
: ハッシュテーブル内の有効なデータの数を記録するために使用されるカウンターです。ハッシュ テーブルでの挿入、削除、その他の操作中に、このカウンターは正確なデータ量を維持するために更新されます。
HashTable
このクラスの機能は、ハッシュ テーブル データ構造を実装し、キーと値のペア データの保存をサポートし、挿入、検索、削除などの基本操作を提供することです。ハッシュ テーブルはチェーン アドレス方式を使用してハッシュの競合を解決し、 を使用してvector
ハッシュ バケットを保存し、各バケットはデータを保存するためのリンク リストに対応します。さらに、_size
有効なデータの量を追跡し、負荷係数とハッシュ テーブルの自動拡張の管理に役立ちます。
挿入関数
クローズド ハッシュと同様に、挿入時に拡張の問題を考慮する必要があるため、バケットの数は確実です。要素が挿入され続けると、各バケット内の要素の数は増加し続けます。極端なケースでは、バケットが不足する可能性があります。リンクリストに多くのノードがあり、ハッシュテーブルのパフォーマンスに影響を与えるため、特定の条件下ではハッシュテーブルを拡張する必要がありますが、この条件を確認するにはどうすればよいですか? ハッシュ化に最適な状況は、各ハッシュ バケットにノードが 1 つだけ存在することです。要素の挿入を続けると、毎回ハッシュの競合が発生します。したがって、要素の数がバケットの数と正確に等しい場合、次のことが可能です。ハッシュテーブルの容量拡張を行う
bool Insert(const pair<K, V>& kv)
{
// 去重
if (Find(kv.first))
{
return false;
}
// 负载因子到1就扩容
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
newTables.resize(newSize, nullptr);
// 旧表中节点移动映射新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_kv.first % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = kv.first % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
- まず、同じキーを持つノードがすでに存在するかどうかを確認します。つまり、を呼び出してキーがハッシュ テーブルにすでに存在するかどうかを
Find(kv.first)
確認します。kv.first
すでに存在する場合は、false
重複キーが許可されていないために挿入が失敗したことを示す を返します。 _size
次に、ハッシュ バケットの数に対する格納されたデータの数の比率である負荷率を確認します_tables.size()
。負荷率が 1 に達すると (各ハッシュ バケットに平均して 1 つのデータが格納されることを意味します)、容量拡張操作が実行されます。- 拡張が必要な場合は、まず新しいハッシュ テーブルのサイズを計算します
newSize
。現在のハッシュ テーブルが空 (つまり_tables.size()
0) の場合は、新しいサイズを 10 に設定し、それ以外の場合は、新しいサイズを現在のサイズの 2 倍に設定します。次に、新しいvector
コンテナを作成しnewTables
、そのサイズを に設定しnewSize
、すべての要素を に初期化しますnullptr
。 - 現在のハッシュ テーブルの各スロットをスキャンし
_tables
(各スロットはリンク リストに対応します)、リンク リスト内のノードを新しいハッシュ バケットに再マップします。具体的な操作は、リンク リストを走査し、ハッシュ関数を使用して各ノードの key の新しいスロット インデックスを計算し、hashi
(ヘッド挿入メソッドを使用して) ノードを新しいハッシュ バケットに挿入し、_next
構築するノードのポインタを更新することです。リンクされたリスト。完了したら、古いスロットを に設定しますnullptr
。 - 最後に、
swap
操作を使用して古いハッシュ テーブルと新しいハッシュ テーブルを交換し、古いハッシュ テーブルを新しいハッシュ テーブルに置き換えて拡張操作を完了します。 - 拡張が必要ない場合 (負荷係数が 1 に達しない場合)、 key のハッシュ値を計算し、
hashi
挿入するスロットを決定し、ヘッド挿入メソッドを使用して、対応するスロットのリンク リストに新しいノードを挿入します。 。有効なデータの数を増やして_size
、挿入が成功したことを示す を返しますtrue
。
このコードの中心となるアイデアは、ハッシュ テーブルの負荷率を維持し、負荷率が高すぎてハッシュ テーブルのパフォーマンスを維持できない場合に拡張操作をトリガーすることです。同時に、リンクされたリストを通じてハッシュの競合を処理し、複数のキーが同じハッシュ バケットにマップされる状況をサポートします。キーの一意性を確保するために、既存のキーに対しては挿入操作が実行されないことに注意してください。
検索機能
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
- まず、ハッシュ テーブルが空であるかどうかを確認します
_tables.size() == 0
。ハッシュ テーブルが空の場合はデータがないことを意味し、nullptr
データが見つからないため直接返されます。 - キーのハッシュ値を計算し、キーのモジュロ演算
hashi
を通じてスロット インデックスを取得し、検索するハッシュ バケットを決定します。key
% _tables.size()
cur
選択したハッシュ バケット_tables[hashi]
内のヘッド ノード、つまりリンク リストの開始位置を指すようにポインターを初期化します。- ループに入り、リンクされたリスト内のノードを走査します。各反復で、
cur
現在のノードが空かどうかを確認します。空の場合は、リンク リストの最後まで移動し、一致するキーが見つからなかったことを意味し、返された場合は、キーが見つからなかったことを意味しますnullptr
。 - ノードが
cur
空でない場合は、現在のノードのキーと値のペアのキーが_kv.first
ターゲット key と等しいかどうかの確認を続けますkey
。それらが等しい場合は、一致するキーと値のペアが見つかったことを意味し、cur
データにアクセスまたは変更するために現在のノードへのポインターが返されます。 - 現在のノードが一致しない場合は、
cur
次のノードを指します。つまりcur = cur->_next
、リンク リスト内の次のノードの検索が続行されます。 - 一致するノードが見つかるか、リンクされたリスト全体が走査されるまでループします。
削除機能
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false; // 哈希表为空,无法删除
}
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr; // 用于记录当前节点的前一个节点
// 遍历链表
while (cur)
{
if (cur->_kv.first == key)
{
// 找到匹配的节点,进行删除操作
if (prev)
{
prev->_next = cur->_next; // 从链表中移除当前节点
}
else
{
// 如果当前节点是链表的头节点,则更新哈希桶的头指针
_tables[hashi] = cur->_next;
}
delete cur; // 释放当前节点的内存
--_size; // 减少有效数据个数
return true; // 删除成功
}
prev = cur;
cur = cur->_next; // 移动到下一个节点
}
return false; // 未找到匹配的键,删除失败
}
- 現在のノードがリンク リストのヘッド ノードでない場合は、前のノード
prev
のポインタ_next
を現在のノードの次のノードにポイントし、それによってリンク リストから現在のノードを削除します。 - 現在のノードがリンク リストのヘッド ノードである場合、リンク リストのヘッドが正しく更新されるように、ハッシュ バケットのヘッド ポインタを
_tables[hashi]
現在のノードの次のノードに直接更新します。 - 現在のノードのメモリを解放し、有効なデータの数を減らします
_size
。 - 戻る
true
ということは、削除が成功したことを意味します。 - 一致するキーが見つからない場合、最終的な戻り値は
false
削除が失敗したことを示します。
デストラクター
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
free(cur);
cur = next;
}
_tables[i] = nullptr;
}
}
ハッシュ テーブルの各スロットを反復処理し、リンク リスト内のノードを解放してから、スロットを に設定してnullptr
、割り当てられたすべてのメモリが確実に解放されるようにします。コードの主なロジックは次のとおりです。
- ループを使用して、ハッシュ テーブルのすべてのスロットを格納するコンテナ
for
を走査します。_tables
_tables
- 各反復で、現在のスロット
_tables[i]
に対応するリンク リストのヘッド ノードが取得されますNode* cur
。 - 内側の
while
ループに入り、リンクされたリスト内の各ノードを走査します。各反復では、まず次のノード ポインターがNode* next
現在のノードの直後のノードに設定されます。 - この関数を使用して、
free
現在のノードcur
が占有しているメモリを解放します。オブジェクトのメモリ割り当ては、free
の代わりに、または同様の関数を通じて行われる可能性があるdelete
ため、ここでは の代わりに関数が使用されていることに注意してください。cur
malloc
new
- 現在のノードを
cur
次のノードにポイントしてnext
、リンク リストの移動を続けます。 - リンク リストにノードがなくなるまでループします。つまり、
cur
になりますnullptr
。 - 各反復の後、現在のスロットは
_tables[i]
に設定されnullptr
、スロットにノードが含まれないことが保証されます。
すべてのコード
#pragma once
#include<iostream>
using namespace std;
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{
}
};
template<class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
free(cur);
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
// 去重
if (Find(kv.first))
{
return false;
}
// 负载因子到1就扩容
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
newTables.resize(newSize, nullptr);
// 旧表中节点移动映射新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_kv.first % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = kv.first % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false; // 哈希表为空,无法删除
}
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr; // 用于记录当前节点的前一个节点
// 遍历链表
while (cur)
{
if (cur->_kv.first == key)
{
// 找到匹配的节点,进行删除操作
if (prev)
{
prev->_next = cur->_next; // 从链表中移除当前节点
}
else
{
// 如果当前节点是链表的头节点,则更新哈希桶的头指针
_tables[hashi] = cur->_next;
}
delete cur; // 释放当前节点的内存
--_size; // 减少有效数据个数
return true; // 删除成功
}
prev = cur;
cur = cur->_next; // 移动到下一个节点
}
return false; // 未找到匹配的键,删除失败
}
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};
オープンハッシュとクローズドハッシュの比較
チェーン アドレス方式では、オーバーフローを処理するためにリンク ポインターを追加する必要があり、ストレージのオーバーヘッドが増加するようです。実際:オープン アドレス方式では、検索効率を確保するために大量の空き領域を維持する必要があるため、たとえば、2 次探索方式では、負荷係数 a <= 0.7 が必要であり、テーブル エントリはポインタよりもはるかに大きな領域を占有します。したがって、チェーンアドレス方式を使用すると、オープンアドレス方式よりもストレージスペースが節約されません。