【高次データ構造】ハッシュ(ハッシュテーブル、ハッシュバケット)

⭐ブログのホームページ: ️CS セミのホームページ
⭐フォロー歓迎: いいね、お気に入り、メッセージを残す
⭐ コラム シリーズ: 高度な C++
⭐ コード リポジトリ: 高度な C++
ホーム 人々が更新するのは簡単ではありません。あなたの「いいね!」と関心は私にとって非常に重要です。友達、いいね、フォローしてください。あなたのサポートが私の創作の最大の動機です。友達は質問するためにプライベートメッセージを送信することを歓迎します。家族メンバーの皆さん、忘れないでくださいいいね&集めて+フォロー! ! !


1. ハッシュの概念

シーケンシャル構造とバランスツリーでは要素キーとその格納場所の間に対応関係がないので、要素では、キー コードを複数回比較する必要があります。逐次探索の時間計算量は O(N) です。バランスの取れたツリーでは、それは木の高さ、つまり O( l o g 2 N log_2 N log2 N)、検索の効率は、検索プロセス中の要素の比較の数によって決まります。

理想的な検索方法: 比較を行わず、 検索対象の要素をテーブルから一度に直接取得します、つまり、検索の時間計算量は O(1) です。

記憶構造が構築されている場合、特定の関数 (hashFunc) を通じて、 の場合、検索中にこの機能を使用して要素をすばやく見つけることができます。 要素の記憶場所とそのキー コードの間に 1 対 1 のマッピング関係を確立できます。

この構造に入るとき:

  • 要素を挿入

挿入する要素のキー コードに従って、この関数を使用して要素の格納場所を計算し、それに従って格納します。この場所へ。

  • 検索要素

要素のキー コードに対して同じ計算を実行します。取得した関数値を要素の格納場所とみなして、構造内のこの場所に従って要素を比較します。 < a i=2>、キー コードが等しい場合、検索は成功します。

このメソッドはハッシュ (ハッシュ) メソッドです。ハッシュメソッドで使用される変換関数はハッシュ (ハッシュ) 関数と呼ばれます。 で構築される構造をハッシュテーブル(またはハッシュテーブル)と呼びます。

ここに画像の説明を挿入します

2. ハッシュ衝突

異なるキーワードが同じハッシュ関数を使用して同じハッシュ アドレスを計算するこの現象は、ハッシュ競合またはハッシュ衝突と呼ばれます。

非常に単純な例では、次の図に示すように、データ情報に数値 45 が追加された場合、10 を法としてそれは 5 になり、元の 5 とあいまいになり、2 つは同じ位置にマッピングされます。これにより、ハッシュ衝突/ハッシュ衝突が発生します。
ここに画像の説明を挿入します

3. ハッシュ関数

ハッシュ競合の原因の 1 つは次のようなものです。ハッシュ関数の設計が十分に合理的ではありません。

ハッシュ関数の設計原則

  1. ハッシュ関数のドメインには、保存する必要があるすべてのキーが含まれている必要があり、ハッシュ テーブルで m 個のアドレスが許可されている場合、その値の範囲は 0 ~ m-1 である必要があります。
  2. ハッシュ関数によって計算されたアドレスは空間全体に均等に分散されます。
  3. ハッシュ関数は比較的単純である必要があります

一般的なハッシュ関数は次のとおりです。

  1. 直接アドレス指定方法 (一般的に使用されます)
    キーワードの一次関数をハッシュ アドレスとして取得します。ハッシュ(キー)= A*Key + B
    利点: シンプルで均一
    欠点: キーワードの分布を事前に知る必要がある a >
    使用シナリオ: 比較的小規模で継続的な状況を見つけるのに適しています

  2. 剰余による除算方法 (一般的に使用されます)
    ハッシュ テーブルで許可されるアドレスの数が m であると仮定し、それより大きくないものを選択しますm よりも近いが、最も近い または、ハッシュ関数に従って、m に等しい素数 p が除数として使用されます: Hash(key) = key% p(p<=m), キー コードをハッシュ アドレスに変換します

  3. 中二乗法
    キーワードが 1234 で、その二乗が 1522756 であるとします。中央の 3 桁 227 をハッシュ アドレスとして抽出します。別の例としては、キーワードは4321、その二乗は1522756です。二乗は18671041で、真ん中の3桁671(または710)がハッシュアドレスとして抽出されます。
    正方形-中央方式の方が適しています。キーワードの分布が不明であり、桁数がそれほど大きくない

  4. 折り畳む方法
    折り畳む方法は、キーワードを左から右へ同じ桁数でいくつかの部分に分割し (最後の部分は短い桁でも構いません)、これらの部分を重ね合わせます。合計を計算し、ハッシュ テーブルの長さに応じて最後の数桁をハッシュ アドレスとして取得します。
    折りたたみ方法は、キーワードの分布を事前に知る必要がない状況や、キーワードの桁数が多い状況に適しています

  5. 乱数法
    ランダム関数を選択し、キーワードのランダム関数値をハッシュ アドレスとして取得します。つまり、H(key) = random(key) となります。ランダムは乱数関数です。
    この方法は通常、キーワードの長さが異なる場合に使用されます

  6. 数学的分析方法
    n d 桁があるとします。各桁には r 個の異なる記号がある可能性があります。各桁に現れる r 個の異なる記号の頻度は必ずしも同じではありません。同じです。 、一部のビットの分布は比較的均一で、各シンボルが出現する機会は等しいが、一部のビットの分布は不均一で、特定のシンボルのみが頻繁に出現する可能性があります。 ハッシュ テーブルのサイズに応じて、さまざまなシンボルが均等に分散された複数のビットをハッシュ アドレスとして選択できます。例:

ここに画像の説明を挿入します
会社の従業員登録フォームを保存したいとします。携帯電話番号をキーワードとして使用すると、最初の 7 桁が同じである可能性が高く、最後の 4 桁をハッシュ アドレスとして選択できます。このような抽出の場合、作業は簡単です。競合が発生した場合は、抽出した数値を反転し(1234 から 4321 など)、右のリングをシフトし(1234 から 4123 など)、左のリングをシフトして、最初の 2 つの数値と最後の 2 つの数値を重ね合わせることができます。数値 (1234 から 4123 など)、12+34=46) およびその他の方法。

数値解析手法は通常、キーワードの桁数が比較的大きい場合の処理​​に適していますが、キーワードの分布が事前にわかっており、キーワードの数桁の分布が比較的均一である場合には、

4. ハッシュ競合の解決

ハッシュの衝突を解決する一般的な方法は次の 2 つです。クローズド ハッシュとオープン ハッシュ

1. クローズド ハッシュ - オープン アドレッシング方式

オープン アドレッシング方式とも呼ばれます。ハッシュの競合が発生した場合、ハッシュ テーブルがいっぱいでない場合は、ハッシュ テーブルに空の位置が存在する必要があり、キーを「次の」空の位置に格納できます。衝突位置にあります。

(1) 線形探査

競合が発生した位置から開始して、次の空の位置が見つかるまで後方に調査します。

以下に示すように値 41 を挿入し、線形探索を進めて、座るための空の位置を見つけてみましょう。
ここに画像の説明を挿入します

Hi = (H0 + i) % m(i=1,2,3……)

Hi: 線形探索を通じて競合する要素によって見つかった空の位置。
H0: 要素のキー コードのハッシュ関数によって計算された位置。
m: ハッシュ テーブルの容量

一目で問題がわかりますが、データを入れすぎたり、余りをとって同じデータを繰り返し挿入したりすると、線形探索時間が多くなり、容量不足になる可能性はありませんか? ?写真で見てみましょう:

ここに画像の説明を挿入します

上に示したように、1000 は 2 回競合し、101 は 2 回競合し、40 は 4 回競合していることがわかりました。線形探索では検索するものが多すぎるため、次のようになります。

限られたスペースにデータを挿入します。スペースに要素が多くなると、要素の挿入時に競合が発生する可能性が高くなります。複数の競合の後、ハッシュ テーブルの要素が挿入されます。< a i=1>検索効率も低下します。この間に、負荷係数 (負荷係数) がハッシュ テーブルに導入されます。

負荷率 = ハッシュテーブルの有効データ数/総容量

ここに画像の説明を挿入します

ここに画像の説明を挿入します
上の図の拡張後のハッシュの競合は大幅に減少していますが、多くのスペースが無駄になっていることがわかります。これが上で説明した負荷率の問題です。負荷率は 0.7 の範囲にあることを保証する必要があります。 -0.8。

要約すると:
線形探索の利点: シンプルで理解しやすいです。
線形探索の欠点: ハッシュの競合が発生すると、すべての競合が結びつき、データが簡単に「蓄積」される可能性があります。 < a i=4 > では、さまざまなデータが積み重なりやすく、後で空き位置を見つける必要があり、空き位置を見つけるために複数の比較が必要になり、最後に負荷率の制御も困難になります。

(2) 二次探査

線形検出の欠点は、矛盾するデータが一緒に蓄積されることです。これは、次の空の位置を見つけることに関連しています。空の位置を見つける方法は、空の位置を 1 つずつ見つけることであるため、二次検出はこれを回避することです。質問です。次の空の位置を見つける方法は次のとおりです。

+i 計算された位置からの数値

ハイハイHi = ( H 0 H_0 H0 + i 2 i^2 2 )% m(i=1,2,3……)

H0H_0H0 は要素のキーコード key をハッシュ関数 Hash(x) で計算して得られる位置です。
m 是哈希表的大小。
H i Hi Hi は、二次検出後に取得された競合要素の格納場所です

ここに画像の説明を挿入します

二次検出は、ハッシュ競合を生成するデータの次の場所を見つけるために使用されます。線形検出と比較して、二次検出を使用するハッシュ テーブル内の要素の分布は比較的まばらになるため、データの蓄積につながる可能性が低くなります。ただし、10 個のスペースでは確かに少しまばらですが、それでも何度も比較しました。20 個のスペースに拡張して見てみましょう。

ここに画像の説明を挿入します

研究によると: テーブルの長さが素数で、テーブルの読み込み係数 a が 0.5 を超えない場合、新しいテーブル エントリは確実に挿入でき、どの位置にも挿入されません。二度探索されます。したがって、テーブルに半分の空きポジションがある限り、テーブルがいっぱいになる問題は発生しません。検索時は表がいっぱいであることを考慮する必要はありませんが、挿入時は表の負荷率aが0.5を超えないようにする必要があり、超えた場合は容量の増加を検討する必要があります。

2. オープンハッシュチェーンアドレス方式

オープンハッシュ方式はチェーンアドレス方式(オープンチェーン方式)とも呼ばれ、まずハッシュ関数を用いてキーコードセットのハッシュアドレスを計算し、同じアドレスを持つキーコードは同じサブセットに属し、それぞれが同じサブセットに属するものとする。サブセットはバケットと呼ばれ、バケット内の要素は単一リンク リストによってリンクされ、各リンク リストのヘッド ノードがハッシュ テーブルに格納されます。

ここに画像の説明を挿入します

もちろん、ハッシュ バケットを開くことでハッシュ バケットを拡張することもできます。開いた空間の各ノードにヘッド ノードがぶら下がっており、各ヘッド ノードの下に異なる番号がぶら下がっている場合、新しい番号が挿入されるたびに、その値がハッシュの競合が発生し、その下に新しい数値をハングする必要があります。これは問題ありませんが、ハッシュのパフォーマンスが低下しているため、新しく挿入された値の一部が反映されるように容量を拡張できます。は、後続の値バケットの下に配置されます。

3. 閉鎖型と分散型の違い

クローズド ハッシュのオープン アドレッシング方式の場合、負荷係数は 1 を超えることはできず、一般に [0.0, 0.7] の間で制御することが推奨されます。
オープン ハッシュのハッシュ バケットの場合、負荷係数は 1 を超える可能性があります。一般に、[0.0, 1.0] の間で制御することをお勧めします。

したがって、実際の生活では、オープン ハッシュを使用するハッシュ バケットがさらに多くなります。これは、オープン ハッシュのハッシュ バケットの負荷係数が非常に大きくなる可能性があるため、つまり、より多くの値を収容できるためであり、ハッシュ バケットの極端な場合には (挿入されたすべてのデータは 1 つのバケット内にあります) 次のような解決策もあります。

ここに画像の説明を挿入します

上記の状況はすべて 1 つのバケット内にあるため、検索効率は非常に低く、O(N) です。そのため、リンク リストを使用する代わりに、赤黒ツリーのストレージ モードに従ってこれらの値を挿入するアプローチです。モード:

ここに画像の説明を挿入します

したがって、たとえ 10 億のデータがあるとしても、この値を見つけるために必要なのは 30 回のトラバースだけです。
したがって、一部のコンパイラでは、ハッシュ バケットに大量のデータが挿入されると、たとえば新しい JAVA コンパイラでは、8 個を超えるデータが下にハングすると、赤黒になります。 Tree. が 8 以下の場合でも、単一リンク リストです。

5. ハッシュテーブルのクローズドハッシュ実装

1. ハッシュテーブルの構造

クローズド ハッシュ ハッシュ テーブルでは、指定されたデータを保存することに加えて、ハッシュ テーブル内の各場所にその場所の現在の状態も保存する必要があります。ハッシュ テーブル内の各場所の可能な状態は次のとおりです。

0、EMPTY: この位置にはデータがなく、空です。
1. EXITS: この場所にはデータがあり、空ではありません。
2. 削除: この位置には元々値がありましたが、削除されました。値はなく空です。

(1) 疑わしい質問 – なぜ国家でなければならないのか?なぜ 3 つの州があるのでしょうか?

そこで疑問が生じます: なぜこれら 3 つの状態が必要なのでしょうか? 空かどうかを直接判断することはできないのでしょうか? なぜ 2 つの状態を与えればよいのでしょうか?

まずはステータスを説明

思い出してください。テーブルに要素が存在するかどうかを調べたい場合はどうすればよいでしょうか?たとえば、以下のハッシュ テーブルで、要素 40 の位置を見つけたいとします。

ここに画像の説明を挿入します

線形探索を使用して、剰余メソッドで除算することにより、ハッシュ テーブルの要素 40 のハッシュ アドレスが 0 であることを見つけます。添字 0 から逆方向に検索し、40 が見つかった場合は存在します。空の位置が見つかった場合、このテーブルには 40 は存在しません。

ハッシュの意味は、剰余を取得した後にハッシュの競合が発生した後、空のスペースを逆方向に挿入することです。線形検出では、競合する要素の次の位置を見つけるときに、順番に逆方向に検索するため、空の位置が見つかったので、この空の位置の後ろの添え字 0 の位置から始まる競合する要素はなくなることを意味します。

別の例として、今数値 80 を探しているとします。アドレス 0 から遡って検索すると、5 番目の空の位置が空であることがわかります。何をしても 80 と 40 が一致するため、すぐに 80 は存在しないと判断します。アドレス 0 から遡って検索が実行されるため、最初の空の位置が見つかったときにこの値が存在してはなりません。

では、私たちのステータスは何でしょうか?不在を表すには 0 を使用し、存在を表すには 1 を使用しますか?一見何の問題もないように見えますが、非常に深刻な問題があります。では、入力した数字自体は 1 ですか、それとも 0 ですか?これは大きな間違いではないでしょうか?したがって、状態を使用します。

3つの状態があることを説明する

次に、削除操作を使用して値 1000 を削除します。このときのハッシュ テーブルがどのように見えるかを見てみましょう。

ここに画像の説明を挿入します

この時点で値 40 が存在するかどうかを調べてみましょう。今、状態は EMPTY と EXITS の 2 つだけだと言いました。次に、アドレス 0 から開始し、剰余法に従って逆方向に検索します。位置 2 がEMPTYとマークされた空の位置ですが、前述のハッシュテーブルの構造によれば、空の位置に遭遇した場合、直接存在しないと判断されるので、大きな間違いではありませんか? 明らかに40という値があります。後で!そこで、ここでは DELETE ボタンを紹介します。これは、以前は存在していましたが削除されたことを意味します。この穴をスキップして、後で探し続けてください。

ここに画像の説明を挿入します

このように、ハッシュ テーブル内の要素を検索するときに、現在位置の要素が検索対象の要素と一致しないが、現在位置のステータスが EXITS または DELETE の場合は、後で検索を続行する必要があります。 . 要素を挿入するときは、ステータスが EMPTY または DELETE の位置に要素を挿入できます。

(2) ステータスと負荷率

したがって、私たちのステータスは次のとおりです。

// 标记状态
enum State
{
    
    
	EMPTY, // 空
	EXITS, // 存在
	DELETE // 删除
};

ハッシュデータ:

// HashData数据
template<class K, class V>
struct HashData
{
    
    
	pair<K, V> _kv;
	State _state = EMPTY;
};

ハッシュ表:

// HashTable
template<class K, class V>
class HashTable
{
    
    
public:

private:
	vector<HashData<K, V>> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数(用来控制负载因子)
};

(3) 事前作業

// 将kv.first强转成size_t
// 仿函数
template<class K>
struct DefaultHashTable
{
    
    
	size_t operator()(const K& key)
	{
    
    
		return (size_t)key;
	}
};

ここに画像の説明を挿入します

2. ハッシュテーブルへの挿入

挿入には次の 4 つの手順があります。

  1. キー値のキーと値のペアがハッシュ テーブルに存在するかどうかを確認します。すでに存在する場合、挿入は失敗します。
  2. 負荷率が大きすぎるため、ハッシュ テーブルを調整する必要があります。
  3. キーと値のペアをハッシュ テーブルに挿入する
  4. ハッシュテーブル内の有効な要素 +1

ハッシュテーブルを調整する

0.7 は 10 進数であるため、0.7 に 10 を掛けます。ハッシュ テーブルの負荷係数が 0.7 より大きい場合、新しいテーブルを作成する必要があります。この新しいテーブルのサイズは、元のテーブルのサイズの 2 倍です。単純に拡張する場合、元のハッシュ テーブルのデータが新しいテーブルの場所に応じて新しいテーブルの異なる場所に挿入され、最終的に元のテーブルと新しいテーブルが交換されます。

キーと値のペアをハッシュテーブルに挿入するにはどうすればよいですか?

まず、ハッシュ関数 (剰余アルゴリズム) を通じて対応するハッシュ アドレスを計算し、ハッシュの競合があるかどうかを確認し、ハッシュの競合がある場合は、ハッシュ アドレスから EMPTY または DELETE の位置を見つけて挿入します (注、今回は、値がハッシュ テーブルにないことを確認するために以前に Find 関数が呼び出されているため)、最終的に値がこの位置に挿入され、この位置のステータスが EXITS に変更されます。

注: ハッシュの競合が発生し、逆方向に検出された場合、ハッシュ テーブルの負荷係数が 0.7 未満に制御されているため、挿入に適した場所が見つかります。つまり、ハッシュ テーブルがいっぱいになることはありません。

コードは以下のように表示されます:

	HashTable()
	{
    
    
		// 构造10个空间
		_table.resize(10);
	}

	bool Insert(const pair<K, V>& kv)
	{
    
    
		// 查看哈希表中是否存在该键值的键值对,若已存在则插入失败
		// 负载因子过大都需要对哈希表的大小进行调整
		// 将键值对插入哈希表
		// 哈希表中的有效元素个数加1

		// 1、判断哈希表中是否存在该键值的键值对
		HashData<K, V>* key_ = Find(kv.first); 
		// 利用Find函数的bool值来判断是否存在
		if (key_)
		{
    
    
			return false; // 键值已经存在了,返回false
		}

		// 2、负载因子过大需要调整 -- 即大于0.7
		if (_n * 10 / _table.size() >= 7)
		{
    
    
			// 增容
			// a.创建新的哈希表,并将新表开辟到原始表的两倍
			size_t newSize = _table.size() * 2;
			HashTable<K, V, HashFunc> NewHashTable;
			NewHashTable._table.resize(newSize);

			// b.将原始表数据迁移到新表
			for (size_t i = 0; i < _table.size(); i++)
			{
    
    
				if (_table[i]._state == EXITS)
				{
    
    
					NewHashTable.Insert(_table[i]._kv);
				}
			}
			//for (auto& e : _table)
			//{
    
    
			//	if (e._state == EXITS)
			//	{
    
    
			//		NewHashTable.Insert(e._kv);
			//	}
			//}
			// c.交换原始表和新表
			_table.swap(NewHashTable._table);
		}

		// 3、将键值对插入到哈希表
		// 线性探索方式
		// a.取余计算原始哈希地址
		HashFunc hf;
		size_t hashi = hf(kv.first) % _table.size();
		// b.当是DELETE或者是EMPTY即插入,EXIST即跳过
		while (_table[hashi]._state == EXITS)
		{
    
    
			++hashi;
			hashi %= _table.size();// 防止超出
		}
		// c.插入进去并将状态进行改变
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXITS;
		// 3、哈希表中的有效元素个数加1
		++_n;

		return true;
	}

3. ハッシュテーブルで検索する

この方法には 2 つのステップがあります。

  1. ハッシュ関数を使用してハッシュアドレスを計算します。
  2. ハッシュ アドレスを起点として、線形検出を使用して、検索対象の要素が見つかった場合は検索成功と判断されるか、ステータスが EMPTY の場所が見つかった場合は検索成功と判断されるまで、データを前後方向に検索します。検索失敗。

注: 検索プロセス中に、位置ステータスが EXIST であり、キー値が一致する要素が検索成功とみなされなければなりません。キー値のみが一致しても、その位置の現在のステータスが DELETE の場合、その位置の要素は削除されているため、検索を続行する必要があります。

	// 查找函数
	HashData<K, V>* Find(const K& key)
	{
    
    
		// 1通过哈希函数计算出对应的哈希地址。
		// 2从哈希地址处开始,采用线性探测向后进行数据的查找,
		// 直到找到待查找的元素判定为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。
		HashFunc hf;
		size_t hashi = hf(key) % _table.size();
		while (_table[hashi]._state != EMPTY)
		{
    
    
			if (_table[hashi]._state == EXITS
				&& _table[hashi]._kv.first == key)
			{
    
    
				return (HashData<K, V>*)&_table[hashi];
			}
			++hashi;
			hashi %= _table.size();
		}

		// 没找到
		return nullptr;
	}

4. ハッシュテーブルの削除

擬似削除では実際にデータを削除する必要はなく、データのステータスをDELETEに変更するだけで済みます。

  1. このキーのキーと値のペアがハッシュ テーブルに存在するかどうかを確認してください。存在しない場合、削除は失敗します。
  2. 存在する場合は、キーと値のペアのステータスを DELETE に変更するだけです。
  3. ハッシュ テーブル内の有効な要素の数が 1 つ減ります。

ここではデータを完全に削除するのではなく、データのステータスを DELETE に変更しました。実際のデータは削除されませんでしたが、データを挿入したい場合は、古いデータを新しく挿入したデータで上書きできます。 。

	// 删除
	bool Erase(const K& key)
	{
    
    
		// 1查看哈希表中是否存在该键值的键值对,若不存在则删除失败
		// 2若存在,则将该键值对所在位置的状态改为DELETE即可
		// 3哈希表中的有效元素个数减一
		HashData<K, V>* key_ = Find(key);
		if (key_)
		{
    
    
			key_->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}

6. ハッシュテーブル(ハッシュバケット)のオープンハッシュ実装

1. ハッシュテーブルの構造

オープン ハッシュ テーブルでは、ハッシュ テーブル内の各格納場所のポインタ タイプは同じです。つまり、単結合リストの先頭ノードです。もちろん、与えられたデータを格納するだけでなく、ノード タイプもさらに、次のノードを指す次ノードポインタも格納する必要がある。

	template<class K, class V>
	struct HashNode
	{
    
    
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{
    
    }

		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

以前のクローズド ハッシュでは、各ハッシュ テーブルの位置に状態 (つまり、EXITS、EMPTY、DELETE) を設定する必要があることを今でも覚えていますが、オープン ハッシュのハッシュ テーブルでは、各ハッシュ テーブルごとに設定されるため、その必要がありません。ハッシュ テーブルの場所には挿入されたヘッド ノードが格納されており、挿入された新しいノードはその下に接続される可能性があります。オープン ハッシュのハッシュ テーブルでは、同じハッシュ アドレスを持つ要素を同じハッシュに入れます。ギリシャ バケットでは、いわゆる「次の場所」を探す必要はありません。

では、容量を増やす必要があるのでしょうか?答えはもちろん、リンク リスト ストレージであるハッシュ バケットにデータを保存できるため、容量を増やす必要があるということですが、負荷率が大きすぎてハッシュ テーブル全体がほぼいっぱいになる場合は、もちろん、このとき、負荷率が大きすぎます。新しい番号が追加されるたびに、ハッシュ競合が発生します。変数リストを末尾に挿入する必要があります。操作は依然として非常に複雑なので、拡張することをお勧めします。容量を増やして負荷率を小さくすることで、それほど大きくないハッシュ バケット全体の効率を向上させることができます。

	//哈希表
	template<class K, class V>
	class HashTable
	{
    
    
		typedef HashNode<K, V> Node;
	public:

	private:
		vector<Node*> _table; //哈希表
		size_t _n = 0; //哈希表中的有效元素个数
	};

2. 事前準備

	// 将kv.first强转成size_t
	// 仿函数
	template<class K>
	struct DefaultHashTable
	{
    
    
		size_t operator()(const K& key)
		{
    
    
			return (size_t)key;
		}
	};

	template<class K, class V>
	struct HashNode
	{
    
    
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{
    
    }

		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

ここに画像の説明を挿入します

3. ハッシュテーブル(バケット)の挿入

ハッシュ テーブル (バケット) の挿入には、主に次の 4 つの手順があります。

  1. キー値のキーと値のペアがハッシュ テーブルに存在するかどうかを確認します。すでに存在する場合、挿入は失敗します。
  2. 負荷率を調整する必要があるかどうかを判断し、負荷率が大きすぎる場合は、ハッシュ テーブルのサイズを調整する必要があります。
  3. キーと値のペアをハッシュ テーブルに挿入する
  4. ハッシュ テーブル内の有効な要素の数が 1 増加します。

負荷率が 1 になったため、負荷率を調整するために容量を拡張する必要があります。容量を 2 倍に拡張することを考えています。初期ハッシュ テーブルを走査し、簡単なピッキングを実行し、ハッシュ テーブルを直接移行する必要があります。元のハッシュ テーブルのノードのデータを新しいテーブルに変換し、新しいテーブルを古いテーブルと交換します。これにより、古いテーブル ノードを解放するプロセスが回避され、より便利で高速になります。次に、展開後の挿入された要素の分布を見てみましょう。

説明: 元のハッシュ テーブルの各ハッシュ バケットを走査し、ハッシュ関数を使用して各ハッシュ バケット内のノードの対応する位置を見つけて、それを新しいハッシュ テーブルに挿入するだけでよく、ノードを作成する必要はありません。そして解放します。

ハッシュの競合に対処する方法:
1. ハッシュ関数を使用して、対応するハッシュ アドレスを計算します。
2. ハッシュの競合が発生した場合は、対応する単一リンク リストにノードを挿入します。

以下は拡張例ですが、10 個の位置をすべて埋めると手順が多すぎるため、シミュレーションには 7 つのノードを使用します。
ここに画像の説明を挿入します

コードは以下のように表示されます:

		HashTable()
		{
    
    
			_table.resize(10, nullptr);
		}

		// 插入
		bool Insert(const pair<K, V>& kv)
		{
    
    
			// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败
			// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整
			// 3将键值对插入哈希表
			// 4哈希表中的有效元素个数加一


			HashFunc hf;
			// 1、查看键值对,调用Find函数
			Node* key_ = Find(kv.first);
			if (key_) // 如果key_是存在的则插入不了
			{
    
    
				return false; // 插入不了
			}

			// 2、判断负载因子,负载因子是1的时候进行增容
			if (_n == _table.size()) // 整个哈希表都已经满了
			{
    
    
				// 增容
				// a.创建一个新表,将原本容量扩展到原始表的两倍
				int HashiNewSize = _table.size() * 2;
				vector<Node*> NewHashiTable;
				NewHashiTable.resize(HashiNewSize, nullptr);

				// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中
				for (size_t i = 0; i < _table.size(); ++i)
				{
    
    
					if (_table[i]) // 这个桶中的有数据/链表存在
					{
    
    
						Node* cur = _table[i]; // 记录头结点
						while (cur)
						{
    
    
							Node* next = cur->_next; // 记录下一个结点
							size_t hashi = hf(kv.first) % _table.size(); // 记录一下新表的位置
							// 头插到新表中
							cur->_next = _table[hashi];
							_table[hashi] = cur;

							cur = next; // 哈希这个桶的下一个结点
						}
						_table[i] = nullptr;
					}
				}
				// c.交换两个表
				_table.swap(NewHashiTable);
			}

			// 3、将键值对插入到哈希表中
			size_t hashii = hf(kv.first) % _table.size();
			Node* newnode = new Node(kv);
			// 头插法
			newnode->_next = _table[hashii];
			_table[hashii] = newnode;

			// 4、将_n++
			++_n;
			return true;
		}

4. ハッシュテーブル(バケット)内の検索

  1. ハッシュ関数を使用して、対応するハッシュ アドレスを計算します。
  2. ハッシュ アドレスを通じて対応するハッシュ バケット内の単一リンク リストを見つけ、単一リンク リストを走査して検索します。
		// 查找
		HashNode<K, V>* Find(const K& key)
		{
    
    
			//1通过哈希函数计算出对应的哈希地址
			//2通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();

			Node* cur = _table[hashi]; // 刚好到哈希桶的位置
			while (cur)
			{
    
    
				// 找到匹配的了
				if (cur->_kv.first == key)
				{
    
    
					return (HashNode<K, V>*)cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

5. ハッシュテーブル(バケット)の削除

  1. ハッシュ関数を使用して、対応するハッシュ バケット番号を計算します。
  2. 対応するハッシュ バケットを走査して、削除するノードを見つけます。
  3. 削除するノードが見つかった場合は、そのノードを単一リンクリストから削除し、解放します。
  4. ノードを削除した後、ハッシュ テーブル内の有効な要素の数を 1 減らします。
		// 删除
		bool Erase(const K& key)
		{
    
    
			//1通过哈希函数计算出对应的哈希桶编号
			//2遍历对应的哈希桶,寻找待删除结点
			//3若找到了待删除结点,则将该结点从单链表中移除并释放
			//4删除结点后,将哈希表中的有效元素个数减一
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点
			// cur记录的是当前结点,当然是要删除的结点
			Node* prev = nullptr;
			Node* cur = _table[hashi];

			while (cur) // 遍历到结尾
			{
    
    
				if (cur->_kv.first == key) // 刚好找到这个值
				{
    
    
					// 第一种情况:这个要删除的值刚好是哈希桶的头结点
					if (prev == nullptr)
					{
    
    
						_table[hashi] = cur->_next;
					}
					// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值
					else // (prev != nullptr)
					{
    
    
						prev->_next = cur->_next;
					}
					delete cur; // 删除cur结点
					_n--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

6. ハッシュテーブル(バケット)の印刷

大きなループの中に小さなループが 2 つあります。

		// 打印一下
		void Print()
		{
    
    
			// 大循环套小循环
			for (size_t i = 0; i < _table.size(); ++i)
			{
    
    
				printf("[%d]->", i);
				Node* cur = _table[i];
				// 小循环
				while (cur)
				{
    
    
					cout << cur->_kv.first << ":" << cur->_kv.second << "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
		}

7. 文字列型の値をハッシュ テーブルに挿入します。

文字列型の値はまだ実装されていないため、オーバーロードされた構造体と特殊なテンプレートを使用することでこの問題を解決できます。コードは次のとおりです。

131 は素数です。他の素数に置き換えることもできます。その主な機能は、2 つの異なる文字列から計算されたハッシュ値が等しくなるのを防ぐことです。

	// 模版的特化(其他类型) 
	template<>
	struct DefaultHashTable<string>
	{
    
    
		size_t operator()(const string& str)
		{
    
    
			// BKDR
			size_t hash = 0;
			for (auto ch : str)
			{
    
    
				// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等
				hash *= 131;
				hash += ch;
			}

			return hash;
		}
	};

7. 実験をしてみましょう – ハッシュテーブルの長さに素数を使用する方が良い理由

合成数と素数を使って説明してみましょう。

合成数10と素数11。

  1. 合成数 10 の約数: 1 2 5 10
  2. 素数 11 の約数: 1 11

上記の要素に基づいて、実験用に 5 つのシーケンスを選択します。

数値間の間隔は 1 です:
s1=:{1 2 3 4 5 6 7 8 9 10}

数与数之间间隔2:
s2={2 4 6 8 10 12 14 16 18 20}

数値間の間隔は 5 です:
s3={5 10 15 20 25 30 35 40 45 50}

数値間の間隔は 10 です:
s4={10 20 30 40 50 60 70 80 90 100}

数値間の間隔は 11 です:
s5={11 22 33 44 55 66 77 88 99 101}

実験 1:
間隔 1 の数値シーケンスを、テーブル長 10 とテーブル長 11 のハッシュ バケットにそれぞれ挿入します。

ここに画像の説明を挿入します
ここに画像の説明を挿入します

実験 2:
数値間隔 2 のシーケンスを、テーブル長 10 とテーブル長 11 のハッシュ バケットにそれぞれ挿入します。

ここに画像の説明を挿入します

ここに画像の説明を挿入します

実験 3:
5 で区切られた一連の数値を、それぞれテーブル長 10 とテーブル長 11 のハッシュ バケットに挿入します。

ここに画像の説明を挿入します
ここに画像の説明を挿入します

実験 4:
間隔 10 の数値シーケンスを、テーブル長 10 とテーブル長 11 のハッシュ バケットにそれぞれ挿入します。

ここに画像の説明を挿入します
ここに画像の説明を挿入します

実験 5:
間隔 11 の数値シーケンスを、テーブル長 10 とテーブル長 11 のハッシュ バケットにそれぞれ挿入します。

ここに画像の説明を挿入します

ここに画像の説明を挿入します

結論を得る:

1. 全員の間隔が 1 の場合、テーブルの長さに関係なく、全員が均等に分散されます。
2. 数値間の間隔がハッシュ テーブルの長さ、またはハッシュ テーブルの長さの倍数である場合、必ずハッシュの衝突が発生します。2 番目の数値からはハッシュの衝突が発生します。始まり。
3. 数値間の距離がテーブルの長さの要因となる場合、ハッシュ競合の確率は非常に大きくなります。基本的に、数値が 3 つ存在しない場合に発生します。to 2 hash衝突。

素数の因数が最も少なく、1 とそれ自体だけであることがわかりました。したがって、テーブルの長さには素数の方が適しています。

おすすめ

転載: blog.csdn.net/m0_70088010/article/details/133483476