Table de hachage et seau de hachage de la structure de données (y compris l'implémentation du code)

contenu

1. Le concept de base de la table de hachage 

 Deux fonctions de hachage

2.1 Méthode de la valeur directe

 2.2 La méthode du reste de la division

 2.3 Plusieurs méthodes moins couramment utilisées

Conflit des Trois Hassi

Méthode à quatre adresses ouvertes

4.1 Détection linéaire

4.2 Détection secondaire

Méthode à cinq fermetures à glissière

1. Le concept de base de la table de hachage 

La table de hachage (également appelée table de hachage) est une structure de données accessible directement en fonction de la valeur de la clé . Autrement dit, il accède aux enregistrements en mappant la valeur de clé à un emplacement dans la table pour accélérer les recherches. Cette fonction de mappage s'appelle une fonction de hachage et le tableau d'enregistrements s'appelle une table de hachage .

Étant donné une table M, il existe une fonction f(clé), pour toute clé de valeur de mot-clé donnée, si l'adresse de l'enregistrement contenant le mot-clé dans la table peut être obtenue après substitution de la fonction dans la fonction, alors la table M est appelée une table de hachage (Hash), la fonction f(key) est une fonction de hachage. ------ De Baidu.

Il existe de nombreux endroits où la table de hachage est utilisée dans la vie, tels que :

1. Apprendre l'anglais Lorsque nous apprenons l'anglais, lorsque nous rencontrons un mot que nous ne connaissons pas, nous recherchons toujours ce mot en ligne :

 Bien que le professeur d'anglais ne nous recommande pas de le faire, car les données chinoises trouvées dans le dictionnaire électronique sont trop limitées et le dictionnaire papier traditionnel peut trouver une variété de significations, de parties du discours, d'exemples de phrases, etc. Mais perso je préfère ça

Dans notre monde de la programmation, il est souvent nécessaire de stocker un tel "dictionnaire" en mémoire pour faciliter des requêtes et des statistiques efficaces.

Par exemple, pour développer un système de gestion des étudiants, il est nécessaire de connaître rapidement le nom de l'étudiant correspondant en saisissant le numéro d'étudiant. Au lieu d'interroger la base de données à chaque fois, une table de cache peut être créée en mémoire, ce qui peut améliorer l'efficacité des requêtes.

 Pour un autre exemple, si nous devons compter la fréquence de certains mots dans un livre anglais, nous devons parcourir le contenu de tout le livre et enregistrer le nombre d'occurrences de ces mots en mémoire.

En raison de ces exigences, une structure de données importante est née.Cette structure de données est appelée table de hachage ou table de hachage.

Le processus d'insertion et de recherche d'éléments dans la table de hachage est le suivant :

1. Insérer un élément
Selon le code clé de l'élément à insérer, utilisez cette fonction pour calculer l'emplacement de stockage de l'élément et le stocker en fonction de cet emplacement
2. L'élément de recherche
effectue le même calcul sur le code clé de l'élément à insérer. élément, et considère la valeur de la fonction obtenue comme l'élément. L'emplacement de stockage de , dans la structure, compare les éléments selon cet emplacement
, si les clés sont égales, la recherche est réussie.

 Deux fonctions de hachage

2.1 Méthode de la valeur directe

Méthode de personnalisation directe : prenez une fonction linéaire du mot-clé comme adresse de hachage : Hash(Key) = A*Key + B

1. Avantages : simple et uniforme

2. Inconvénients : vous devez connaître à l'avance la répartition des mots-clés et n'utiliser qu'une petite plage

3. Scénario d'utilisation : adapté à la recherche de situations relativement petites et continues

4. Scénarios non applicables : lorsque la distribution des données est relativement dispersée, telle que 1, 199847348, 90, 5, il n'est pas approprié d'utiliser cette méthode

 2.2 La méthode du reste de la division

Méthode de division du reste : définissez le nombre d'adresses autorisées dans la table de hachage sur m, prenez un nombre premier p non supérieur à m, mais le plus proche ou égal à m comme diviseur, selon la fonction de hachage : Hash(key) = key % p(p< =m), convertit la clé en une adresse de hachage.

Avantages : Large plage d'utilisation, quasiment illimitée.

Inconvénients : il existe des conflits de hachage, qui doivent être résolus. Lorsqu'il y a de nombreux conflits de hachage, l'efficacité diminue considérablement.

 2.3 Plusieurs méthodes moins couramment utilisées

1. Méthode de prise de carré

hash(key)=key*key puis prendre les bits du milieu de la valeur de retour de la fonction comme adresse de hachage

En supposant que le mot clé est 1234, son carré est 1522756 et les 3 bits du milieu 227 sont extraits comme adresse de hachage ; par exemple, le mot clé est 4321, son carré est 18671041 et les 3 bits du milieu 671 (ou 710) sont extraites en tant qu'adresse de hachage. .

La méthode du carré est plus adaptée : la répartition des mots-clés est inconnue, et le nombre de chiffres n'est pas très grand.

2. Méthode de pliage

La méthode de pliage consiste à diviser le mot clé en plusieurs parties avec des chiffres égaux de gauche à droite (la dernière partie peut être plus courte), puis à superposer et additionner ces parties, et selon la longueur de la table de hachage, prendre les derniers chiffres comme l'adresse de la colonne de hachage. La méthode de pliage est adaptée à la distribution de mots-clés qui n'ont pas besoin d'être connus à l'avance, et convient au cas où le nombre de mots-clés est relativement important.

3. Méthode des nombres aléatoires

Sélectionnez une fonction aléatoire et prenez la valeur de fonction aléatoire du mot-clé comme adresse de hachage, c'est-à-dire H(clé) = aléatoire(clé), où aléatoire est la fonction de nombre aléatoire.

4. Analyse mathématique

Il y a n d chiffres, et chaque chiffre peut avoir r symboles différents. La fréquence de ces r symboles différents peut ne pas être la même sur chaque bit, et peut être uniformément répartie sur certains bits. Égalité des chances, répartition inégale sur certains bits, seulement certains types de symboles apparaissent fréquemment. Selon la taille de la table de hachage, plusieurs bits avec des symboles uniformément répartis peuvent être sélectionnés comme adresse de hachage.En supposant que la table d'enregistrement des employés d'une entreprise doit être stockée, si le numéro de téléphone mobile est utilisé comme clé, il est très probable que les 7 premiers bits sont les mêmes. , alors nous pouvons choisir les quatre bits suivants comme adresse de hachage. Si un tel travail d'extraction est sujet à des conflits, nous pouvons également inverser les nombres extraits (tels que 1234 à 4321), déplacement de l'anneau droit (comme 1234 pour passer à 4123), décalage de l'anneau gauche, superposition des deux premiers chiffres et des deux derniers chiffres (par exemple, 1234 est changé en 12+34=46) et d'autres méthodes.
La méthode d'analyse numérique convient généralement pour traiter la situation où le nombre de mots-clés est relativement important. Si la distribution des mots-clés est connue à l'avance et que la distribution de plusieurs bits des mots-clés est relativement uniforme

Conflit des Trois Hassi

Le conflit de hachage signifie que différentes clés calculent la même adresse de carte de hachage via la même fonction de hachage.Ce phénomène est appelé conflit de hachage. Voici un exemple:

Nous insérons d'abord un ensemble  de paires clé-valeur Key pour  002931et Value pour  王五 . Comment faire?

La première étape consiste à le  Key convertir en un indice de tableau  via une fonction de hachage 5.

Étape 2, s'il n'y a pas d'élément à la position correspondant à l'indice du tableau 5,  Entry remplissez-le à la position de l' 5 indice .

Cependant, comme la longueur du tableau est limitée, lorsque de plus en plus d'entrées sont insérées, les indices obtenus par différentes clés via la fonction de hachage peuvent être les mêmes. Par exemple, l'indice du tableau correspondant à la clé 002936 est 2 ; l'indice du tableau correspondant à la clé 002947 est également 2.

Cette situation s'appelle une  collision de hachage. Oups, la fonction de hachage est "collisionnée", que dois-je faire ?
Les collisions de hachage sont inévitables, et puisqu'elles ne peuvent pas être évitées, nous devons trouver un moyen de les résoudre. Il existe deux façons principales de résoudre les collisions de hachage, l'une est la méthode d'adressage ouvert et l'autre est la méthode de liste chaînée.

 Méthode à quatre adresses ouvertes

4.1 Détection linéaire

L'adressage ouvert est également appelé hachage fermé.

Le principe de la méthode d'adressage ouvert est très simple : lorsqu'une clé obtient l'index de tableau correspondant grâce à la fonction de hachage et qu'elle a été occupée, on peut "trouver un autre travail" pour trouver la prochaine position libre.

En prenant la situation ci-dessus comme exemple, Entry6 obtient l'indice 2 via la fonction de hachage, et l'indice a déjà d'autres éléments dans le tableau, donc reculez de 1 bit pour voir si la position de l'indice 3 dans le tableau est libre.

 Malheureusement, l'indice  3 est déjà occupé, déplacez donc  1 le bit vers l'arrière pour voir si la position de l' 4 indice est libre.

Heureusement, la position de l' 4 indice  n'est pas occupée, il est donc Entry6 stocké à la position de l' 4 indice .

 Nous pouvons constater que plus il y a de données de table de hachage, plus le conflit de hachage est grave. De plus, la table de hachage est implémentée sur la base de tableaux, de sorte que la table de hachage implique également la question de l'expansion.

Lorsque la table de hachage atteint une certaine saturation après plusieurs insertions d'éléments, la probabilité d'un conflit dans la position de mappage de clé augmentera progressivement. De cette façon, un grand nombre d'éléments sont entassés dans la même position d'indice de tableau, formant une très longue liste chaînée, ce qui a un impact important sur les performances des opérations d'insertion et des opérations de requête ultérieures. À ce stade, la table de hachage doit étendre sa longueur, c'est-à-dire étendre .

Alors dans quelles circonstances faut-il étendre la table de hachage ? Comment étendre ? de la plénitude de la table de hachage . Puisque la longueur du tableau est une valeur fixe, α est proportionnel au "nombre d'éléments remplis dans le tableau", donc plus α est grand, plus il y a d'éléments remplis dans le tableau, et plus la possibilité de conflit est grande ; au contraire , α Plus il est petit, moins il y a d'éléments indiqués pour remplir le tableau, et moins il y a de risque de conflit . En fait, la longueur de recherche moyenne d'une table de hachage est fonction du facteur de charge o, seules différentes méthodes de gestion des collisions ont des fonctions différentes.
Pour la méthode d'adressage ouvert, le facteur de charge est un facteur particulièrement important et doit être strictement limité en dessous de 0,7-0,8. S'il dépasse 0,8 , les manques de cache CPU (cachemissing) lors de la recherche de table augmenteront selon une courbe exponentielle. Par conséquent, certaines bibliothèques de hachage qui utilisent la méthode d'adressage ouverte, telles que la bibliothèque système de Java, limitent le facteur de charge à 0,75, et la table de hachage sera redimensionnée au-delà de cette valeur.

4.2 Détection secondaire

Le défaut de la détection linéaire est que les données conflictuelles sont accumulées en une seule pièce, ce qui est lié à la recherche de la prochaine position vide, car la façon de trouver la position vide est de rechercher une par une, donc pour éviter ce problème, la deuxième détection trouvera la prochaine position vide.La méthode pour une position vide est : H, =(Ho+i2 )% m, ou : H, =(Ho -i2 )% m. Où : i=1,2,3..., H est la position obtenue en calculant la clé de l'élément grâce à la fonction de hachage Hash(x), et m est la taille du tableau. Pour 2.1, si vous souhaitez insérer 44, un conflit se produit et la situation après l'utilisation de la solution est :

 La recherche montre que : lorsque la longueur de la table est un nombre premier et que le facteur de charge de la table a ne dépasse pas 0,5, la nouvelle entrée de la table doit pouvoir être insérée et aucune position ne sera sondée deux fois. Donc tant qu'il y a la moitié des positions vides dans la table, il n'y a pas de problème de table pleine. Il n'est pas nécessaire de considérer la situation où la table est pleine lors de la recherche, mais il faut s'assurer que le facteur de charge a de la table ne dépasse pas 0,5 lors de l'insertion. S'il dépasse, la capacité doit être prise en compte.

Code correspondant :

Voici une explication de l'apparence de la table de hachage :

Recherche  Key la  valeur correspondante 002936 dans  Entry la table de hachage pour . Comment faire? Prenons la méthode de la liste chaînée comme exemple.

 Étape 1 : Convertir la clé en indice de tableau 2 via la fonction de hachage.

Étape 2 : Trouvez l'élément correspondant à l'indice 2 du tableau. Si la clé de cet élément est 002936, alors il est trouvé ; si la clé n'est pas 002936, ce n'est pas grave, continuez à partir de la position suivante. Si la suivante position est vide, cela signifie Sans ce numéro, si la dernière position du tableau est trouvée, la recherche continue depuis le début.

#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;  // 存储有效数据的个数
	};

Méthode à cinq fermetures à glissière

1. Concept
de hachage ouvert La méthode de hachage ouverte est également appelée méthode d'adresse en chaîne (méthode de fermeture à glissière).Tout d'abord, la fonction de hachage est utilisée pour calculer l'adresse de hachage pour le jeu de codes de clé.Les codes de clé avec la même adresse appartiennent au même sous-ensemble, et chaque sous-ensemble est appelé C'est un seau, les éléments de chaque seau sont liés par une liste liée individuellement, et le nœud principal de chaque liste liée est stocké dans la table de hachage. La méthode zipper est différente de la méthode d'adressage ouvert. Chaque élément du tableau dans la méthode zipper n'est pas seulement un objet Entry , mais également le nœud principal d'une liste chaînée. Chaque objet Entry pointe vers son nœud Entry suivant via le pointeur suivant. Lorsque la nouvelle entrée est mappée à la position de tableau en conflit, il suffit de l'insérer dans la liste chaînée correspondante.

 Voici comment le trouver dans la méthode zipper :

Trouvez la valeur correspondant à l'entrée dont la clé est 002936 dans la table de hachage. Comment faire? Prenons la méthode de la liste chaînée comme exemple. La première étape consiste à convertir la clé en un indice de tableau 2 via la fonction de hachage. La deuxième étape consiste à trouver l'élément correspondant à l'indice 2 du tableau. Si la clé de cet élément est 002936, alors il est trouvé ; si la clé n'est pas 002936, cela n'a pas d'importance. Puisque chaque élément du tableau correspond à une liste chaînée, nous pouvons commander Allez lentement dans la liste chaînée pour voir si vous pouvez trouver un nœud qui correspond à la clé.

Bien sûr, le hachage doit également être étendu :

Le nombre de buckets est fixe. Avec l'insertion continue d'éléments, le nombre d'éléments dans chaque bucket continue d'augmenter. Dans les cas extrêmes, il peut y avoir beaucoup de nœuds de liste chaînée dans un bucket, ce qui affectera les performances du hachage table. Par conséquent, sous certaines conditions, la table de hachage doit être augmentée. Comment confirmer les conditions ? Le meilleur cas pour le hachage ouvert est qu'il y a exactement un nœud dans chaque compartiment de hachage, et lorsque des éléments continuent d'être insérés, des collisions de hachage se produiront à chaque fois. Par conséquent, lorsque le nombre d'éléments est exactement égal au nombre de compartiments , vous pouvez donner une extension de table de hachage.

Les étapes d'extension sont les suivantes :

1. Pour développer, créez un nouveau tableau vide d'entrée dont la longueur est le double du tableau d'origine.
2. Re-Hash, parcourez le tableau d'entrée d'origine et re-Hash toute l'entrée dans le nouveau tableau. Pourquoi re-Hash it? Parce qu'après l'extension de la longueur, les règles de Hash changent également. Après l'expansion, la table de hachage surchargée à l'origine redevient clairsemée et l'entrée d'origine est redistribuée aussi uniformément que possible.

Avant extension :

Après extension :

Dans la méthode de la fermeture éclair, si un certain conflit de chaîne est très grave, vous pouvez choisir de suspendre un arbre rouge-noir dans la position correspondante.

pour le code

	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;         // 有效数据的个数
		};

Je suppose que tu aimes

Origine blog.csdn.net/qq_56999918/article/details/123316914
conseillé
Classement