Programación · Estructuras de datos
Tabla hash explicada: cómo funciona, código y complejidad
Respuesta corta
Una tabla hash guarda pares clave-valor en un arreglo de cubos (buckets). Una función hash convierte cada clave en un número, y ese número módulo el tamaño del arreglo elige el cubo, así que buscar, insertar y eliminar cuestan O(1) en promedio. Cuando dos claves caen en el mismo cubo (una colisión), la tabla mantiene una pequeña lista por cubo (encadenamiento) o busca otra posición libre (direccionamiento abierto). El dict y el set de Python son tablas hash.
Qué problema resuelve una tabla hash
Imagina que guardas un millón de registros de usuarios y necesitas encontrar uno por su correo. Recorrer una lista es O(n). Mantenerla ordenada permite búsqueda binaria en O(log n), pero las inserciones se vuelven lentas. Una tabla hash encuentra el registro en O(1) promedio, sin importar cuántos registros haya, y insertar y eliminar son igual de rápidos.
Por eso las tablas hash están por todas partes: diccionarios y conjuntos en los lenguajes de programación, índices hash en bases de datos, cachés, tablas de símbolos en compiladores y deduplicación.
La idea en palabras sencillas
Imagina un guardarropa con 100 ganchos numerados. En lugar de revisar todos los ganchos para encontrar tu abrigo, el encargado calcula un número de gancho a partir de tu nombre y va directo a él. La función que calcula ese número es la función hash. Una buena función hash es rápida, siempre da el mismo número para la misma clave y reparte las claves distintas de forma uniforme entre los ganchos.
- Calcula
h = hash(key). - Elige el cubo:
index = h % capacity. - Busca la clave solo en ese cubo.
Ejemplo resuelto: colisiones con encadenamiento
Toma una tabla con 4 cubos y una función hash de juguete: la suma de las posiciones de las letras (a = 1, b = 2, ...).
| Clave | Hash (suma de letras) | Cubo (hash % 4) | Contenido del cubo tras insertar |
|---|---|---|---|
cat | 3 + 1 + 20 = 24 | 0 | cubo 0: [cat] |
dog | 4 + 15 + 7 = 26 | 2 | cubo 2: [dog] |
act | 1 + 3 + 20 = 24 | 0 | cubo 0: [cat, act] |
bee | 2 + 5 + 5 = 12 | 0 | cubo 0: [cat, act, bee] |
act y bee colisionan con cat. Ahora buscar bee implica revisar tres entradas del cubo 0. Con una función hash pobre (las sumas de letras ignoran el orden) o con muy pocos cubos, las cadenas crecen y la tabla se vuelve lenta. Las funciones hash reales mezclan todos los bits de la clave, y las tablas reales crecen cuando se llenan demasiado.
Implementación en Python con encadenamiento separado
Cada cubo es una lista de pares [key, value]. Cuando el número de elementos supera 0,75 veces el número de cubos (el factor de carga), la tabla duplica su capacidad y vuelve a insertar todo.
class HashTable:
def __init__(self, capacity=8):
self.buckets = [[] for _ in range(capacity)]
self.size = 0
def _bucket(self, key):
return self.buckets[hash(key) % len(self.buckets)]
def put(self, key, value):
bucket = self._bucket(key)
for pair in bucket:
if pair[0] == key:
pair[1] = value # update existing key
return
bucket.append([key, value])
self.size += 1
if self.size > 0.75 * len(self.buckets):
self._resize()
def get(self, key, default=None):
for k, v in self._bucket(key):
if k == key:
return v
return default
def delete(self, key):
bucket = self._bucket(key)
for i, (k, _) in enumerate(bucket):
if k == key:
bucket.pop(i)
self.size -= 1
return True
return False
def _resize(self):
old = [pair for bucket in self.buckets for pair in bucket]
self.buckets = [[] for _ in range(2 * len(self.buckets))]
self.size = 0
for k, v in old:
self.put(k, v)t = HashTable()
for i, word in enumerate(["red", "green", "blue", "cyan", "pink", "gold", "gray"]):
t.put(word, i)
t.put("red", 100)
t.delete("blue")
print(t.get("red"), t.get("blue"), t.size, len(t.buckets)) # 100 None 6 16Comparación de estrategias de colisión
| Estrategia | Cómo funciona | Ventajas | Desventajas |
|---|---|---|---|
| Encadenamiento separado | cada cubo guarda una lista (o árbol) de entradas | simple; eliminar es fácil; tolera cargas altas | memoria extra por entrada; salto entre punteros |
| Sondeo lineal | ante una colisión, prueba la siguiente posición, y luego la siguiente | amigable con la caché; compacto | se forman agrupamientos; eliminar requiere marcas de borrado (tombstones) |
| Sondeo cuadrático / doble hash | salta con un paso creciente o dado por un segundo hash | menos agrupamientos que el sondeo lineal | más complejo; aún necesita tombstones |
El dict de CPython usa direccionamiento abierto con una secuencia de sondeo perturbada y, desde Python 3.7, garantiza que la iteración siga el orden de inserción. El HashMap de Java usa encadenamiento y convierte las cadenas largas en árboles balanceados.
Complejidad temporal y espacial
| Operación | Promedio | Peor caso | Motivo |
|---|---|---|---|
| Búsqueda | O(1) | O(n) | peor caso: todas las claves colisionan en un solo cubo |
| Inserción | O(1) amortizado | O(n) | un redimensionado ocasional vuelve a insertar todos los elementos |
| Eliminación | O(1) | O(n) | igual que la búsqueda |
| Iteración ordenada (claves ordenadas) | O(n log n) | O(n log n) | una tabla hash no mantiene orden, hay que ordenar |
| Espacio | O(n) | O(n) | el arreglo de cubos se mantiene un factor constante mayor que n |
El O(1) promedio depende de una buena función hash y de un factor de carga acotado. Python aleatoriza los hashes de las cadenas en cada proceso (hash seeding) para que un atacante no pueda enviar fácilmente muchas claves que colisionen y ralenticen un servidor a O(n) por petición.
Errores comunes
- Usar un objeto mutable como clave, o definir
__eq__en una clase sin el__hash__correspondiente. Los objetos iguales deben tener hashes iguales. - Esperar un orden. Una tabla hash da búsquedas rápidas, no orden; usa una lista ordenada o un árbol balanceado (un árbol binario de búsqueda) cuando necesites consultas por rango o la clave mínima.
- Confiar en los valores de
hash()entre ejecuciones: los hashes de cadenas cambian entre procesos de Python salvo que se fijePYTHONHASHSEED. Usahashlibpara hashes estables. - Tratar una tabla hash como una primitiva de seguridad. Las tablas hash usan hashes rápidos no criptográficos; las contraseñas necesitan funciones de hash de contraseñas específicas.
Cuándo usarla y usos típicos en entrevistas
Usa una tabla hash siempre que necesites responder rápido a "¿he visto esto antes?" o "¿qué valor corresponde a esta clave?". Usa un árbol binario de búsqueda o un arreglo ordenado cuando además necesites orden, rangos o las claves más cercanas.
Problemas clásicos: two sum (guarda el índice de cada número y busca target - x), contar frecuencias (collections.Counter), agrupar anagramas por letras ordenadas, detectar duplicados y memoizar resultados en programación dinámica.
def two_sum(nums, target):
seen = {} # value -> index
for i, x in enumerate(nums):
if target - x in seen:
return seen[target - x], i
seen[x] = i
return None
print(two_sum([2, 7, 11, 15], 9)) # (0, 1)Preguntas frecuentes
¿Por qué la búsqueda en una tabla hash es O(1)?
Calcular el hash y el índice del cubo toma tiempo constante, y con una buena función hash y un factor de carga acotado cada cubo contiene solo unas pocas entradas en promedio, por lo que la búsqueda dentro del cubo también es de tiempo constante.
¿Qué es el factor de carga?
El número de elementos almacenados dividido entre el número de cubos. Cuando supera un umbral (a menudo entre 0,5 y 0,75), la tabla reserva un arreglo de cubos mayor y vuelve a insertar todos los elementos.
¿Un dict de Python es una tabla hash?
Sí. dict y set son tablas hash implementadas en C con direccionamiento abierto. Desde Python 3.7, dict también conserva el orden de inserción.
¿Cuál es la diferencia entre una tabla hash y un hash map?
En la mayoría de los contextos significan lo mismo: una estructura clave-valor basada en hashing. Algunos lenguajes usan los nombres para clases concretas, como el HashMap de Java y el antiguo Hashtable sincronizado.
¿Puede una tabla hash tener claves duplicadas?
No. Insertar una clave existente reemplaza su valor. Para guardar varios valores por clave, haz que el valor sea una lista o usa collections.defaultdict(list).