Un artículo para ayudarte a aprender qué es el hash.

Insertar descripción de la imagen aquí

concepto de hachís

El hash se usa ampliamente en C ++ y es una estructura de datos y un algoritmo que se utiliza para buscar y almacenar datos rápidamente. Las siguientes son algunas aplicaciones comunes de hash en C++:

  1. Tabla hash: una tabla hash es una estructura de datos eficiente que se utiliza para almacenar pares clave-valor. En C++, std::unordered_mapy std::unordered_setson las implementaciones de tablas hash proporcionadas por la biblioteca estándar. Estos contenedores proporcionan operaciones de búsqueda, inserción y eliminación en tiempo constante, lo que los hace ideales para buscar y almacenar datos rápidamente.
#include <unordered_map>
#include <unordered_set>

std::unordered_map<std::string, int> hashMap;
hashMap["apple"] = 3;
int value = hashMap["apple"];

std::unordered_set<int> hashSet;
hashSet.insert(42);
bool exists = hashSet.count(42) > 0;
  1. Función hash: una función hash asigna datos de entrada a un código hash de tamaño fijo (valor hash), generalmente un número entero. La biblioteca estándar de C++ proporciona múltiples funciones hash y también permite a los usuarios personalizarlas.
std::hash<int> intHash;
size_t hashCode = intHash(42);
  1. Colecciones Hash y mapas Hash: además de las tablas hash de la biblioteca estándar, C++ también proporciona otras bibliotecas e implementaciones de terceros, como las tablas hash y de Google, que proporcionan una funcionalidad absl::flat_hash_mapsimilar absl::flat_hash_setpero con una menor sobrecarga de memoria.
  2. Los conjuntos de hash se utilizan para la deduplicación: al insertar elementos en un conjunto de hash, los duplicados se pueden eliminar rápidamente.
std::unordered_set<int> uniqueValues;
uniqueValues.insert(1);
uniqueValues.insert(2);
uniqueValues.insert(1); // 重复元素将被自动去重
  1. Aplicación de hash en criptografía: las funciones hash se utilizan en criptografía para almacenar y verificar contraseñas. Las funciones hash de contraseñas más utilizadas incluyen SHA-256 y bcrypt.
  2. Implementación de caché: las tablas hash se pueden usar para implementar el almacenamiento en caché. Al almacenar pares clave-valor en una tabla hash, los datos en el caché se pueden buscar en tiempo constante.
  3. Verificación de la unicidad de los datos: las funciones hash y las tablas hash se pueden utilizar para verificar la unicidad de los datos y garantizar que no se almacenen datos duplicados.

En la estructura secuencial y el árbol equilibrado, no existe una relación correspondiente entre la clave del elemento y su ubicación de almacenamiento, por lo que al buscar un elemento se deben realizar múltiples comparaciones de la clave. La complejidad temporal de la búsqueda secuencial es O(N), en un árbol equilibrado, la altura del árbol, es decir O(log_2 N), la eficiencia de la búsqueda depende del número de comparaciones de elementos durante el proceso de búsqueda.

El método de búsqueda ideal: puede obtener los elementos que se buscarán directamente desde la tabla al mismo tiempo sin ninguna comparación .
Si construye una estructura de almacenamiento y utiliza una determinada función (hashFunc) para establecer una relación de mapeo uno a uno entre la ubicación de almacenamiento del elemento y su código clave, entonces el elemento se puede encontrar rápidamente a través de esta función durante la búsqueda.

Al ingresar a esta estructura :

Insertar un elemento.
De acuerdo con el código clave del elemento a insertar, utilice esta función para calcular la ubicación de almacenamiento del elemento y almacenar el
elemento
de búsqueda de acuerdo con esta ubicación . Realice el mismo cálculo en el código clave del elemento, y utilice el valor de función obtenido como ubicación de almacenamiento del elemento. Compare los elementos en esta posición en la estructura. Si los códigos clave son iguales, la búsqueda es exitosa.

Este método es el método hash (hash). La función de conversión utilizada en el método hash se llama función hash (hash) y la estructura construida se llama tabla hash (tabla hash) (o tabla hash). Por ejemplo:
Datos establecer {1, 7, 6, 4, 5, 9};

La función hash se establece en:; hash(key) = key % capacityla capacidad es el tamaño total del espacio subyacente para almacenar elementos
Insertar descripción de la imagen aquí

La búsqueda con este método no requiere comparaciones múltiples de códigos clave, por lo que la velocidad de búsqueda es más rápida, sin embargo, la pregunta que surge es: ¿qué debo hacer si los restos de dos números son iguales al tomar el resto ?

colisión de hash

En una forma de almacenamiento de tabla hash (tabla hash), un conflicto hash se refiere a cuando la función hash calcula diferentes claves (o datos) y obtienen el mismo valor hash e intentan almacenarlo en la misma tabla hash. (o ubicación de la tabla hash) . Dado que las funciones hash asignan datos de entrada a un número limitado de depósitos, y la cantidad de datos de entrada puede ser mucho mayor que la cantidad de depósitos, las colisiones hash son un problema común en las tablas hash.

Las colisiones hash pueden causar los siguientes problemas :

  1. Sobrescritura de datos: si se asignan dos claves diferentes a la misma ubicación del depósito, los datos de una clave sobrescribirán los datos de la otra clave, lo que provocará la pérdida de datos.
  2. Eficiencia de búsqueda reducida: cuando varias claves se asignan a la misma ubicación del depósito, encontrar una clave específica puede ser menos eficiente porque se debe buscar dentro del depósito para encontrar la clave correcta.

Para resolver colisiones hash, las tablas hash suelen utilizar uno de los siguientes métodos :

  1. Encadenamiento: en este método, cada depósito almacena una lista vinculada, una matriz u otra estructura de datos que almacena múltiples elementos asignados al mismo valor hash. Cuando se produce una colisión de hash, se agregan nuevos elementos a la lista o matriz vinculada dentro del depósito sin sobrescribir los elementos existentes.
  2. Direccionamiento abierto: en este método, cuando se produce una colisión de hash, el algoritmo busca el siguiente depósito disponible en la tabla hash e inserta datos en ese depósito hasta que se encuentra un depósito libre. Este proceso suele implicar una serie de métodos de detección, como detección lineal, detección secundaria, etc.
  3. Buena función hash: elegir la función hash correcta puede reducir la aparición de colisiones hash. Una buena función hash debería asignar datos en depósitos de la manera más uniforme posible, reduciendo así la probabilidad de colisiones.

El manejo de conflictos hash es un tema importante que debe considerarse al diseñar e implementar tablas hash. Diferentes escenarios de aplicación pueden requerir diferentes estrategias de resolución de conflictos. Los métodos razonables de manejo de conflictos pueden mejorar el rendimiento y la eficiencia de las tablas hash.

Función hash

Principios de diseño de la función hash :

El dominio de la función hash debe incluir todas las claves que deben almacenarse, y si la tabla hash permite m direcciones, su rango de valores debe estar entre 0 y m-1. Las direcciones calculadas por la función hash se pueden distribuir uniformemente en todo
el En el espacio
, la función hash debería ser relativamente simple.

1. Método de direccionamiento directo

El "direccionamiento directo" es una tecnología de hash (hash) simple y eficaz, que a menudo se utiliza para resolver el problema de almacenamiento y recuperación de pares clave-valor. En el método de direccionamiento directo, cada clave posible corresponde a un depósito (o ranura), y estos depósitos se asignan de acuerdo con el rango de claves, generalmente implementado como una matriz .

La idea central de este enfoque es que cada clave se asigna directamente a un depósito específico, por lo que idealmente no hay colisiones de hash ya que cada clave tiene un índice de depósito único .

Las siguientes son las principales características y limitaciones del método de direccionamiento directo :

  1. Complejidad del espacio : este método requiere asignar espacio de almacenamiento lo suficientemente grande como para acomodar todas las claves posibles, por lo que la complejidad del espacio depende del tamaño del rango de las claves. Si el rango de claves es grande, se producirá un alto consumo de memoria.
  2. Resolución de conflictos : el direccionamiento directo generalmente no requiere una estrategia de resolución de conflictos porque cada clave tiene un índice de depósito único. Esto hace que las operaciones de almacenamiento y recuperación sean O (1) de complejidad de tiempo constante y muy eficientes.
  3. Aplicabilidad : el direccionamiento directo suele funcionar cuando el rango de claves es relativamente pequeño y contiguo. Si el rango de claves es muy grande o no es contiguo, este enfoque puede no ser adecuado ya que requeriría asignar una gran cantidad de espacio de almacenamiento.
  4. Ejemplo : un ejemplo común es utilizar el direccionamiento directo para implementar una estructura de datos que contiene claves enteras, como un contador. Si tiene un contador que necesita realizar un seguimiento de las apariciones de una gran cantidad de valores enteros, puede usar una matriz donde el índice de la matriz sea el valor entero y el valor de la matriz sea el número de apariciones de ese valor entero.
// 使用直接定址法实现计数器
const int MAX_RANGE = 1000; // 假设整数范围在0到999之间
int counter[MAX_RANGE] = {
    
    0};

// 增加某个整数值的计数
int key = 42;
counter[key]++;

En resumen, el direccionamiento directo es un método de hash simple pero eficaz, adecuado para situaciones en las que el rango de claves es relativamente pequeño y contiguo. Proporciona operaciones de almacenamiento y recuperación con una complejidad de tiempo constante, pero es necesario prestar atención al rango de claves y al consumo de memoria, y es adecuado para situaciones en las que la búsqueda es relativamente pequeña y continua.

2. Método de división con resto

El método de división es una técnica de hash común que se utiliza para asignar claves en depósitos (ranuras) de tablas hash o índices de matriz. La idea básica es calcular un valor hash dividiendo la clave por un número adecuado y tomando el resto, y luego usar el valor hash como índice del depósito. Este resto debe ser un número entero positivo pequeño, normalmente un número primo, para garantizar una buena distribución .

Los pasos para la división con restos son los siguientes :

  1. Elija un divisor adecuado (normalmente un número primo), denotado por M.
  2. Cálculo hash en la clave K: valor hash = K % M.
  3. Utilice el valor hash como índice del depósito y almacene los datos en el depósito correspondiente.

La ventaja de este método es que es simple y fácil de implementar. Sin embargo, también tiene algunas limitaciones y advertencias :

  1. Elija un divisor adecuado : Elegir un divisor M adecuado es crucial para el método de división con resto. Una buena elección garantiza una buena distribución de hash y evita colisiones de hash. A menudo, elegir números primos puede ayudar a reducir la probabilidad de conflictos.
  2. Distribución uniforme : para obtener una buena distribución hash, las claves deben distribuirse uniformemente en todo el espacio de claves. Si las claves se distribuyen de manera desigual, algunos depósitos pueden estar superpoblados mientras que otros tienen pocos o ningún dato.
  3. Manejo de números negativos : el método de dividir y dejar el resto generalmente requiere que las claves sean números enteros positivos. Si necesita cifrar números negativos, puede considerar algunos ajustes, como convertir números negativos en números positivos.
  4. Manejo de colisiones de hash : aunque el método de división y resto reduce la probabilidad de colisiones, aún pueden ocurrir colisiones. En aplicaciones prácticas, normalmente es necesario utilizar estrategias de resolución de conflictos, como encadenamiento o direccionamiento abierto, para manejar los conflictos.

3. Método cuadrado-medio

El "Método del cuadrado medio" es un método simple de generación de números pseudoaleatorios, que generalmente se utiliza para generar secuencias de números enteros pseudoaleatorios. Su idea básica es generar números pseudoaleatorios de forma iterativa elevando al cuadrado un número entero y luego tomando el número del medio como el siguiente entero . A pesar de su simplicidad, su calidad es generalmente inferior a la de algoritmos de generación de números pseudoaleatorios más complejos.

Los siguientes son los pasos básicos para encontrar el centro de cuadrados :

  1. Elija una semilla inicial (o valor inicial), generalmente un número entero positivo.
  2. Cuadra la semilla y obtén un número entero más grande.
  3. Del resultado de este cuadrado, saca el número de dígitos del medio como el siguiente número pseudoaleatorio.
  4. Utilice este nuevo número pseudoaleatorio como semilla para la siguiente ronda y repita los pasos anteriores.

Supongamos que la palabra clave es 1234 y al cuadrado es 1522756. Extraiga los 3 dígitos del medio 227 como dirección hash.
Por ejemplo, si la palabra clave es 4321, al cuadrado es 18671041. Extraiga los 3 dígitos del medio 671 (o 710) como hash DIRECCIÓN.

La idea central de este método es generar números pseudoaleatorios elevando al cuadrado repetidamente y tomando el número del medio. Sin embargo, la calidad y uniformidad del método del centro del cuadrado es generalmente inferior a la de los algoritmos de generación de números pseudoaleatorios más complejos, por lo que se utiliza principalmente para la enseñanza o para algunos problemas de simulación simples.

El rendimiento y la uniformidad del método de centrado al cuadrado dependen de la elección de la semilla inicial y del número de bits extraídos . Si la semilla se elige mal o el número de bits extraídos es inadecuado, puede dar lugar a secuencias de números pseudoaleatorios periódicos o desiguales. Por lo tanto, en aplicaciones prácticas, generalmente se utilizan generadores de números pseudoaleatorios más avanzados y confiables, como Linear Congruential Generator o Mersenne Twister, para obtener números pseudoaleatorios de mayor calidad. Número, el método del cuadrado es más adecuado para situaciones donde la distribución No se conoce el número de palabras clave y el número de dígitos no es muy grande .

4. Método de plegado

El método de plegado es una técnica de hash que se utiliza a menudo para asignar claves enteras grandes o de cadenas largas a valores hash más pequeños para que puedan almacenarse en una tabla hash o en el centro de una tabla hash. La idea básica de plegar es dividir la clave de entrada en partes de longitud fija y luego agregar las partes o realizar otras operaciones matemáticas para generar un valor hash .

Los siguientes son los pasos generales para el método de plegado :

  1. Elija un tamaño de división apropiado, normalmente un número entero positivo. Este tamaño se puede elegir según las necesidades de la aplicación y suele ser un valor que permite dividir las claves de entrada de manera uniforme.
  2. Divida una clave de entrada en partes de tamaño fijo (fragmentos divididos), que pueden ser caracteres consecutivos, números u otras unidades apropiadas. Si las entradas son números, generalmente se dividen en un número igual de partes.
  3. Realizar una operación matemática sobre estas partes, normalmente una suma. Se pueden seleccionar diferentes operaciones matemáticas como suma, operaciones de bits, etc. para generar valores hash según la situación.
  4. El resultado final es un valor hash, que se puede utilizar como índice de depósito en una tabla hash o tabla hash.

Aquí hay un ejemplo simple de cómo asignar una clave entera a un hash usando el plegado :

Supongamos que la clave de entrada es 1234567890, elegimos un tamaño de bloque dividido de 3 y luego dividimos este número entero en 1, 234, 567 y 890. A continuación, sumamos las partes y obtenemos el valor hash: 1 + 234 + 567 + 890 = 1692. Este valor hash se puede utilizar para almacenar y recuperar datos.

El rendimiento y la uniformidad del método de plegado dependen del tamaño de la división y de la elección de las operaciones matemáticas . Si se elige correctamente, puede proporcionar una mejor distribución de hash, pero los parámetros deben elegirse con cuidado para evitar posibles problemas como colisiones o distribuciones de hash desiguales. El método de plegado es adecuado para palabras clave sin conocer la distribución de las palabras clave de antemano . donde el número de dígitos es relativamente grande .

5. Método de números aleatorios

Elija 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(key) = random(key), donde aleatoria es una función de número aleatorio.
Este método se suele utilizar cuando las palabras clave tienen diferentes longitudes.

6. Análisis matemático

Supongamos que hay n d dígitos. Cada dígito puede tener r símbolos diferentes. La frecuencia de estos r símbolos diferentes que aparecen en cada dígito puede no ser la misma. Pueden estar distribuidos uniformemente en algunos dígitos. La frecuencia de cada símbolo que aparece es Igualdad de oportunidades, Distribución desigual en algunos bits, sólo ciertos símbolos aparecen con frecuencia. Según el tamaño de la tabla hash, se pueden seleccionar como dirección hash varios bits en los que varios símbolos estén distribuidos uniformemente.

Supongamos que queremos almacenar el formulario de registro de empleados de una empresa. Si utilizamos números de teléfono móvil como palabras clave, entonces es muy probable que los primeros 7 dígitos sean iguales. Entonces podemos elegir los últimos cuatro dígitos como dirección hash. Si tal extracción El trabajo es fácil. Si se produce un conflicto, también puede invertir los números extraídos (como 1234 a 4321), desplazar el anillo derecho (como 1234 a 4123), desplazar el anillo izquierdo y superponer los dos primeros números y los dos últimos. números (como 1234 a 4123). 12+34=46) y otros métodos.

El método de análisis numérico suele ser adecuado para situaciones de procesamiento en las que los dígitos de las palabras clave son relativamente grandes. Si la distribución de las palabras clave se conoce de antemano y la distribución de varios dígitos de las palabras clave es relativamente uniforme,

Cuanto más sofisticada esté diseñada la función hash, menor será la posibilidad de colisiones hash, pero las colisiones hash no se pueden evitar.

Resolución de conflictos de hash

Dos formas comunes de resolver colisiones de hash son: hash cerrado y hash abierto

1. hash cerrado

Hash cerrado: también llamado método de direccionamiento abierto. Cuando ocurre un conflicto de hash, si la tabla hash no está llena, significa que debe haber una posición vacía en la tabla hash, entonces la clave se puede almacenar en la posición en conflicto "bajo go a una posición vacía

1.1 Detección lineal

Por ejemplo, en el escenario mencionado en el concepto anterior, ahora necesita insertar el elemento 44. Primero, calcule la dirección hash a través de la función hash. El hashAddr es 4, por lo que teóricamente debería insertarse 44 en esta posición, pero el valor de En esta posición ya se han colocado 4 elementos, es decir, se produce una colisión hash.

Detección lineal: comenzando desde la posición donde ocurre el conflicto, detecte hacia atrás hasta encontrar la siguiente posición vacía .

insertar

Obtenga la posición del elemento que se insertará en la tabla hash a través de la función hash.
Si no hay ningún elemento en la posición, inserte el nuevo elemento directamente. Si hay un conflicto hash con el elemento en la posición, use la detección lineal para busque la siguiente posición vacía e inserte el nuevo elemento.

Insertar descripción de la imagen aquí

Eliminación
Cuando se utiliza hash cerrado para manejar conflictos de hash, no se pueden eliminar físicamente los elementos existentes en la tabla hash. La eliminación directa de elementos afectará la búsqueda de otros elementos. Por ejemplo, si elimina el elemento 4 directamente, la búsqueda de 44 puede verse afectada. Por lo tanto, el sondeo lineal utiliza una pseudoeliminación marcada para eliminar un elemento.

Cada espacio en la tabla hash recibe una marca
VACÍO. Esta posición está vacía. Ya hay un elemento en esta posición EXIST. Los elementos DELETE han sido eliminados.

enum State{
    
    EMPTY, EXIST, DELETE}; 

Implementación de detección lineal

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

Defina una estructura de plantilla HashDatapara representar elementos de datos en la tabla hash. Contiene dos miembros principales:

  1. _kv: Este es un par clave-valor ( pair<K, V>), que se utiliza para almacenar datos correspondientes al valor clave.
  2. _state: Este es un tipo de enumeración Stateque representa el estado del elemento de datos, que puede ser uno de los tres siguientes:
    • EMPTY: Indica que el slot está vacío, es decir, no hay datos.
    • EXIST: Indica que la ranura contiene datos válidos.
    • DELETE: Indica que la ranura contiene datos eliminados.

Esta estructura está diseñada para almacenar pares clave-valor en una tabla hash y realizar un seguimiento del estado de cada ranura. La existencia de estado _statepermite que la tabla hash maneje eliminaciones, no solo inserciones y búsquedas.

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

Define una plantilla de función hash genérica HashFuncque se puede utilizar para cualquier tipo de clave K. La implementación de esta función hash es muy simple: keyconvierte la clave de entrada directamente en size_ttipo y regresa.

Específicamente, los pasos de operación de esta función hash son:

  1. Acepta una Kclave de tipo keycomo parámetro de entrada.
  2. Transmitir claves keya size_t, es decir, asignar claves de diferentes tipos a un entero sin signo.
  3. Devuelve el size_tvalor convertido como resultado hash.

Cabe señalar que es posible que no funcione para todos los tipos de claves, especialmente para tipos de datos personalizados, que pueden requerir funciones hash más complejas para garantizar un buen rendimiento y uniformidad del hash.

template<>
struct HashFunc<string>//对字符型特化给定值乘以固定质数131降低冲突
{
    
    
	size_t operator()(const string& key)
	{
    
    
		size_t val = 0;
		for (auto ch : key)
		{
    
    
			val *= 131;
			val += ch;
		}

		return val;
	}
};

Define una versión especializada de la función hash HashFuncpara manejar stringclaves de tipo cadena (). Esta función hash convierte cada carácter de la cadena en su valor entero correspondiente y los combina para producir un valor hash.

Así es como funciona esta versión especializada de la función hash:

  1. Iterar sobre cada carácter de una cadena.
  2. Para cada carácter, multiplique el valor hash actual por un número primo fijo (131) y luego agregue el valor entero del carácter.
  3. Repita los pasos 1 y 2 hasta haber atravesado toda la cuerda.
  4. Devuelve el valor hash final como resultado del hash de cadena.

Esta función hash se caracteriza por ser simple y efectiva, toma en cuenta cada carácter de la cadena en el valor hash y utiliza el número primo 131 para mezclar (consulte la implementación en el código fuente STL) para aumentar la uniformidad del hachís sexo. Este método puede producir buenos resultados de hash en muchas situaciones.

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    
    
private:
	vector<HashData<K, V>> _tables;
	size_t _size = 0; // 存储多少个有效数据
};

Defina una clase de plantilla HashTablepara implementar la estructura de datos de la tabla hash. Esta tabla hash puede almacenar pares clave-valor, donde la clave es de tipo Ky el valor es de tipo V, y opcionalmente se puede especificar el tipo de función hash. De forma predeterminada, se utiliza HashFunc<K>como función hash.

Los siguientes son los principales miembros y características de esta clase de tabla hash:

  1. _tables: Este es un HashData<K, V>vector ( ) que almacena elementos de datos vectory representa el espacio de almacenamiento de la tabla hash. Cada elemento corresponde a una ranura en la tabla hash y puede almacenar un par clave-valor. El tamaño de la tabla hash está determinado por el tamaño del vector.
  2. _size: Este es un contador que registra la cantidad de elementos de datos válidos e indica cuántos pares clave-valor válidos están almacenados actualmente en la tabla hash. Se actualiza durante las operaciones de inserción y eliminación _size.
  3. Función hash predeterminada: a través de los parámetros de la plantilla Hash, opcionalmente puede especificar una función hash personalizada. Si no se proporciona ningún tipo de función hash personalizada, se utilizará la predeterminada HashFunc<K>como función hash. Esto permite utilizar diferentes funciones hash en diferentes tipos de claves.

Las siguientes funciones miembro son todas públicas.

función de inserción

La función de inserción debe considerar el problema de la expansión, y en la expansión del formulario de almacenamiento de la tabla hash, debemos considerar el problema del factor de carga.

El factor de carga de una tabla hash se define como :α=填入表中的元素个数/散列表的长度

α es un factor que indica qué tan llena está la tabla hash. Dado que la longitud de la tabla es un valor fijo, α es proporcional al "número de elementos completados en la tabla", por lo que cuanto mayor es α, más elementos se completan en la tabla y mayor es la posibilidad de conflicto; por el contrario, α Cuanto menor sea el valor, menos elementos se completarán en la tabla y es menos probable que ocurra un conflicto. De hecho, la longitud de búsqueda promedio de una tabla hash es función del factor de carga α, pero diferentes métodos para manejar colisiones tienen diferentes funciones.

Para el método de direccionamiento abierto, el factor de carga es un factor particularmente importante y debe limitarse estrictamente a 0.7-0.8lo siguiente. Si se excede 0.8, las pérdidas de caché de la CPU durante la búsqueda de tablas aumentarán según una curva exponencial. Por lo tanto, algunas bibliotecas hash que utilizan métodos de direccionamiento abiertos, como la biblioteca del sistema de Java, limitan el factor de carga a 0,75. Si se excede este valor, se cambiará el tamaño de la tabla hash.

bool Insert(const pair<K, V>& kv)
{
    
    
	if (Find(kv.first))
		return false;

	// 负载因子到了就扩容
	if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
	{
    
    
		size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		HashTable<K, V, Hash> newHT;
		newHT._tables.resize(newSize);
		// 旧表的数据映射到新表
		for (auto e : _tables)
		{
    
    
			if (e._state == EXIST)
			{
    
    
				newHT.Insert(e._kv);
			}
		}

		_tables.swap(newHT._tables);
	}

	Hash hash;
	size_t hashi = hash(kv.first) % _tables.size();
	while (_tables[hashi]._state == EXIST)
	{
    
    
		hashi++;
		hashi %= _tables.size();
	}

	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	++_size;

	return true;
}

InsertImplementación del método utilizado para insertar nuevos pares clave-valor en la tabla hash. Los siguientes son los pasos principales y la lógica del método:

  1. FindPrimero, verifique si ya existe un elemento de datos con la misma clave llamando al método. Si existe la misma clave, la inserción falla y regresa directamente false.
  2. A continuación, verifique el factor de carga de la tabla hash. El factor de carga es la relación entre el número de elementos de datos válidos actualmente almacenados _sizey el número de espacios de la tabla hash _tables.size(). Si el factor de carga excede el umbral especificado (7/10), significa que la tabla hash está sobrecargada y es necesario expandirla. El propósito de la expansión es mantener el rendimiento y la uniformidad de la tabla hash.
  3. Si se requiere expansión, cree una nueva tabla hash newHTcon el doble de tamaño que la tabla hash actual (o inicialícela a 10 si la tabla hash actual está vacía). Luego, _tablesasigne los datos de la tabla hash anterior a la nueva tabla hash newHTllamando newHT.Insert(e._kv)a insertar cada elemento de datos válido en la nueva tabla hash.
  4. Una vez que se completa la asignación de datos, swapintercambie la tabla hash antigua _tablesy la nueva tabla hash llamando a la función newHTpara convertir la nueva tabla hash en la tabla hash actual.
  5. A continuación, calcule el hash de la clave que se insertará. El índice de ranura que se insertará se determina aplicando hash a la hashclave llamando a una función hash y luego realizando la operación de módulo .kv.firsthashi
  6. Utilice sondeo lineal para encontrar ranuras disponibles. Si _tables[hashi]el estado de la ranura actual es EXIST, continúe buscando la siguiente ranura hasta encontrar una ranura vacía.
  7. Una vez que se encuentra una ranura vacía, el par clave-valor se kvalmacena en la ranura y el estado se marca EXISTpara indicar que la ranura contiene datos válidos. Luego, incremente el número de elementos de datos válidos _size.
  8. Finalmente, return trueindica una inserción exitosa.

Encontrar función

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

    Hash hash;
    size_t start = hash(key) % _tables.size();
    size_t hashi = start;
    while (_tables[hashi]._state != EMPTY)
    {
    
    
        if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
        {
    
    
            return &_tables[hashi];
        }

        hashi++;
        hashi %= _tables.size();

        if (hashi == start)
        {
    
    
            break;
        }
    }

    return nullptr;
}

FindLa implementación del método se utiliza para encontrar el elemento de datos de la clave especificada y devolver el HashData<K, V>puntero correspondiente. Los siguientes son los pasos principales y la lógica del método:

  1. Primero, verifique el tamaño de la tabla hash actual. Si la tabla hash está vacía (es decir, _tables.size()es 0), la búsqueda no se puede realizar y un retorno directo nullptrindica que no se encuentra.
  2. Crea un objeto de función hash hashy utiliza la función hash para calcular keyel valor hash de la clave dada. % _tables.size()El valor hash se utiliza para obtener el índice de ranura mediante operación de módulo start, indicando la posición donde comienza la búsqueda.
  3. Inicialice un índice hash y comience a buscar hashidesde la ranura . startIngrese al bucle.
  4. En el bucle, primero verifique _tables[hashi]el estado de la ranura actual. Si el estado es EMPTY, significa que la ranura actual está vacía, lo que indica que no se encuentra la clave especificada y la búsqueda continúa en la siguiente ranura.
  5. Si el estado no es así EMPTY, continúe verificando el estado del elemento de datos _tables[hashi]._state. Si el estado es DELETE, significa que los datos en la ranura actual se han eliminado y la búsqueda de la siguiente ranura continuará.
  6. Si el estado no es EMPTYy no lo es DELETE, significa que la ranura actual contiene datos válidos. Continúe comprobando si la clave del elemento de datos _tables[hashi]._kv.firstes igual a la clave de destino key. Si es igual, se encuentra la clave especificada y se devuelve un puntero al elemento de datos &_tables[hashi].
  7. Si no se cumple ninguna de las condiciones anteriores, significa que los datos en el espacio actual no coinciden con la clave de destino y la búsqueda continúa en el siguiente espacio. La búsqueda de bucle se implementa incrementando hashiy tomando módulo ._tables.size()
  8. El bucle continúa hasta llegar nuevamente a la posición inicial start, lo que indica que toda la tabla hash se ha recorrido una vez y no se encontró ninguna clave coincidente. Salga del bucle en este punto.
  9. Finalmente, si todo el ciclo termina sin encontrar una clave coincidente, return nullptrsignifica no encontrada.

función de eliminación

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

EraseLa implementación del método se utiliza para eliminar el elemento de datos correspondiente a la clave especificada. Los siguientes son los pasos principales y la lógica del método:

  1. Primero, llame Findal método para encontrar keyel elemento de datos correspondiente a la clave especificada. Si se encuentra un elemento de datos coincidente, Findel método devuelve un puntero al elemento de datos y lo almacena en formato ret. Si no se encuentra ningún elemento de datos coincidente, Findel método devuelve nullptr.
  2. A continuación, compruebe retsi es un puntero no nulo. Si retno está vacío, significa que se ha encontrado un elemento de datos coincidente y se puede realizar la operación de eliminación.
  3. En la operación de eliminación, establezca el estado del elemento de datos coincidente _stateen DELETE, lo que indica que el elemento de datos se ha eliminado.
  4. Al mismo tiempo, el recuento del número de elementos de datos válidos en la tabla hash disminuye _sizepara reflejar la eliminación.
  5. Finalmente, return trueindica una eliminación exitosa.
  6. Si retestá vacío (es decir, no se encuentra ningún elemento de datos coincidente), se devuelve falsepara indicar que la eliminación falló.

Este Erasemétodo implementa la eliminación de elementos de datos en la tabla hash, marcando el estado como DELETEindicativo del estado de eliminación, en lugar de eliminar realmente los datos de la tabla hash. Este enfoque permite omitir los datos eliminados durante las búsquedas preservando al mismo tiempo la integridad de la tabla hash.

Todo el código

#pragma once
#include<iostream>
using namespace std;
enum State
{
    
    
	EMPTY,
	EXIST,
	DELETE
};

template<class K, class V>
struct HashData
{
    
    
	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>//对字符型特化给定值乘以固定质数131降低冲突
{
    
    
	size_t operator()(const string& key)
	{
    
    
		size_t val = 0;
		for (auto ch : key)
		{
    
    
			val *= 131;
			val += ch;
		}

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

		// 负载因子到了就扩容
		if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
		{
    
    
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V, Hash> newHT;
			newHT._tables.resize(newSize);
			// 旧表的数据映射到新表
			for (auto e : _tables)
			{
    
    
				if (e._state == EXIST)
				{
    
    
					newHT.Insert(e._kv);
				}
			}

			_tables.swap(newHT._tables);
		}

		Hash hash;
		size_t hashi = hash(kv.first) % _tables.size();
		while (_tables[hashi]._state == EXIST)
		{
    
    
			hashi++;
			hashi %= _tables.size();
		}

		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_size;

		return true;
	}

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

		Hash hash;
		size_t start = hash(key) % _tables.size();
		size_t hashi = start;
		while (_tables[hashi]._state != EMPTY)
		{
    
    
			if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
			{
    
    
				return &_tables[hashi];
			}

			hashi++;
			hashi %= _tables.size();

			if (hashi == start)
			{
    
    
				break;
			}
		}

		return nullptr;
	}

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

	void Print()
	{
    
    
		for (size_t i = 0; i < _tables.size(); ++i)
		{
    
    
			if (_tables[i]._state == EXIST)
			{
    
    
				printf("[%d:%d] ", i, _tables[i]._kv.first);
			}
			else
			{
    
    
				printf("[%d:*] ", i);
			}
		}
		cout << endl;
	}

private:
	vector<HashData<K, V>> _tables;
	size_t _size = 0; // 存储多少个有效数据
};

Desventajas de la detección lineal : una vez que ocurre un conflicto hash, todos los conflictos están conectados entre sí y es fácil "apilar" datos, es decir, diferentes códigos clave ocupan posiciones vacías disponibles, por lo que es difícil encontrar la ubicación de un determinado código clave. requiere muchas comparaciones, lo que reduce la eficiencia de la búsqueda .

1.2 Detección secundaria

El defecto de la detección lineal es que los datos conflictivos se acumulan, lo que está relacionado con encontrar la siguiente posición vacía, porque la forma de encontrar posiciones vacías es encontrarlas una por una, por lo que para evitar este problema, la detección secundaria debe encontrar la siguiente posición vacía . Los métodos para posiciones vacías son: H_i = (H_0 + i^2 )% mo H_i = (H_0 - i^2)% m:. Entre ellos: i =1,2,3…, H_0está la posición obtenida al calcular el código clave del elemento mediante la función hash Hash (x), y m es el tamaño de la tabla.

Modifique la función de interpolación en el sondeo lineal :

Hash hash;
size_t start = hash(kv.first) % _tables.size();
size_t i = 0;
size_t hashi = start;
// 二次探测
while (_tables[hashi]._state == EXIST)
{
    
    
    ++i;
    hashi = start + i*i;
    hashi %= _tables.size();
}

_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;

Los pasos principales y la lógica del código :

  1. Cree un objeto de función hash hash.
  2. Calcule el valor hash de la clave dada kv.firsty use la operación de módulo % _tables.size()para obtener el índice de ranura start, indicando la posición para iniciar la búsqueda.
  3. Inicialice un número entero ipara realizar un seguimiento del número de intentos e inicialícelo hashipara startrepresentar la ranura actual que se buscará.
  4. Ingrese al bucle y utilice un sondeo secundario para encontrar ranuras disponibles. En cada iteración, incrementa iy luego calcula un nuevo índice hash hashi, start + i*icalculado por. A continuación, se utiliza una operación de módulo % _tables.size()para garantizar que el índice hash no exceda el tamaño de la tabla hash.
  5. Verifique _tables[hashi]el estado de la ranura actual. Si el estado es EXIST, significa que el espacio ya está ocupado, continúe con la siguiente iteración para probar el siguiente espacio.
  6. Si se encuentra una ranura _tables[hashi]._statevacía EXIST, significa que la ranura puede almacenar datos. Almacene el kvpar clave-valor en la ranura y marque el estado EXISTpara indicar que la ranura contiene datos válidos.
  7. Incrementa el número de elementos de datos válidos _size, lo que indica una inserción exitosa de datos.

Al utilizar el sondeo cuadrático, los elementos de datos se distribuyen de manera más uniforme y se reducen los efectos de agrupamiento observados con el sondeo lineal .

Cuando la longitud de la tabla es un número primo y el factor de carga de la tabla a no excede 0,5, se pueden insertar nuevas entradas sin que se explore ninguna posición dos veces . Por lo tanto, mientras haya posiciones medio vacías en la tabla, no habrá problema de que la mesa esté llena. Al buscar no es necesario considerar que la tabla está llena, pero al insertar se debe asegurar que el factor de carga a de la tabla no exceda de 0.5, si excede se debe considerar aumentar la capacidad . Por lo tanto, el mayor defecto del hash cerrado es la utilización relativamente baja del espacio, que también es un defecto del hash.

2. Hashing abierto

El método hash abierto también se denomina método de dirección en cadena (método de cadena abierta). Primero, se utiliza una función hash para calcular la dirección hash del conjunto de códigos clave. Los códigos clave con la misma dirección pertenecen al mismo subconjunto. Cada uno El subconjunto se llama depósito. Los elementos del depósito están vinculados a través de una lista vinculada individualmente y el nodo principal de cada lista vinculada se almacena en la tabla hash .

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Cada depósito en el hash abierto contiene elementos que tienen un conflicto de hash.

template<class K, class V>
struct HashNode
{
    
    
	pair<K, V> _kv;
	HashNode<K, V>* _next;

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

Defina una estructura HashNodepara representar los nodos en la tabla hash. Este nodo contiene los siguientes miembros:

  1. _kv: Este es un par clave-valor ( pair<K, V>) que se utiliza para almacenar datos clave-valor en el nodo.
  2. _next: Este es un puntero al siguiente nodo, que se utiliza para construir una estructura de lista vinculada y manejar conflictos hash. Si se produce un conflicto de hash, se pueden asignar varios nodos al mismo depósito de hash (ranura) y los _nextpunteros de la lista vinculada se utilizan para conectar nodos con el mismo valor de hash.

En el método de dirección en cadena, cada depósito de hash (ranura) mantiene una lista vinculada. Cuando varias claves se asignan a la misma ranura, se agregan a la lista vinculada en orden y se conectan mediante punteros _next. De esta manera, se pueden almacenar varios pares clave-valor en el mismo depósito de hash, resolviendo el problema de los conflictos de hash. Cuando necesite buscar o eliminar un par clave-valor, puede recorrer la lista vinculada para localizar el nodo específico. La implementación de este método de direcciones en cadena permite que las tablas hash administren datos de manera efectiva y mantengan un rendimiento eficiente .

template<class K, class V>
class HashTable
{
    
    
	typedef HashNode<K, V> Node;
private:
	vector<Node*> _tables;
	size_t _size = 0; // 存储有效数据个数
};

Una clase de plantilla que define una tabla hash HashTable, utilizada para almacenar datos de pares clave-valor. Los siguientes son los principales miembros y propiedades de esta clase :

  • typedef HashNode<K, V> Node;: Esta es una declaración de alias de tipo, HashNode<K, V>simplificada a Node, para mejorar la legibilidad del código.
  • private:: Este es un identificador de acceso privado, que indica que las siguientes variables miembro y métodos son miembros privados de la clase y no se puede acceder a ellos directamente desde el exterior.
  • vector<Node*> _tables;: Este es un vectorcontenedor que se utiliza para almacenar los depósitos de hash (ranuras) de la tabla hash. Cada elemento es un HashNode<K, V>puntero de tipo, que es el nodo principal de la lista vinculada. Este contenedor almacena todos los datos de la tabla hash.
  • size_t _size = 0;: Este es un contador que se utiliza para registrar la cantidad de datos válidos en la tabla hash. Durante la inserción, eliminación y otras operaciones en la tabla hash, este contador se actualizará para mantener la cantidad exacta de datos.

HashTableLa función de la clase es implementar una estructura de datos de tabla hash, admitir el almacenamiento de datos de pares clave-valor y proporcionar operaciones básicas como inserción, búsqueda y eliminación. La tabla hash utiliza el método de dirección en cadena para resolver conflictos de hash, utilizando a vectorpara almacenar depósitos de hash, y cada depósito corresponde a una lista vinculada para almacenar datos. Además, _sizese utiliza para rastrear la cantidad de datos válidos y ayudar a administrar el factor de carga y la expansión automática de la tabla hash.

función de inserción

Al igual que el hash cerrado, debemos considerar el problema de expansión al insertar, por lo que la cantidad de depósitos es segura. A medida que se continúan insertando elementos, la cantidad de elementos en cada depósito continúa aumentando. En casos extremos, puede conducir a un depósito. Hay muchos nodos en la lista vinculada, lo que afectará el rendimiento de la tabla hash, por lo que, bajo ciertas condiciones, es necesario expandir la tabla hash ¿Cómo confirmar esta condición? La mejor situación para el hash es: hay exactamente un nodo en cada depósito de hash. Cuando continúa insertando elementos, se producirá un conflicto de hash cada vez. Por lo tanto, cuando el número de elementos es exactamente igual al número de depósitos, puede dar expansión de capacidad de la tabla Hash

bool Insert(const pair<K, V>& kv)
{
    
    
	// 去重
	if (Find(kv.first))
	{
    
    
		return false;
	}

	// 负载因子到1就扩容
	if (_size == _tables.size())
	{
    
    
		size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		vector<Node*> newTables;
		newTables.resize(newSize, nullptr);
		// 旧表中节点移动映射新表
		for (size_t i = 0; i < _tables.size(); ++i)
		{
    
    
			Node* cur = _tables[i];
			while (cur)
			{
    
    
				Node* next = cur->_next;

				size_t hashi = cur->_kv.first % newTables.size();
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;

				cur = next;
			}

			_tables[i] = nullptr;
		}

		_tables.swap(newTables);
	}

	size_t hashi = kv.first % _tables.size();
	// 头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_size;

	return true;
}
  1. Primero, verifique si ya existe un nodo con la misma clave, es decir, Find(kv.first)busque kv.firstsi la clave ya existe en la tabla hash llamando a . Si ya existe, devuelve false, indicando que la inserción falló porque no se permiten claves duplicadas.
  2. A continuación, verifique el factor de carga, que es la relación entre la cantidad de datos almacenados _sizey la cantidad de depósitos de hash _tables.size(). Si el factor de carga alcanza 1 (lo que significa que cada depósito hash almacena un dato en promedio), se realiza la operación de expansión de capacidad.
  3. Si se requiere expansión, primero calcule el tamaño de la nueva tabla hash newSize. Si la tabla hash actual está vacía (es decir, _tables.size()es 0), establezca el nuevo tamaño en 10; de lo contrario, establezca el nuevo tamaño en el doble del tamaño actual. Luego, cree un nuevo vectorcontenedor newTablesy establezca su tamaño en newSize, mientras inicializa todos los elementos en nullptr.
  4. Recorra _tablescada ranura en la tabla hash actual (cada ranura corresponde a una lista vinculada) y reasigne los nodos en la lista vinculada al nuevo depósito hash. La operación específica es recorrer la lista vinculada, usar la función hash para calcular el nuevo índice de ranura de la clave de cada nodo hashi, luego insertar el nodo en el nuevo depósito hash (usando el método de inserción de cabeza) y actualizar el _nextpuntero del nodo para construir. la lista enlazada. Una vez hecho esto, configure la ranura anterior en nullptr.
  5. Finalmente, utilice swapla operación para intercambiar las tablas hash antigua y nueva, y reemplace la tabla hash antigua con la nueva tabla hash para completar la operación de expansión.
  6. Si no se requiere expansión (el factor de carga no llega a 1), calcule el valor hash de la clave hashi, determine la ranura que se insertará y luego use el método de inserción de encabezado para insertar el nuevo nodo en la lista vinculada de la ranura correspondiente. . Aumente la cantidad de datos válidos _sizey regrese true, lo que indica una inserción exitosa.

La idea central de este código es mantener el factor de carga de la tabla hash y activar una operación de expansión cuando el factor de carga es demasiado alto para mantener el rendimiento de la tabla hash . Al mismo tiempo, maneja conflictos de hash a través de listas vinculadas y admite la situación en la que se asignan varias claves al mismo depósito de hash . Cabe señalar que no se realizará ninguna operación de inserción para las claves existentes para garantizar la unicidad de las claves .

Encontrar función

Node* Find(const K& key)
{
    
    
	if (_tables.size() == 0)
	{
    
    
		return nullptr;
	}

	size_t hashi = key % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
    
    
		if (cur->_kv.first == key)
		{
    
    
			return cur;
		}

		cur = cur->_next;
	}

	return nullptr;
}
  1. Primero, verifique si la tabla hash está vacía, es decir _tables.size() == 0. Si la tabla hash está vacía, significa que no hay datos y se devuelve directamente nullptrporque no se pueden encontrar datos.
  2. Calcule el valor hash de la clave , obtenga un índice de ranura hashia través de keyla operación de módulo en la clave y determine en qué depósito hash buscar.% _tables.size()
  3. Inicialice un puntero para que apunte al nodo principal en el curdepósito de hash seleccionado , es decir, la posición inicial de la lista vinculada._tables[hashi]
  4. Ingrese al bucle y recorra los nodos en la lista vinculada. En cada iteración, verifique cursi el nodo actual está vacío. Si está vacío, significa que se ha recorrido el final de la lista vinculada y no se ha encontrado la clave coincidente, si se devuelve, significa nullptrque no se ha encontrado.
  5. Si el nodo curno está vacío, continúe verificando si la clave en el par clave-valor del nodo actual _kv.firstes igual a la clave de destino key. Si son iguales, significa que se encuentra un par clave-valor coincidente y curse devuelve un puntero al nodo actual para acceder o modificar los datos.
  6. Si el nodo actual no coincide, apuntará cural siguiente nodo, es decir cur = cur->_next, continuará buscando el siguiente nodo en la lista vinculada.
  7. Repita hasta que se encuentre un nodo coincidente o se recorra toda la lista vinculada.

función de eliminación

bool Erase(const K& key)
{
    
    
    if (_tables.size() == 0)
    {
    
    
        return false; // 哈希表为空,无法删除
    }

    size_t hashi = key % _tables.size();
    Node* cur = _tables[hashi];
    Node* prev = nullptr; // 用于记录当前节点的前一个节点

    // 遍历链表
    while (cur)
    {
    
    
        if (cur->_kv.first == key)
        {
    
    
            // 找到匹配的节点,进行删除操作
            if (prev)
            {
    
    
                prev->_next = cur->_next; // 从链表中移除当前节点
            }
            else
            {
    
    
                // 如果当前节点是链表的头节点,则更新哈希桶的头指针
                _tables[hashi] = cur->_next;
            }

            delete cur; // 释放当前节点的内存
            --_size;    // 减少有效数据个数
            return true; // 删除成功
        }

        prev = cur;
        cur = cur->_next; // 移动到下一个节点
    }

    return false; // 未找到匹配的键,删除失败
}
  1. prevSi el nodo actual no es el nodo principal de la lista vinculada, apunte el puntero del nodo anterior _nextal siguiente nodo del nodo actual, eliminando así el nodo actual de la lista vinculada.
  2. Si el nodo actual es el nodo principal de la lista vinculada, actualice directamente el puntero principal del depósito hash _tables[hashi]al siguiente nodo del nodo actual para garantizar la actualización correcta del encabezado de la lista vinculada.
  3. Libere la memoria del nodo actual y reduzca la cantidad de datos válidos _size.
  4. La devolución truesignifica que la eliminación se realizó correctamente.
  5. Si no se encuentra ninguna clave coincidente, la devolución final falseindica que la eliminación falló.

incinerador de basuras

~HashTable()
{
    
    
    for (size_t i = 0; i < _tables.size(); ++i)
    {
    
    
        Node* cur = _tables[i];
        while (cur)
        {
    
    
            Node* next = cur->_next;
            free(cur);
            cur = next;
        }
        _tables[i] = nullptr;
    }
}

Itere a través de cada ranura de la tabla hash, libere el nodo en la lista vinculada y luego configure la ranura en nullptr, asegurándose de que se libere toda la memoria asignada. La siguiente es la lógica principal del código:

  1. Utilice un forbucle para atravesar _tablesel contenedor, que _tablesalmacena todas las ranuras de la tabla hash.
  2. En cada iteración se obtiene _tables[i]el nodo principal de la lista enlazada correspondiente al slot actual Node* cur.
  3. Ingrese al whilebucle interno y recorra cada nodo en la lista vinculada. En cada iteración, el puntero del siguiente nodo se Node* nextestablece primero en el nodo inmediatamente posterior al nodo actual.
  4. Utilice la función para liberar la memoria ocupada por freeel nodo actual . curTenga en cuenta que freela función se utiliza aquí en lugar de delete, porque curla asignación de memoria del objeto se puede mallocrealizar a través de una función o similar en lugar de new.
  5. Apunte el nodo actual cural siguiente nodo nextpara continuar recorriendo la lista vinculada.
  6. Repita hasta que no haya más nodos en la lista vinculada, es decir, curse convierta en nullptr.
  7. Después de cada iteración, la ranura actual _tables[i]se establece en nullptr, lo que garantiza que la ranura ya no contenga ningún nodo.

Todo el código

#pragma once
#include<iostream>
using namespace std;
template<class K, class V>
struct HashNode
{
    
    
	pair<K, V> _kv;
	HashNode<K, V>* _next;

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

template<class K, class V>
class HashTable
{
    
    
	typedef HashNode<K, V> Node;
public:

	~HashTable()
	{
    
    
		for (size_t i = 0; i < _tables.size(); ++i)
		{
    
    
			Node* cur = _tables[i];
			while (cur)
			{
    
    
				Node* next = cur->_next;
				free(cur);
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

	bool Insert(const pair<K, V>& kv)
	{
    
    
		// 去重
		if (Find(kv.first))
		{
    
    
			return false;
		}

		// 负载因子到1就扩容
		if (_size == _tables.size())
		{
    
    
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			vector<Node*> newTables;
			newTables.resize(newSize, nullptr);
			// 旧表中节点移动映射新表
			for (size_t i = 0; i < _tables.size(); ++i)
			{
    
    
				Node* cur = _tables[i];
				while (cur)
				{
    
    
					Node* next = cur->_next;

					size_t hashi = cur->_kv.first % newTables.size();
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}

				_tables[i] = nullptr;
			}

			_tables.swap(newTables);
		}

		size_t hashi = kv.first % _tables.size();
		// 头插
		Node* newnode = new Node(kv);
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_size;

		return true;
	}

	Node* Find(const K& key)
	{
    
    
		if (_tables.size() == 0)
		{
    
    
			return nullptr;
		}

		size_t hashi = key % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
    
    
			if (cur->_kv.first == key)
			{
    
    
				return cur;
			}

			cur = cur->_next;
		}

		return nullptr;
	}

	bool Erase(const K& key)
	{
    
    
		if (_tables.size() == 0)
		{
    
    
			return false; // 哈希表为空,无法删除
		}

		size_t hashi = key % _tables.size();
		Node* cur = _tables[hashi];
		Node* prev = nullptr; // 用于记录当前节点的前一个节点

		// 遍历链表
		while (cur)
		{
    
    
			if (cur->_kv.first == key)
			{
    
    
				// 找到匹配的节点,进行删除操作
				if (prev)
				{
    
    
					prev->_next = cur->_next; // 从链表中移除当前节点
				}
				else
				{
    
    
					// 如果当前节点是链表的头节点,则更新哈希桶的头指针
					_tables[hashi] = cur->_next;
				}

				delete cur; // 释放当前节点的内存
				--_size;    // 减少有效数据个数
				return true; // 删除成功
			}

			prev = cur;
			cur = cur->_next; // 移动到下一个节点
		}

		return false; // 未找到匹配的键,删除失败
	}

private:
	vector<Node*> _tables;
	size_t _size = 0; // 存储有效数据个数
};

Comparación de hash abierto y hash cerrado

El método de dirección en cadena necesita agregar un puntero de enlace para manejar el desbordamiento, lo que parece aumentar la sobrecarga de almacenamiento. De hecho: dado que el método de dirección abierta debe mantener una gran cantidad de espacio libre para garantizar la eficiencia de la búsqueda, por ejemplo, el método de exploración secundaria requiere un factor de carga a <= 0,7 y la entrada de la tabla ocupa un espacio mucho mayor que el puntero. por lo tanto, usar el método de dirección en cadena no ahorrará espacio de almacenamiento que el método de dirección abierta

Supongo que te gusta

Origin blog.csdn.net/kingxzq/article/details/133207145
Recomendado
Clasificación