データ構造のハッシュテーブルとハッシュバケット(コード実装を含む)

コンテンツ

1.ハッシュテーブルの基本概念 

 2つのハッシュ関数

2.1直接価値法

 2.2剰余の除算方法

 2.3あまり一般的に使用されていないいくつかの方法

3つのハッシ紛争

4つのオープンアドレス方式

4.1線形検出

4.2二次検出

5ジッパー方式

1.ハッシュテーブルの基本概念 

ハッシュテーブル(ハッシュテーブルとも呼ばれます)は、キー値に従って直接アクセスされるデータ構造です。つまり、キー値をテーブル内の場所にマッピングしてレコードにアクセスし、ルックアップを高速化します。このマッピング関数はハッシュ関数と呼ばれ、レコードの配列はハッシュテーブルと呼ばれます

テーブルMが与えられると、任意のキーワード値キーに対して関数f(key)があり、関数を関数に代入した後でテーブル内のキーワードを含むレコードのアドレスを取得できる場合、テーブルMが呼び出されます。ハッシュ(ハッシュ)テーブルの場合、関数f(key)はハッシュ関数です。------Baiduから。

ハッシュテーブルが生活の中で使用される場所はたくさんあります。たとえば、次のようなものです。

1.英語を学ぶ私たちが英語を学ぶとき、私たちが知らない単語に出会ったとき、私たちは常にこの単語をオンラインで調べます。

 英語の先生は私たちにこれを勧めませんが、電子辞書にある中国語のデータは限られており、伝統的な紙の辞書はさまざまな意味、品詞、例文などを見つけることができるためです。でも個人的にはこの方法が好きです

私たちのプログラミングの世界では、効率的なクエリと統計を容易にするために、そのような「辞書」をメモリに保存する必要があることがよくあります。

たとえば、学生管理システムを開発するには、学生番号を入力して、対応する学生の名前をすばやく見つける必要があります。毎回データベースにクエリを実行する代わりに、メモリ内にキャッシュテーブルを作成できるため、クエリの効率を向上させることができます。

 別の例として、英語の本の特定の単語の頻度を数える必要がある場合は、本全体の内容をトラバースして、これらの単語の出現回数をメモリに記録する必要があります。

これらの要件のために、重要なデータ構造が生まれました。このデータ構造は、ハッシュテーブルまたはハッシュテーブルと呼ばれます。

ハッシュテーブルに要素を挿入して検索するプロセスは次のとおりです。

1.要素の挿入挿入
する要素のキーコードに従って、この関数を使用して要素の保存場所を計算し、この場所に従って保存します
。2.検索要素
は、のキーコードに対して同じ計算を実行します。要素、および取得した関数値を要素と見なします。構造内のの格納場所は、この場所に従って要素を比較し
ます。キーが等しい場合、検索は成功します。

 2つのハッシュ関数

2.1直接価値法

直接カスタマイズ方法:キーワードの線形関数をハッシュアドレスとして使用します: Hash(Key)= A * Key + B

1.利点:シンプルで均一

2.短所:キーワードの分布を事前に知っておく必要があり、使用する範囲は狭いです。

3.使用シナリオ:比較的小さく継続的な状況を見つけるのに適しています

4.該当しないシナリオ:1、199847348、90、5のようにデータ分布が比較的分散している場合、この方法を使用することは適切ではありません。

 2.2剰余の除算方法

剰余の除算方法:ハッシュテーブルで許可されるアドレスの数をmに設定し、ハッシュ関数に従って、素数pをm以下で、除数としてmに最も近いものとします。Hash(key)= key %p(p <= m)、キーをハッシュアドレスに変換します。

利点:幅広い使用、基本的に無制限。

短所:ハッシュの競合があり、解決する必要があります。ハッシュの競合が多いと、効率が大幅に低下します。

 2.3あまり一般的に使用されていないいくつかの方法

1.平方取中法

hash(key)= key * key次に、関数の戻り値の中間ビットをハッシュアドレスとして使用します

キーワードが1234であるとすると、その2乗は1522756であり、中央の3ビット227がハッシュアドレスとして抽出されます。たとえば、キーワードは4321、2乗は18671041、中央の3ビット671(または710)がハッシュアドレスとして抽出されます。

二乗法の方が適しています。キーワードの分布は不明であり、桁数はそれほど多くありません。

2.折りたたみ方法

折りたたみ方法は、キーワードを左から右に等しい桁数でいくつかの部分に分割し(最後の部分は短くすることができます)、これらの部分を重ね合わせて合計し、ハッシュテーブルの長さに応じて最後の数桁を次のように取ります。ハッシュ列アドレス。折り畳み方法は、事前に知る必要のないキーワードの配布に適しており、キーワードの数が比較的多い場合に適しています。

3.乱数法

ランダム関数を選択し、キーワードのランダム関数値をハッシュアドレスとして使用します。つまり、H(key)= random(key)です。ここで、randomは乱数関数です。

4.数学的分析

n d桁あり、各桁にr個の異なるシンボルが含まれる場合があります。これらのr個の異なるシンボルの頻度は、各ビットで同じではなく、一部のビットで均等に分散される場合があります。機会均等、特定のビットでの不均一な分散、特定のビットのみシンボルの種類が頻繁に表示されます。ハッシュテーブルのサイズに応じて、シンボルが均等に分散された数ビットをハッシュアドレスとして選択できます。企業の従業員登録テーブルを格納する場合、携帯電話番号をキーとして使用すると、その可能性が非常に高くなります。最初の7ビットが同じである場合、ハッシュアドレスとして次の4ビットを選択できます。このような抽出作業が競合しやすい場合は、抽出された数値(1234〜4321など)を逆にして、右リングの変位を指定することもできます。 (1234を4123に変更するなど)、左リングシフト、最初の2つの数値と最後の2つの数値の重ね合わせ(たとえば、1234を12 + 34 = 46に変更)およびその他の方法。
数値解析法は、キーワードの数が比較的多い状況に対応するのに適しています。キーワードの分布が事前にわかっていて、キーワードの数ビットの分布が比較的均一である場合

3つのハッシ紛争

ハッシュ競合とは、異なるキーが同じハッシュ関数を介して同じハッシュマップアドレスを計算することを意味します。この現象はハッシュ競合と呼ばれます。次に例を示します。

最初に、とのキーと値のペアのセットを 挿入Key し  ます  。どうやるか?002931Value王五

最初のステップは 、ハッシュ関数をKey 使用して配列の添え字に変換すること 5です。

ステップ2、配列の添え字5に対応する位置に要素がない場合は、 Entry これを配列の 5 添え字。

ただし、配列の長さが制限されているため、挿入されるエントリが増えると、ハッシュ関数を介して異なるキーによって取得される添え字が同じになる場合があります。たとえば、キー002936に対応する配列の添え字は2であり、キー002947に対応する配列の添え字も2です。

この状況は、 ハッシュ衝突と呼ばれます。おっと、ハッシュ関数は「衝突」しています。どうすればよいですか?
ハッシュ衝突は避けられず、回避できないため、解決する方法を見つける必要があります。ハッシュの衝突を解決するには、主に2つの方法があります。1つはオープンアドレス法で、もう1つはリンクリスト法です。

 4つのオープンアドレス方式

4.1線形検出

オープンアドレス法は、クローズドハッシュとも呼ばれます。

オープンアドレッシング法の原理は非常に単純です。キーがハッシュ関数を介して対応する配列インデックスを取得し、占有されている場合、次の空き位置を見つけるために「別のジョブを見つける」ことができます。

上記の状況を例にとると、Entry6はハッシュ関数を介して添え字2を取得し、添え字にはすでに配列内に他の要素があるため、1ビット後方に移動して、配列内の添え字3の位置が空いているかどうかを確認します。

 残念ながら、添え字 3 はすでに使用されているため 1 、ビットを後方に移動して、配列内の 4 添え字。

幸い、配列の 4 添え字 占有されていないためEntry6 、配列の 4 添え字。

ハッシュテーブルデータが多いほど、ハッシュの競合が深刻になる ことがわかります。さらに、ハッシュテーブルは配列に基づいて実装されるため、ハッシュテーブルには拡張の問題も含まれます。

複数の要素を挿入した後、ハッシュテーブルが一定の飽和状態に達すると、キーマッピング位置での競合の可能性が徐々に高くなります。このように、多数の要素が同じ配列の添え字の位置に密集し、非常に長いリンクリストを形成します。これは、後続の挿入操作とクエリ操作のパフォーマンスに大きな影響を与えます。このとき、ハッシュテーブルはその長さを拡張する必要があります。つまり、を拡張する必要があります

次に、どのような状況でハッシュテーブルを拡張する必要がありますか?拡張する方法?ここで紹介するハッシュテーブルの負荷率は、次のように定義されます。α=テーブルに入力される要素の数/ハッシュテーブルの長さαは符号係数ですハッシュテーブルの充実度テーブルの長さは固定値であるため、αは「テーブルに入力される要素の数」に比例します。したがって、αが大きいほど、テーブルに入力される要素が多くなり、競合の可能性が高くなります。 、α小さいほど、テーブルに入力するように示される要素が少なくなり、競合する可能性が低くなります。実際、ハッシュテーブルの平均ルックアップ長は負荷率oの関数であり、衝突を処理する方法が異なれば機能も異なります。
オープンアドレッシング法の場合、負荷率は特に重要な要素であり、0.7〜0.8未満に厳密に制限する必要があります。0.8を超えると、テーブルルックアップ中のCPUキャッシュミス(キャッシュミス)が指数曲線に従って増加します。したがって、Javaのシステムライブラリなど、オープンアドレッシング方式を使用する一部のハッシュライブラリは、負荷率を0.75に制限し、ハッシュテーブルはこの値を超えてサイズ変更されます。

4.2二次検出

線形検出の欠点は、競合するデータが1つに蓄積されることです。これは、空の位置を見つける方法が1つずつ探すことであるため、次の空の位置を見つけることに関連しています。この問題を回避するために、 2回目の検出では、次の空の位置が検出されます。空の位置の方法は、H、=(Ho + i2)%m、または:H、=(Ho -i2)%mです。ここで、i = 1,2,3 ...、Hは、ハッシュ関数Hash(x)を使用して要素のキーを計算することによって取得される位置であり、mはテーブルのサイズです。2.1の場合、44を挿入すると、競合が発生し、ソリューションを使用した後の状況は次のようになります。

 調査によると、テーブルの長さが素数で、テーブルの負荷率aが0.5を超えない場合、新しいテーブルエントリを挿入できる必要があり、どの位置も2回プローブされません。テーブル内の空の位置が半分である限り、テーブルがいっぱいになる問題はありません。検索時にテーブルがいっぱいになる状況を考慮する必要はありませんが、挿入時にテーブルの負荷率aが0.5を超えないようにする必要があります。超えた場合は、容量を考慮する必要があります。

対応するコード:

ハッシュテーブルがどのように検索されるかについての説明は次のとおりです。

 のハッシュテーブルで対応する値 を 検索 Key し ます。どうやるか?例として、リンクリストメソッドを取り上げましょう。002936Entry

 ステップ1:ハッシュ関数を使用してキーを配列添え字2に変換します。

ステップ2:配列の添え字2に対応する要素を検索します。この要素のキーが002936の場合は検索されます。キーが002936でない場合は、問題ありません。次の位置から続行します。位置が空の場合、この番号がないと、配列の最後の位置が見つかった場合、検索は最初から続行されます。

#pragma once
#include <vector>
#include <iostream>
using namespace std;

namespace CloseHash
{
	enum State
	{
		EMPTY,
		EXITS,
		DELETE,
	};

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

	template<class K>
	struct Hash
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};

	// 特化
	template<>
	struct Hash<string>
	{
		// "int"  "insert" 
		// 字符串转成对应一个整形值,因为整形才能取模算映射位置
		// 期望->字符串不同,转出的整形值尽量不同
		// "abcd" "bcad"
		// "abbb" "abca"
		size_t operator()(const string& s)
		{
			// BKDR Hash
			size_t value = 0;
			for (auto ch : s)
			{
				value += ch;
				value *= 131;
			}

			return value;
		}
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
	public:
		bool Insert(const pair<K, V>& kv)
		{
			HashData<K, V>* ret = Find(kv.first);
			if (ret)
			{
				return false;
			}

			// 负载因子大于0.7,就增容
			//if (_n*10 / _table.size() > 7)
			if (_table.size() == 0)
			{
				_table.resize(10);
			}
			else if ((double)_n / (double)_table.size() > 0.7)
			{
				//vector<HashData> newtable;
				// newtable.resize(_table.size*2);
				//for (auto& e : _table)
				//{
				//	if (e._state == EXITS)
				//	{
				//		// 重新计算放到newtable
				//		// ...跟下面插入逻辑类似
				//	}
				//}

				//_table.swap(newtable);

				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(_table.size() * 2);
				for (auto& e : _table)
				{
					if (e._state == EXITS)
					{
						newHT.Insert(e._kv);
					}
				}

				_table.swap(newHT._table);
			}

			HashFunc hf;
			size_t start = hf(kv.first) % _table.size();
			size_t index = start;

			// 探测后面的位置 -- 线性探测 or 二次探测
			size_t i = 1;
			while (_table[index]._state == EXITS)
			{
				index = start + i;
				index %= _table.size();
				++i;
			}

			_table[index]._kv = kv;
			_table[index]._state = EXITS;
			++_n;

			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			if (_table.size() == 0)
			{
				return nullptr;
			}

			HashFunc hf;
			size_t start = hf(key) % _table.size();//使用仿函数将key值取出来有可能key不支持取模
			size_t index = start;
			size_t i = 1;
			while (_table[index]._state != EMPTY)//不为空继续找
			{
				if (_table[index]._state == EXITS
					&& _table[index]._kv.first == key)//找到了
				{
					return &_table[index];
				}

				index = start + i;
				index %= _table.size();
				++i;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				return true;
			}
		}

	private:
		/*	HashData* _table;
			size_t _size;
			size_t _capacity;*/
		vector<HashData<K, V>> _table;
		size_t _n = 0;  // 存储有效数据的个数
	};

5ジッパー方式

1.オープンハッシュの概念
オープンハッシュ方式は、チェーンアドレス方式(ジッパー方式)とも呼ばれます。まず、ハッシュ関数を使用して、キーコードセットのハッシュアドレスを計算します。同じアドレスを持つキーコードは、同じサブセットに属します。各サブセットはバケットと呼ばれ、各バケット内の要素は単一リンクリストによってリンクされ、各リンクリストのヘッドノードはハッシュテーブルに格納されます。zipperメソッドは、オープンアドレス法とは異なります。zipperメソッドの配列の各要素は、Entryオブジェクトであるだけでなく、リンクリストのヘッドノードでもあります。各Entryオブジェクトは、次のポインターを介して次のEntryノードを指します。新しいエントリが競合する配列位置にマップされる場合、対応するリンクリストに挿入するだけで済みます。

 ジッパー方式でそれを見つける方法は次のとおりです。

ハッシュテーブルでキーが002936であるエントリに対応する値を見つけます。どうやるか?例として、リンクリストメソッドを取り上げましょう。最初のステップは、ハッシュ関数を使用してキーを配列添え字2に変換することです。2番目のステップは、配列の添え字2に対応する要素を見つけることです。この要素のキーが002936の場合は検出され、キーが002936でない場合は問題になりません。配列の各要素は対応しているため、リンクリストに移動するには、リンクリストをゆっくりと下に移動して、キーに一致するノードが見つかるかどうかを確認します。

もちろん、ハッシュも拡張する必要があります。

バケットの数は固定されています。要素を継続的に挿入すると、各バケットの要素の数が増え続けます。極端な場合、バケット内にリンクリストノードが多数存在する可能性があり、ハッシュのパフォーマンスに影響します。したがって、特定の条件下では、ハッシュテーブルを増やす必要があります。条件を確認するにはどうすればよいですか。オープンハッシュの最良のケースは、各ハッシュバケットにノードが1つだけあり、要素が挿入され続けると、毎回ハッシュの衝突が発生するため、要素の数がバケットの数と正確に等しい場合ハッシュテーブルを拡張できます。

拡張手順は次のとおりです。

1.展開するには、元の配列の2倍の長さの新しいエントリの空の配列を作成します。
2.再ハッシュし、元のエントリ配列をトラバースし、すべてのエントリを新しい配列に再ハッシュします。なぜそれを再ハッシュするのですか?長さが拡張されると、ハッシュのルールも変更されるためです。拡張後、元々混雑していたハッシュテーブルは再びスパースになり、元のエントリは可能な限り均等に再配布されます。

拡張前:

拡張後:

ジッパー方式では、特定のチェーンの競合が非常に深刻な場合、対応する位置に赤黒木を吊るすことができます。

コード用

	template<class K>
		struct Hash
		{
			size_t operator()(const K& key)
			{
				return key;
			}
		};
	
		// 特化
		template<>
		struct Hash < string >
		{
			// "int"  "insert" 
			// 字符串转成对应一个整形值,因为整形才能取模算映射位置
			// 期望->字符串不同,转出的整形值尽量不同
			// "abcd" "bcad"
			// "abbb" "abca"
			size_t operator()(const string& s)
			{
				// BKDR Hash
				size_t value = 0;
				for (auto ch : s)
				{
					value += ch;
					value *= 131;
				}
	
				return value;
			}
		};
	
		template<class K, class V>
		struct HashNode
		{
			HashNode<K, V>* _next;
			pair<K, V> _kv;
	
			HashNode(const pair<K, V>& kv)
				:_next(nullptr)
				, _kv(kv)
			{}
		};
	
		template<class K, class V, class HashFunc = Hash<K>>
		class HashTable
		{
			typedef HashNode<K, V> Node;
		public:
			size_t GetNextPrime(size_t prime)
			{
				const int PRIMECOUNT = 28;
				static const size_t primeList[PRIMECOUNT] =
				{
					53ul, 97ul, 193ul, 389ul, 769ul,
					1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
					49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
					1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
					50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
					1610612741ul, 3221225473ul, 4294967291ul
				};
	
				size_t i = 0;
				for (; i < PRIMECOUNT; ++i)
				{
					if (primeList[i] > prime)
						return primeList[i];
				}
	
				return primeList[i];
			}
	
			bool Insert(const pair<K, V>& kv)
			{
				if (Find(kv.first))
					return false;
	
				HashFunc hf;
				// 负载因子到1时,进行增容
				if (_n == _table.size())
				{
					vector<Node*> newtable;
					//size_t newSize = _table.size() == 0 ? 8 : _table.size() * 2;
					//newtable.resize(newSize, nullptr);
					newtable.resize(GetNextPrime(_table.size()));
	
					// 遍历取旧表中节点,重新算映射到新表中的位置,挂到新表中
					for (size_t i = 0; i < _table.size(); ++i)
					{
						if (_table[i])
						{
							Node* cur = _table[i];
							while (cur)
							{
								Node* next = cur->_next;
								size_t index = hf(cur->_kv.first) % newtable.size();
								// 头插
								cur->_next = newtable[index];
								newtable[index] = cur;
	
								cur = next;
							}
							_table[i] = nullptr;
						}
					}
	
					_table.swap(newtable);
				}
	
				size_t index = hf(kv.first) % _table.size();
				Node* newnode = new Node(kv);
	
				// 头插
				newnode->_next = _table[index];
				_table[index] = newnode;
				++_n;
	
				return true;
			}
	
			Node* Find(const K& key)
			{
				if (_table.size() == 0)
				{
					return false;
				}
	
				HashFunc hf;
				size_t index = hf(key) % _table.size();
				Node* cur = _table[index];
				while (cur)
				{
					if (cur->_kv.first == key)
					{
						return cur;
					}
					else
					{
						cur = cur->_next;
					}
				}
	
				return nullptr;
			}
	
			bool Erase(const K& key)
			{
				size_t index = hf(key) % _table.size();
				Node* prev = nullptr;
				Node* cur = _table[index];
				while (cur)
				{
					if (cur->_kv.first == key)
					{
						if (_table[index] == cur)
						{
							_table[index] = cur->_next;
						}
						else
						{
							prev->_next = cur->_next;
						}
	
						--_n;
						delete cur;
						return true;
					}
					
					prev = cur;
					cur = cur->_next;
				}
	
				return false;
			}
	
		private:
			vector<Node*> _table;
			size_t _n = 0;         // 有效数据的个数
		};

おすすめ

転載: blog.csdn.net/qq_56999918/article/details/123316914