C++の詳しい説明---ハッシュクローズドハッシュ

ハッシュを理解するための質問

ここに画像の説明を挿入します
この質問を試すにはここをクリックしてください

まず、質問は、この文字列には小文字の英字のみが含まれており、小文字の英字は 26 個しかないことを示しています。そのため、サイズ 26 の文字配列を作成して、文字列内に各文字が出現する回数を記録できます。 create for ループは、次のコードのように、文字列全体を走査して、文字列内の各文字の出現数を取得します。

 char firstUniqChar(string s) {
    
    
    int arr[26]={
    
    0};
    for(auto ch:s)
    {
    
    
        arr[ch-'a']++;
     }
}

各文字の出現回数を取得した後、文字列を左から右の順にたどって、配列内の対応する文字の出現回数に従って 1 回だけ出現する最初の文字を取得し、最終的にそれを返すことができます。 1 回しか出現しない文字がない場合は空白文字が返されるため、コードは次のようになります。

char firstUniqChar(string s) {
    
    
    int arr[26]={
    
    0};
    for(auto ch:s)
    {
    
    
        arr[ch-'a']++;
    }
    for(auto ch:s)
    {
    
    
        if(arr[ch-'a']==1)
        {
    
    
            return ch;
        }
    }
    return ' ';
}

コードを送信すると、コードが正しく実行されることがわかります:
ここに画像の説明を挿入します
上記の考え方はハッシュと似ています。26 個の要素を含む整数配列を作成します。配列内の各要素は文字列内の要素を表します。たとえば、添字 0 は文字 a を表し、添字 1 の要素は文字 b を表し、以下同様です。配列内の各要素のサイズは、文字列内の特定の要素を反映します。出現する要素の数。配列添字 1 の要素が 3 の場合、文字列内の要素 b の出現数が 3 であることを意味します。配列添字 2 の要素のサイズが 4 の場合、文字列内の要素 c の出現数を意味します。が 4 などの場合、配列を通じて実現されるこの 1 対 1 の対応関係をハッシュ構造と呼びます。データの山の各要素は配列内の位置に対応し、この位置の要素のサイズは、データの要素の属性、例えば上記の配列の要素のサイズは、文字列内の特定の要素の出現数を反映しています、そして、これがハッシュ構造です。

ハッシュの実装原理

方法 1

ハッシュの本質は、キー値と保存場所を関連付けるハッシュ マッピングです。たとえば、データの束の中の各データの出現数を取得したい場合があります。これらのデータの特徴は、整数とデータ サイズです。範囲は 0 ~ 1000 で、サイズ 1001 の配列を作成できます。配列内の要素はデータ内の要素の数を表します。この方法は直接値法と呼ばれます。値は直接取得されます。位置を決定するために使用されるか、相対的に位置を決定するために値が使用されます。たとえば、データ範囲が 1000 から 2000 の場合でも、サイズが 1001 の配列を作成しますが、添字が 0 の配列は作成されません。配列はデータ 1000 を表します。これに応じて、この方法は非常に集中したデータにのみ適しています。データが非常に分散している場合、この方法の効率は非常に低くなります。たとえば、データ範囲が 1 ~ 10000 の場合、の場合、サイズ 10000 の配列を作成する必要があります。ただし、ほとんどのデータは 9900 から 10000 の間に分散されます。たとえば、1 つのデータのみが 1 で、他のデータが 9900 より大きい場合、9899 個のスペースが発生します。データは何の効果もなく無駄になるため、このアプローチはデータが非常に集中している場合にのみ適しています。

方法 2

Remainder メソッド: このメソッドでは、スペースの大きさは関係ありませんが、指定したデータの数に基づいて開くスペースの量が決定されます。たとえば、現在 7 つのデータがある場合、ここでは 10 個のスペースが開かれます。スペースの値を使用してデータの値をタッチすると、その結果が保存場所の添字になります。たとえば、現在のスペースのサイズが 10 の場合、処理されるデータが 18 の場合、18 をタッチすると、 10 の場合、結果は 8 になります。そこで、添字 8 の位置に 18 を置くことで、データがどんなに大きくても、小さな配列の中で対応する位置を見つけることができます。しかし、この処理方法は、新しい問題を引き起こします。問題は、ハッシュの衝突です。たとえば、一方の値が 3 で、もう一方の値が 13 で、配列の空間サイズが 10 の場合、これら 2 つの数値の結果は同じになります。つまり、空間には正確に何が格納されるかということになります。下付き文字 3 を付けますか? 問題があります。この現象をハッシュ競合と呼びます。解決方法は 2 つあります。最初の方法について説明します。クローズド ハッシュ - オープン先頭アドレス方法です。この方法は、マップされた位置にすでに値があることを意味します。 , 次に、特定のルールに従って他の位置を検索します。たとえば、この位置を後方に占有することは線形検出です。あなたが私の位置を占有する場合、私は現在の位置の次の位置を占有します。後の位置も占有されている場合、次の図のように、次の位置を占めます。
ここに画像の説明を挿入します
3 に対応する位置に 13 が格納されます。 23 が挿入された場合、この 23 は添え字 3 の位置に対応するはずですが、3 にはすでにデータがあるため、それを逆向きに挿入して、4 の位置である 3 の後ろに置きます。
ここに画像の説明を挿入します
データ 33 を再度挿入すると、対応する位置は依然として 3 ですが、データは 3 に保存され、3 4 の後の位置にもデータが保存されますが、5 が保存されます。 after 4 にはデータが保存されないため、データを 5 の上に置きます。たとえば、次の図:
ここに画像の説明を挿入します
これが挿入のルールです。検索も同様です。該当する位置から逆方向に検索します。見つかった場合、または検索位置が空の場合は、検索を停止します。削除の場合はどうでしょうか。ここで削除を削除するにはどうすればよいですか? 上記の考え方を通じて、削除したいデータを実際に見つけることができます。では、それを削除する場合、この値を何に設定すればよいでしょうか? 空に設定しますか? どれくらい空いてますか?0ですか?そうではないようです。探しているデータが正確に 0 である場合、それは間違っていますか? 空は負の数と見なすことができますか? うまくいかないようですね? 空のスペースが負の場合、ハッシュ テーブルには負の数を格納できないはずです。空のスペースはどのくらいですか? まずこの問題を解決するのではなく、空である妥当な値を見つけたとしましょう。削除とは、指定されたデータを空に設定することを意味します。この方法で本当に問題はないでしょうか? たとえば、下の図:
ここに画像の説明を挿入します
23 を削除したい場合は、添字 4 のデータを空に設定します。その後、コードは次のようになります:
ここに画像の説明を挿入します
データ 33 を検索したい場合、エラーは発生しますか?ルール 要素が見つかった場合、または現在の要素が空の場合、検索は停止します。ここで要素 33 を見つけたい場合は、添え字 3 から検索を開始します。3 に対応する値は 13 であり、明らかに等しくありません。スペースを 1 つ飛ばして 4 に戻しました。しかし、添え字 4 に対応する値は空です。規則に従って、検索を終了する必要があります。そのため、データ 33 は存在しますが、要素の削除方法が次のとおりであるため、検索は失敗します。不正確です。これは、要素を空に設定しても問題は解決しないことを意味します。第一に、それがどの値であるかわかりません。第二に、この方法は検索でも問題を引き起こすため、友人の中には別のグリッドを考えた人もいました。メソッド。削除された要素の背後にある値を 1 グリッド前に移動して削除を実現できますか?
ここに画像の説明を挿入します
たとえば、下の図で 23 を削除した場合、空のスペースが見つかるまで、後続のすべてのデータを 1 スペース前に移動する必要があります (たとえば、下の図)。
ここに画像の説明を挿入します
データを移動すると、以前対応していたデータとの対応ができなくなるため、直接移動する方法ではデータを削除できません。解決策として、ステータスを表す配列を追加します。ここでのステ​​ータスは 3 種類に分けられます。 is empty は現在位置にデータがないこと、Erase は現在位置より前にデータがあるが削除されたこと、exist は現在位置の要素が存在することを意味し、削除は状態を変更することを意味します。検索する場合は、指定した位置から空のステータスの位置が見つかるまで検索します。データは見つかっても、データに対応する位置ステータスが存在しない場合は、存在しないことを返します。 . これが挿入と削除を見つけるロジックです. 次に、上記の関数コードをシミュレーションで実装します。

準備

まず第一に、各ノードには状態があります。3 つの状態があるため、ここで 3 つの状態を説明するための列挙を作成できます。ハッシュの最下層はベクトルです。次に、各要素にはデータを保存するためのペアと、 state という名前の変数は現在のノードのステータスを記録するために使用されるため、ノードを記述するためにここで構造体を作成する必要があります。また、ノードはさまざまなタイプのデータを保存する必要があるため、ノード クラスにテンプレートを追加する必要があります。テンプレートに 2 つのパラメーターを追加し、コードを次のように記述します。

enum state
{
    
    
	EMPTY,
	EXIST,
	ERASE,
};

template<class K, class V>
struct HashNode
{
    
    
	pair<K, V> _kv;
	state _state = EMPTY;
};

ハッシュは配列を介してシミュレートおよび実装されるため、ここでのハッシュ クラスの最下層はベクトルを介して実装されます。ベクトルのデータ型は HashNode です。ハッシュはさまざまなデータに直面するため、ここにもテンプレートを追加する必要がありますテンプレートには 2 つのパラメーターがあるため、コードは次のようになります。

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

 private:
	 vector<HashNode<K,V>> _tables;
 };

挿入ロジックはデータを正の整数に変換し、この正の整数に基づいてベクトルにデータを挿入することですが、ハッシュ テーブルによって処理されるデータはカスタム タイプを持つ可能性があるため、ここでパラメーターを追加する必要があります。テンプレートを受け取ります。ファンクターを受け取ります。ファンクターの機能は、保存されたデータのタイプから変換された値を取得することです。この値を使用して、データを保存する場所を計算します。たとえば、文字列のデータ内容の場合、型が abcd の場合、ファンクターによる処理の後、abcd は各文字の合計であると仮定して値に変換されるため、abcd は 394 に変換され、この 394 を使用して格納場所を計算します。例えばこの時の配列の容量が10なら位置は4に格納されますが、普段ハッシュテーブルを使って整数データや文字列型データを処理する場合、ファンクターを渡さなくても普通に実行できます。これですか?答えは、テンプレート パラメーターには整数データを処理するファンクターのデフォルト値が渡され、文字列型を処理するファンクターは整数データを処理する特殊化であるということです。ここで質問がありますが、ハッシュ テーブル内の有効な文字の数はベクトルのサイズと等しくてもよいでしょうか? できそうな気がしますが、本当に可能なのでしょうか?ベクトルに大量のデータが含まれている場合、検索、削除、挿入を行うと、ハッシュの競合が非常に明白になります。これにより、ハッシュの競合がますます明らかになり、ハッシュ テーブルがますます効率的になります。低いので、ここで負荷係数と呼ばれるものを追加する必要があります。その値は、テーブル内の有効なデータの数/テーブルのサイズに等しいです。この負荷係数の値は、0.7 程度が最適です。負荷係数がが 0.7 に等しい場合、容量を拡張します。負荷率が小さいほど、クォータが競合する可能性は低くなりますが、消費されるスペースが大きくなるため、クラスに変数を追加して有効な値を記録する必要があります。現在のテーブルの場合、コードは次のようになります。

template<class K, class V>
struct HashNode
{
    
    
	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>
{
    
    
	size_t operator()(const string& key)
	{
    
    
		size_t res = 0;
		for (auto ch : key)
		{
    
    
			res *= 131;
			res += 131;
		}
		//这样的处理方式可以更好的降低重复率
		return res;
	}
};
 template<class K,class V,class Hash=HashFunc<K>>
 struct HashTable
 {
    
    
 public:

 private:
	 vector<HashNode<K,V>> _tables;
	 size_t _n;//记录有效值的个数
 };

次に、最後のステップはコンストラクターを追加することです。ここでのコンストラクターは、ベクトル データを展開し、変数 _n を 0 に初期化します。その場合のコードは次のとおりです。

HashTable()
	:_n(0)
{
    
    
	 _tables.resize(10);
}

ここまでが準備作業です。次に挿入関数を実装します。

入れる

まず、挿入関数にはペア タイプのパラメーターが必要です。次に、関数本体の最初のステップでファンクター オブジェクトを作成します。2 番目のステップでは、データを挿入する位置を計算します。ここでのコードは次のとおりです。

 bool Insert(const pair<K, V>& kv)
 {
    
    
	 Hash hf;
	 size_t hashi = hf(kv.first) %  _tables.size();
 }

次に、目的の位置を見つけたら、その位置からずっと遡ることができます。現在の位置のステータスが存在する場合は、引き続き戻ることができます。削除または空の場合は、要素を挿入できます。もちろん、 、境界外を防ぐために、各トラバーサルは i の値をモジュロして size を計算し、要素を挿入して現在の状態を変更し、見つかった場合は size 変数に 1 を追加します。その場合、ここでのコードは次のようになります。


bool Insert(const pair<K, V>& kv)
 {
    
    
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
    
    
		 if (_tables[hashi]._kv.first == kv.first)
		 {
    
    
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

次のステップは拡張です。有効なデータの数がテーブル内の全要素の 70% を占めると、拡張する必要があるため、ここで新しいハッシュ テーブルを作成します。テーブルのサイズは 2 倍になります。次に、元のハッシュ テーブルの各要素を for ループで走査し、各要素を新しいハッシュ テーブルに挿入し、最後に 2 つのハッシュ テーブルの内部ベクトルを交換します。ここでのコードは次のようになります。

bool Insert(const pair<K, V>& kv)
 {
    
    
	 if (_n * 10 / _tables.size()>=7)
	 {
    
    
		 HashTable<K, V, HashFunc<K>> newHash;
		 newHash._tables.resize(_tables.size() * 2);
		 for (auto &ch : _tables)
		 {
    
    
			 if (ch._state == EXIST)
			 {
    
    
				 newHash.Insert(ch._kv);
			 }
		 }
		 _tables.swap(newHash._tables);
	 }
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
    
    
		 if (_tables[hashi]._kv.first == kv.first)
		 {
    
    
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

ここで、容量を拡張する場合、内部ベクトルの容量を単純に2倍に拡張することはできません、拡張後は内部データの対応関係が全て変わってしまうため、例えば容量が10の場合、対応するデータは18となります。 8. 拡張後のサイズは次のようになります 20 に達すると、18 は位置 18 に対応するため、ここで単純に拡張することはできません。第 2 に、拡張を実現する別の方法があります。それは、配列のベクトル変数を double に再作成することです。古い配列は、各値に対応する新しい添字を見つけて 1 つずつ挿入します。ただし、これを実装すると、多くの繰り返し作業が発生します。上で書いた方法は次のとおりです。既存のものを借りて、繰り返しの作業を大幅に削減します。

検索関数

まず要素が存在すべき位置を検索し、次に空の位置を検索します。空の場合は false を返します。値が等しく、現在位置の要素が存在する場合は true を返しますが、見つかった後に表示されます。値は変更できます。ここで見つからなかった場合は、nullptr を返します。見つかった場合は、要素のアドレスを返します。すると、これが実装アイデアです。 find 関数。この関数のコードは次のように実装されます。

 HashNode<K,V>* Find(const K& key)
 {
    
    
	 HashFunc<K> hf;
	 size_t hashi = hf(key) % _tables.size();
	 while (_tables[hashi]._state != EMPTY)
	 {
    
    
		 if (_tables[hashi]._kv.first == key&&
			 _tables[hashi]._state==EXIST)
		 {
    
    
			 return &_tables[hashi];
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 return nullptr;
 }

消去機能

まず、find関数でデータが存在する場所を見つけます。find関数はデータのアドレスを返すので、データが存在する場合は直接データのステータスを変更して消去します。データが存在しない場合は、直接データを削除します。 false を返します。次に、ここにコードを書きます。

bool erase(const K& key)
 {
    
    
	 HashNode<K, V>* Date = Find(key);
	 if (Date)
	 {
    
    
		 Date->_state = ERASE;
		 --_n;
		 return true;
	 }
	 else
	 {
    
    
		 return false;
	 }
 }

計測コード

検出されたコードは次のとおりです。

void TestHT1()
{
    
    
	HashTable<int, int> ht;
	int a[] = {
    
     18, 8, 7, 27, 57, 3, 38, 18 };
	for (auto e : a)
	{
    
    
		ht.Insert(make_pair(e, e));
	}
	ht.Insert(make_pair(17, 17));
	ht.Insert(make_pair(5, 5));
	if (ht.Find(7)){
    
    cout << "存在" << endl;}
	else{
    
    cout << "不存在" << endl;}
	ht.erase(7);
	if (ht.Find(7)) {
    
     cout << "存在" << endl; }
	else {
    
     cout << "不存在" << endl; }
}

コードの実行結果は次のとおりです。
ここに画像の説明を挿入します
期待どおりです。次に、次の検出コードを見てみましょう。

void TestHT2()
{
    
    
	string arr[] = {
    
     "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> countHT; 
	HashTable<string, int> countHT;
	for (auto& e : arr)
	{
    
    
		HashNode<string, int>* ret = countHT.Find(e);
		if (ret)
		{
    
    
			ret->_kv.second++;
		}
		else
		{
    
    
			countHT.Insert(make_pair(e, 1));
		}
	}
}

このコードの結果は次のとおりです。
ここに画像の説明を挿入します
期待どおりです。これは、コードが正しいことを意味します。この記事はここで終わります。

おすすめ

転載: blog.csdn.net/qq_68695298/article/details/131445819