Tabla hash y cubo hash de estructura de datos (incluida la implementación del código)

contenido

1. El concepto básico de tabla hash 

 Dos funciones hash

2.1 Método del valor directo

 2.2 El método del resto de la división

 2.3 Varios métodos menos utilizados

Conflicto de los Tres Hassi

Método de cuatro direcciones abiertas

4.1 Detección lineal

4.2 Detección secundaria

Método de cinco cremalleras

1. El concepto básico de tabla hash 

La tabla hash (también llamada tabla hash) es una estructura de datos a la que se accede directamente según el valor de la clave . Es decir, accede a los registros asignando el valor clave a una ubicación en la tabla para acelerar las búsquedas. Esta función de mapeo se llama función hash , y la matriz de registros se llama tabla hash .

Dada una tabla M, hay una función f (clave), para cualquier clave de valor de palabra clave dada, si la dirección del registro que contiene la palabra clave en la tabla se puede obtener después de sustituir la función en la función, entonces la tabla M se llama una tabla hash (Hash), la función f (clave) es una función hash. ------De Baidu.

Hay muchos lugares donde la tabla hash se usa en la vida, como:

1. Aprendiendo inglés Cuando aprendemos inglés, cuando encontramos una palabra que no sabemos, siempre buscamos esta palabra en línea:

 Aunque el profesor de inglés no nos recomienda hacer esto, porque los datos de chino que se encuentran en el diccionario electrónico son demasiado limitados, y el diccionario tradicional en papel puede encontrar una variedad de significados, partes del discurso, oraciones de ejemplo, etc. Pero personalmente prefiero así.

En nuestro mundo de programación, a menudo es necesario almacenar dicho "diccionario" en la memoria para facilitar consultas y estadísticas eficientes.

Por ejemplo, para desarrollar un sistema de gestión de estudiantes, es necesario averiguar rápidamente el nombre del estudiante correspondiente ingresando el número de estudiante. En lugar de consultar la base de datos cada vez, se puede crear una tabla de caché en la memoria, lo que puede mejorar la eficiencia de la consulta.

 Para otro ejemplo, si necesitamos contar la frecuencia de ciertas palabras en un libro en inglés, necesitamos recorrer el contenido de todo el libro y registrar el número de ocurrencias de estas palabras en la memoria.

Debido a estos requisitos, nació una estructura de datos importante.Esta estructura de datos se denomina tabla hash o tabla hash.

El proceso de inserción y búsqueda de elementos en la tabla hash es el siguiente:

1. Insertar elemento
Según el código clave del elemento a insertar, use esta función para calcular la ubicación de almacenamiento del elemento y almacenarlo de acuerdo con esta ubicación
2. El elemento de búsqueda
realiza el mismo cálculo en el código clave del elemento, y considera el valor de la función obtenida como el elemento.La ubicación de almacenamiento de, en la estructura, compare los elementos de acuerdo con esta ubicación
, si las claves son iguales, la búsqueda es exitosa.

 Dos funciones hash

2.1 Método del valor directo

Método de personalización directa : tome una función lineal de la palabra clave como la dirección hash: Hash(Clave) = A*Clave + B

1. Ventajas: simple y uniforme

2. Desventajas: Necesita conocer la distribución de palabras clave de antemano y usar solo un pequeño rango

3. Escenario de uso: adecuado para encontrar situaciones relativamente pequeñas y continuas

4. Escenarios no aplicables: cuando la distribución de datos es relativamente dispersa, como 1, 199847348, 90, 5, no es adecuado utilizar este método

 2.2 El método del resto de la división

Método de dividir el resto: establezca el número de direcciones permitidas en la tabla hash en m, tome un número primo p no mayor que m, pero más cercano o igual a m como divisor, de acuerdo con la función hash: Hash(clave) = clave % p(p< =m), convierte la clave en una dirección hash.

Ventajas: Amplio rango de uso, básicamente ilimitado.

Desventajas: Hay conflictos de hash, que necesitan ser resueltos, cuando hay muchos conflictos de hash, la eficiencia cae mucho.

 2.3 Varios métodos menos utilizados

1. Método de toma de cuadrados

hash(key)=key*key y luego toma los bits del medio del valor de retorno de la función como la dirección hash

Suponiendo que la palabra clave es 1234, el cuadrado es 1522756 y los 3 bits centrales 227 se extraen como la dirección hash; por ejemplo, la palabra clave es 4321, el cuadrado es 18671041 y los 3 bits centrales 671 (o 710) se extraen como la dirección hash. .

El método cuadrado es más adecuado: se desconoce la distribución de palabras clave y el número de dígitos no es muy grande.

2. Método de plegado

El método de plegado es dividir la palabra clave en varias partes con dígitos iguales de izquierda a derecha (la última parte puede ser más corta), luego superponer y sumar estas partes, y de acuerdo con la longitud de la tabla hash, tomar los últimos dígitos como la dirección de la columna hash. El método de plegado es adecuado para la distribución de palabras clave que no necesitan conocerse de antemano, y es adecuado para el caso en el que el número de palabras clave es relativamente grande.

3. Método de números aleatorios

Seleccione una función aleatoria y tome el valor de la función aleatoria de la palabra clave como su dirección hash, es decir, H(clave) = aleatorio(clave), donde aleatorio es la función de número aleatorio.

4. Análisis matemático

Hay n d dígitos, y cada dígito puede tener r símbolos diferentes. La frecuencia de estos r símbolos diferentes puede no ser la misma en cada bit, y puede estar distribuida uniformemente en algunos bits. Igualdad de oportunidades, distribución desigual en ciertos bits, solo ciertos tipos de símbolos aparecen con frecuencia. De acuerdo con el tamaño de la tabla hash, se pueden seleccionar varios bits con símbolos distribuidos uniformemente como la dirección hash. Suponiendo que se va a almacenar la tabla de registro de empleados de una empresa, si el número de teléfono móvil se usa como clave, es muy probable que los primeros 7 bits son los mismos. , entonces podemos elegir los siguientes cuatro bits como la dirección hash. Si dicho trabajo de extracción es propenso a conflictos, también podemos invertir los números extraídos (como 1234 a 4321), desplazamiento del anillo derecho (como 1234 para cambiar a 4123), desplazamiento del anillo izquierdo, superposición de los dos primeros números y los dos últimos números (por ejemplo, 1234 se cambia a 12+34=46) y otros métodos.
El método de análisis numérico suele ser adecuado para tratar la situación en la que el número de palabras clave es relativamente grande, si la distribución de las palabras clave se conoce de antemano y la distribución de varias partes de las palabras clave es relativamente uniforme.

Conflicto de los Tres Hassi

Conflicto hash significa que diferentes claves calculan la misma dirección de mapa hash a través de la misma función hash.Este fenómeno se denomina conflicto hash. Aquí hay un ejemplo:

Primero insertamos un conjunto  de pares clave-valor Key para  002931y Value para  王五 . ¿Cómo hacerlo?

El primer paso es  Key convertirlo en un subíndice de matriz a  través de una función hash 5.

Paso 2, si no hay ningún elemento en la posición correspondiente al subíndice 5 de la matriz,  Entry rellénelo hasta la posición del 5 subíndice .

Sin embargo, dado que la longitud de la matriz es limitada, cuando se insertan más y más entradas, los subíndices obtenidos por diferentes claves a través de la función hash pueden ser los mismos. Por ejemplo, el subíndice de matriz correspondiente a la clave 002936 es 2; el subíndice de matriz correspondiente a la clave 002947 también es 2.

Esta situación se llama  colisión hash. Vaya, la función hash está "colisionada", ¿qué debo hacer?
Las colisiones hash son inevitables y, como no se pueden evitar, debemos encontrar una manera de resolverlas. Hay dos formas principales de resolver las colisiones hash, una es el método de direccionamiento abierto y la otra es el método de lista enlazada.

 Método de cuatro direcciones abiertas

4.1 Detección lineal

El direccionamiento abierto también se denomina hashing cerrado.

El principio del método de direccionamiento abierto es muy simple, cuando una Clave obtiene el índice de matriz correspondiente a través de la función hash y ha sido ocupada, podemos "buscar otro trabajo" para encontrar la siguiente posición libre.

Tomando la situación anterior como ejemplo, Entry6 obtiene el subíndice 2 a través de la función hash, y el subíndice ya tiene otros elementos en la matriz, así que muévase 1 bit hacia atrás para ver si la posición del subíndice 3 en la matriz está libre.

 Desafortunadamente, el subíndice  3 ya está ocupado, así que mueva  1 el bit hacia atrás para ver si la posición del 4 subíndice está libre.

Afortunadamente, la posición del 4 subíndice  no está ocupada, por lo que se Entry6 almacena en la posición del 4 subíndice .

 Podemos encontrar que cuantos más datos de la tabla hash, más grave es el conflicto hash. Además, la tabla hash se implementa en base a matrices, por lo que la tabla hash también implica el tema de la expansión.

Cuando la tabla hash alcanza una cierta saturación después de múltiples inserciones de elementos, la probabilidad de un conflicto en la posición de mapeo clave aumentará gradualmente. De esta manera, una gran cantidad de elementos se amontonan en la misma posición de subíndice de matriz, formando una lista enlazada muy larga, lo que tiene un gran impacto en el rendimiento de las operaciones de inserción y operaciones de consulta posteriores. En este momento, la tabla hash necesita expandir su longitud, es decir, expandir .

Entonces, ¿en qué circunstancias se debe expandir la tabla hash? ¿Cómo expandirse? El factor de carga de la tabla hash que se presenta aquí se define como: α = el número de elementos llenados en la tabla / la longitud de la tabla hash α es el factor de signo de la plenitud de la tabla hash . Dado que la longitud de la tabla es un valor fijo, α es proporcional al "número de elementos que se llenan en la tabla", por lo que cuanto mayor sea α, más elementos se llenan en la tabla y mayor es la posibilidad de conflicto ; por el contrario , α Cuanto más pequeño es, menos elementos se indican para completar la tabla y es menos probable que entre en conflicto . De hecho, la longitud de búsqueda promedio de una tabla hash es una función del factor de carga o, solo que los diferentes métodos de manejo de colisiones tienen funciones diferentes.
Para el método de direccionamiento abierto, el factor de carga es un factor particularmente importante y debe limitarse estrictamente por debajo de 0,7-0,8. Si excede 0.8 , las fallas de caché de la CPU (cachemissing) durante la búsqueda en la tabla aumentarán de acuerdo con una curva exponencial. Por lo tanto, algunas bibliotecas hash que utilizan el método de direccionamiento abierto, como la biblioteca del sistema de Java, limitan el factor de carga a 0,75 y la tabla hash se redimensionará más allá de este valor.

4.2 Detección secundaria

El defecto de la detección lineal es que los datos conflictivos se acumulan en una sola pieza, lo que está relacionado con encontrar la siguiente posición vacía, porque la forma de encontrar la posición vacía es buscar una por una, así que para evitar este problema, la segunda detección encontrará la siguiente posición vacía El método para una posición vacía es: H, =(Ho+i2 )% m, o: H, =(Ho -i2 )% m. Donde: i=1,2,3..., H es la posición que se obtiene al calcular la clave del elemento mediante la función hash Hash(x), y m es el tamaño de la tabla. Para 2.1, si desea insertar 44, se produce un conflicto y la situación después de usar la solución es:

 La investigación muestra que: cuando la longitud de la tabla es un número primo y el factor de carga de la tabla a no excede 0,5, la nueva entrada de la tabla debe poder insertarse y ninguna posición se probará dos veces. Entonces, mientras haya la mitad de las posiciones vacías en la mesa, no hay problema de que la mesa esté llena. No es necesario considerar la situación de que la mesa esté llena al buscar, pero se debe asegurar que el factor de carga a de la mesa no supere 0.5 al insertar, si lo supera, se debe considerar la capacidad.

Código correspondiente:

Aquí hay una explicación de cómo se ve la tabla hash:

Encuentra  Key el  valor correspondiente 002936 en  Entry la tabla hash para . ¿Cómo hacerlo? Tomemos como ejemplo el método de la lista enlazada.

 Paso 1: Convierta la clave en el subíndice 2 de la matriz a través de la función hash.

Paso 2: Encuentre el elemento correspondiente al subíndice 2 de la matriz, si la clave de este elemento es 002936, entonces se encuentra, si la clave no es 002936, no importa, continúe desde la siguiente posición, si la siguiente position está vacía, significa que sin este número, si se encuentra la última posición de la matriz, la búsqueda continúa desde el principio.

#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étodo de cinco cremalleras

1. Concepto
de hash abierto El método de hash abierto también se denomina método de dirección de cadena (método zipper). Primero, la función hash se usa para calcular la dirección hash para el conjunto de códigos clave. Los códigos clave con la misma dirección pertenecen al mismo subconjunto. y cada subconjunto se denomina It is a bucket, los elementos de cada cubeta están vinculados por una lista vinculada de forma individual, y el nodo principal de cada lista vinculada se almacena en la tabla hash. El método zip es diferente del método de direccionamiento abierto.Cada elemento de la matriz en el método zip no es solo un objeto de entrada, sino también el nodo principal de una lista vinculada. Cada objeto de entrada apunta a su siguiente nodo de entrada a través del siguiente puntero. Cuando la nueva entrada se asigna a la posición de matriz en conflicto, solo necesita insertarse en la lista vinculada correspondiente.

 He aquí cómo encontrarlo en el método de la cremallera:

Encuentre el valor correspondiente a la Entrada cuya clave es 002936 en la tabla hash. ¿Cómo hacerlo? Tomemos como ejemplo el método de la lista enlazada. El primer paso es convertir la clave en un subíndice de matriz 2 a través de la función hash. El segundo paso es encontrar el elemento correspondiente al subíndice 2 del arreglo, si la clave de este elemento es 002936 entonces se encuentra, si la clave no es 002936 no importa, ya que cada elemento del arreglo corresponde a una lista enlazada, podemos ordenar Ir hacia abajo en la lista enlazada lentamente para ver si puede encontrar un nodo que coincida con la Clave.

Por supuesto, el hash también debe expandirse:

El número de cubos es fijo. Con la inserción continua de elementos, el número de elementos en cada cubo continúa aumentando. En casos extremos, puede haber muchos nodos de lista enlazada en un cubo, lo que afectará el rendimiento del hash. tabla. Por lo tanto, bajo ciertas condiciones, la tabla hash debe aumentarse. ¿Cómo confirmar las condiciones? El mejor caso para el hashing abierto es que hay exactamente un nodo en cada cubo de hash, y cuando se continúan insertando elementos, siempre se producirán colisiones de hash. Por lo tanto, cuando el número de elementos es exactamente igual al número de cubos , puede dar la expansión de la tabla Hash.

Los pasos de expansión son los siguientes:

1. Para expandir, cree una nueva matriz vacía de entrada cuya longitud sea el doble de la matriz original.
2. Vuelva a hacer hash, recorra la matriz de entrada original y vuelva a hacer hash de todas las entradas en la nueva matriz. ¿Por qué volver a hacer hash? Porque después de que se expande la longitud, las reglas de Hash también cambian. Después de la expansión, la tabla hash originalmente abarrotada vuelve a ser escasa y la entrada original se redistribuye de la manera más uniforme posible.

Antes de la expansión:

Después de la expansión:

En el método de la cremallera, si un determinado conflicto de cadena es muy grave, puede optar por colgar un árbol rojo-negro en la posición correspondiente.

por código

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

Supongo que te gusta

Origin blog.csdn.net/qq_56999918/article/details/123316914
Recomendado
Clasificación