Un artículo para ayudarte a aprender qué es el hash.
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++:
- Tabla hash: una tabla hash es una estructura de datos eficiente que se utiliza para almacenar pares clave-valor. En C++,
std::unordered_map
ystd::unordered_set
son 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;
- 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);
- 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_map
similarabsl::flat_hash_set
pero con una menor sobrecarga de memoria. - 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); // 重复元素将被自动去重
- 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.
- 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.
- 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 % capacity
la capacidad es el tamaño total del espacio subyacente para almacenar elementos
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 :
- 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.
- 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 :
- 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.
- 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.
- 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 :
- 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.
- 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.
- 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.
- 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 :
- Elija un divisor adecuado (normalmente un número primo), denotado por M.
- Cálculo hash en la clave K: valor hash = K % M.
- 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 :
- 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.
- 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.
- 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.
- 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 :
- Elija una semilla inicial (o valor inicial), generalmente un número entero positivo.
- Cuadra la semilla y obtén un número entero más grande.
- Del resultado de este cuadrado, saca el número de dígitos del medio como el siguiente número pseudoaleatorio.
- 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 :
- 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.
- 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.
- 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.
- 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.
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 HashData
para representar elementos de datos en la tabla hash. Contiene dos miembros principales:
_kv
: Este es un par clave-valor (pair<K, V>
), que se utiliza para almacenar datos correspondientes al valor clave._state
: Este es un tipo de enumeraciónState
que 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 _state
permite 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 HashFunc
que se puede utilizar para cualquier tipo de clave K
. La implementación de esta función hash es muy simple: key
convierte la clave de entrada directamente en size_t
tipo y regresa.
Específicamente, los pasos de operación de esta función hash son:
- Acepta una
K
clave de tipokey
como parámetro de entrada. - Transmitir claves
key
asize_t
, es decir, asignar claves de diferentes tipos a un entero sin signo. - Devuelve el
size_t
valor 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 HashFunc
para manejar string
claves 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:
- Iterar sobre cada carácter de una cadena.
- 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.
- Repita los pasos 1 y 2 hasta haber atravesado toda la cuerda.
- 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 HashTable
para implementar la estructura de datos de la tabla hash. Esta tabla hash puede almacenar pares clave-valor, donde la clave es de tipo K
y 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:
_tables
: Este es unHashData<K, V>
vector ( ) que almacena elementos de datosvector
y 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._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
.- 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 predeterminadaHashFunc<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.8
lo 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;
}
Insert
Implementació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:
Find
Primero, 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 directamentefalse
.- 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
_size
y 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. - Si se requiere expansión, cree una nueva tabla hash
newHT
con el doble de tamaño que la tabla hash actual (o inicialícela a 10 si la tabla hash actual está vacía). Luego,_tables
asigne los datos de la tabla hash anterior a la nueva tabla hashnewHT
llamandonewHT.Insert(e._kv)
a insertar cada elemento de datos válido en la nueva tabla hash. - Una vez que se completa la asignación de datos,
swap
intercambie la tabla hash antigua_tables
y la nueva tabla hash llamando a la funciónnewHT
para convertir la nueva tabla hash en la tabla hash actual. - 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
hash
clave llamando a una función hash y luego realizando la operación de módulo .kv.first
hashi
- Utilice sondeo lineal para encontrar ranuras disponibles. Si
_tables[hashi]
el estado de la ranura actual esEXIST
, continúe buscando la siguiente ranura hasta encontrar una ranura vacía. - Una vez que se encuentra una ranura vacía, el par clave-valor se
kv
almacena en la ranura y el estado se marcaEXIST
para indicar que la ranura contiene datos válidos. Luego, incremente el número de elementos de datos válidos_size
. - Finalmente, return
true
indica 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;
}
Find
La 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:
- 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 directonullptr
indica que no se encuentra. - Crea un objeto de función hash
hash
y utiliza la función hash para calcularkey
el valor hash de la clave dada.% _tables.size()
El valor hash se utiliza para obtener el índice de ranura mediante operación de módulostart
, indicando la posición donde comienza la búsqueda. - Inicialice un índice hash y comience a buscar
hashi
desde la ranura .start
Ingrese al bucle. - En el bucle, primero verifique
_tables[hashi]
el estado de la ranura actual. Si el estado esEMPTY
, 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. - Si el estado no es así
EMPTY
, continúe verificando el estado del elemento de datos_tables[hashi]._state
. Si el estado esDELETE
, significa que los datos en la ranura actual se han eliminado y la búsqueda de la siguiente ranura continuará. - Si el estado no es
EMPTY
y no lo esDELETE
, significa que la ranura actual contiene datos válidos. Continúe comprobando si la clave del elemento de datos_tables[hashi]._kv.first
es igual a la clave de destinokey
. Si es igual, se encuentra la clave especificada y se devuelve un puntero al elemento de datos&_tables[hashi]
. - 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
hashi
y tomando módulo ._tables.size()
- 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. - Finalmente, si todo el ciclo termina sin encontrar una clave coincidente, return
nullptr
significa 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;
}
}
Erase
La 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:
- Primero, llame
Find
al método para encontrarkey
el elemento de datos correspondiente a la clave especificada. Si se encuentra un elemento de datos coincidente,Find
el método devuelve un puntero al elemento de datos y lo almacena en formatoret
. Si no se encuentra ningún elemento de datos coincidente,Find
el método devuelvenullptr
. - A continuación, compruebe
ret
si es un puntero no nulo. Siret
no está vacío, significa que se ha encontrado un elemento de datos coincidente y se puede realizar la operación de eliminación. - En la operación de eliminación, establezca el estado del elemento de datos coincidente
_state
enDELETE
, lo que indica que el elemento de datos se ha eliminado. - Al mismo tiempo, el recuento del número de elementos de datos válidos en la tabla hash disminuye
_size
para reflejar la eliminación. - Finalmente, return
true
indica una eliminación exitosa. - Si
ret
está vacío (es decir, no se encuentra ningún elemento de datos coincidente), se devuelvefalse
para indicar que la eliminación falló.
Este Erase
método implementa la eliminación de elementos de datos en la tabla hash, marcando el estado como DELETE
indicativo 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 )% m
o H_i = (H_0 - i^2)% m
:. Entre ellos: i =1,2,3…, H_0
está 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 :
- Cree un objeto de función hash
hash
. - Calcule el valor hash de la clave dada
kv.first
y use la operación de módulo% _tables.size()
para obtener el índice de ranurastart
, indicando la posición para iniciar la búsqueda. - Inicialice un número entero
i
para realizar un seguimiento del número de intentos e inicialícelohashi
parastart
representar la ranura actual que se buscará. - Ingrese al bucle y utilice un sondeo secundario para encontrar ranuras disponibles. En cada iteración, incrementa
i
y luego calcula un nuevo índice hashhashi
,start + i*i
calculado 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. - Verifique
_tables[hashi]
el estado de la ranura actual. Si el estado esEXIST
, significa que el espacio ya está ocupado, continúe con la siguiente iteración para probar el siguiente espacio. - Si se encuentra una ranura
_tables[hashi]._state
vacíaEXIST
, significa que la ranura puede almacenar datos. Almacene elkv
par clave-valor en la ranura y marque el estadoEXIST
para indicar que la ranura contiene datos válidos. - 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 .
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 HashNode
para representar los nodos en la tabla hash. Este nodo contiene los siguientes miembros:
_kv
: Este es un par clave-valor (pair<K, V>
) que se utiliza para almacenar datos clave-valor en el nodo._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_next
punteros 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 aNode
, 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 unvector
contenedor que se utiliza para almacenar los depósitos de hash (ranuras) de la tabla hash. Cada elemento es unHashNode<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.
HashTable
La 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 vector
para almacenar depósitos de hash, y cada depósito corresponde a una lista vinculada para almacenar datos. Además, _size
se 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;
}
- Primero, verifique si ya existe un nodo con la misma clave, es decir,
Find(kv.first)
busquekv.first
si la clave ya existe en la tabla hash llamando a . Si ya existe, devuelvefalse
, indicando que la inserción falló porque no se permiten claves duplicadas. - A continuación, verifique el factor de carga, que es la relación entre la cantidad de datos almacenados
_size
y 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. - 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 nuevovector
contenedornewTables
y establezca su tamaño ennewSize
, mientras inicializa todos los elementos ennullptr
. - Recorra
_tables
cada 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 nodohashi
, luego insertar el nodo en el nuevo depósito hash (usando el método de inserción de cabeza) y actualizar el_next
puntero del nodo para construir. la lista enlazada. Una vez hecho esto, configure la ranura anterior ennullptr
. - Finalmente, utilice
swap
la 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. - 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_size
y regresetrue
, 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;
}
- 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 directamentenullptr
porque no se pueden encontrar datos. - Calcule el valor hash de la clave , obtenga un índice de ranura
hashi
a través dekey
la operación de módulo en la clave y determine en qué depósito hash buscar.% _tables.size()
- Inicialice un puntero para que apunte al nodo principal en el
cur
depósito de hash seleccionado , es decir, la posición inicial de la lista vinculada._tables[hashi]
- Ingrese al bucle y recorra los nodos en la lista vinculada. En cada iteración, verifique
cur
si 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, significanullptr
que no se ha encontrado. - Si el nodo
cur
no está vacío, continúe verificando si la clave en el par clave-valor del nodo actual_kv.first
es igual a la clave de destinokey
. Si son iguales, significa que se encuentra un par clave-valor coincidente ycur
se devuelve un puntero al nodo actual para acceder o modificar los datos. - Si el nodo actual no coincide, apuntará
cur
al siguiente nodo, es decircur = cur->_next
, continuará buscando el siguiente nodo en la lista vinculada. - 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; // 未找到匹配的键,删除失败
}
prev
Si el nodo actual no es el nodo principal de la lista vinculada, apunte el puntero del nodo anterior_next
al siguiente nodo del nodo actual, eliminando así el nodo actual de la lista vinculada.- 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. - Libere la memoria del nodo actual y reduzca la cantidad de datos válidos
_size
. - La devolución
true
significa que la eliminación se realizó correctamente. - Si no se encuentra ninguna clave coincidente, la devolución final
false
indica 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:
- Utilice un
for
bucle para atravesar_tables
el contenedor, que_tables
almacena todas las ranuras de la tabla hash. - En cada iteración se obtiene
_tables[i]
el nodo principal de la lista enlazada correspondiente al slot actualNode* cur
. - Ingrese al
while
bucle interno y recorra cada nodo en la lista vinculada. En cada iteración, el puntero del siguiente nodo seNode* next
establece primero en el nodo inmediatamente posterior al nodo actual. - Utilice la función para liberar la memoria ocupada por
free
el nodo actual .cur
Tenga en cuenta quefree
la función se utiliza aquí en lugar dedelete
, porquecur
la asignación de memoria del objeto se puedemalloc
realizar a través de una función o similar en lugar denew
. - Apunte el nodo actual
cur
al siguiente nodonext
para continuar recorriendo la lista vinculada. - Repita hasta que no haya más nodos en la lista vinculada, es decir,
cur
se convierta ennullptr
. - Después de cada iteración, la ranura actual
_tables[i]
se establece ennullptr
, 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