Tabla hash
Guarda pares clave-valor en un array: una función hash convierte la clave en una posición y buscar, insertar o borrar cuesta O(1) de media. Es lo que hay detrás de HashMap y dict.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Tabla hash
Escribe las claves que se insertan y mira en qué cubeta cae cada una, las colisiones y cómo la tabla crece cuando se llena.
Paso 1
Una tabla hash de 5 cubetas vacías. La función hash convierte cada clave en un número; el resto de dividirlo entre el número de cubetas dice en cuál va.
1class TablaHash {
2 private List<String>[] cubetas = new List[5];
3 private int n = 0;
4
5 static int hash(String s) { // como String.hashCode, sin desbordarse
6 int h = 0;
7 for (char c : s.toCharArray()) h = (31 * h + c) % 1_000_003;
8 return h;
9 }
10
11 void insertar(String clave) {
12 int i = hash(clave) % cubetas.length;
13 if (cubetas[i] == null) cubetas[i] = new ArrayList<>();
14 if (cubetas[i].contains(clave)) return;
15 cubetas[i].add(clave);
16 n++;
17 if (n > 0.75 * cubetas.length) redimensionar();
18 }
19
20 private void redimensionar() {
21 List<String>[] viejas = cubetas;
22 cubetas = new List[2 * viejas.length + 1];
23 n = 0;
24 for (List<String> c : viejas)
25 if (c != null) for (String k : c) insertar(k);
26 }
27}Variables
- n
- 0
- cubetas
- 5
- carga
- 0/5 = 0,00
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Buscar en una lista es O(n); en un array ordenado, O(log n). La tabla hash baja a O(1) con un truco: en vez de buscar dónde está una clave, calcula dónde debería estar.
Una función hash convierte la clave (un texto, un número, un objeto) en un número entero; el resto de dividirlo entre el tamaño del array da la casilla, llamada cubeta. Para guardar «ana», se calcula su hash y se mete en su cubeta; para buscarla, se calcula el mismo hash y se mira solo esa cubeta.
El problema son las colisiones: dos claves distintas pueden caer en la misma cubeta (hay infinitas claves y pocas cubetas). La solución más común es el encadenamiento: cada cubeta guarda una pequeña lista con todas las claves que caen en ella. Si la función hash reparte bien, esas listas son muy cortas y todo sigue siendo O(1) de media.
Para que las listas no crezcan, la tabla vigila su factor de carga (claves / cubetas). Cuando pasa de un límite (0,75 en HashMap), crea un array más grande y vuelve a colocar todas las claves, porque con otro tamaño el resto cambia. Es caro, pero pasa tan pocas veces que insertar sigue siendo O(1) amortizado.
Cuándo usarlo
- Buscar por una clave muy rápido: un usuario por su correo, un producto por su código, una palabra en un diccionario.
- Contar apariciones (frecuencias de palabras, votos) o agrupar elementos por una característica.
- Quitar repetidos o comprobar si algo ya se ha visto (
HashSet). - Cachés: guardar resultados ya calculados por su entrada (memoización).
Cuándo no
- Si se necesitan las claves ordenadas o búsquedas por rango («entre 10 y 20»): un árbol (
TreeMap). - Si las claves son enteros pequeños y densos (0 a 100): un array normal es más simple y rápido.
Paso a paso
- Calcular el hash. La función hash convierte la clave en un entero, siempre el mismo para la misma clave (en Java,
hashCode()). - Elegir la cubeta.
hash % número de cubetas(sin signo): la posición del array donde va la clave. - Buscar en la cubeta. Se recorre la lista de esa cubeta comparando con
equals: si está, se lee o se actualiza; si no, se añade. - Crecer. Si claves / cubetas supera el factor de carga, se crea un array mayor y se reinserta todo (rehash).
El código
Contar palabras con una tabla hash propia
Siete cubetas con listas de nodos clave-valor: «el» e «y» aparecen varias veces y se cuentan en su nodo; las colisiones comparten cubeta.
1public class Main {
2 /** Una tabla hash de String a int con encadenamiento: cada cubeta es una lista de nodos. */
3 static class Tabla {
4 static class Nodo {
5 final String clave;
6 int valor;
7 Nodo siguiente;
8 Nodo(String clave, int valor, Nodo siguiente) { this.clave = clave; this.valor = valor; this.siguiente = siguiente; }
9 }
10
11 private final Nodo[] cubetas = new Nodo[7];
12
13 static int hash(String s) { // como String.hashCode, sin desbordarse
14 int h = 0;
15 for (char c : s.toCharArray()) h = (31 * h + c) % 1_000_003;
16 return h;
17 }
18
19 /** Suma uno al valor de la clave (y la crea con 1 si no estaba). */
20 void contar(String clave) {
21 int i = hash(clave) % cubetas.length;
22 for (Nodo n = cubetas[i]; n != null; n = n.siguiente) {
23 if (n.clave.equals(clave)) { n.valor++; return; } // ya estaba
24 }
25 cubetas[i] = new Nodo(clave, 1, cubetas[i]); // nueva, al principio de su cubeta
26 }
27
28 void mostrar() {
29 for (int i = 0; i < cubetas.length; i++) {
30 StringBuilder sb = new StringBuilder("cubeta " + i + ":");
31 for (Nodo n = cubetas[i]; n != null; n = n.siguiente) sb.append(" ").append(n.clave).append("=").append(n.valor);
32 System.out.println(sb);
33 }
34 }
35 }
36
37 public static void main(String[] args) {
38 Tabla t = new Tabla();
39 for (String p : "el gato y el perro y el raton".split(" ")) t.contar(p);
40 t.mostrar();
41 }
42}class Nodo:
def __init__(self, clave, valor, siguiente):
self.clave, self.valor, self.siguiente = clave, valor, siguiente
class Tabla:
"""Una tabla hash de texto a entero con encadenamiento: cada cubeta es una lista de nodos."""
def __init__(self):
self.cubetas = [None] * 7
@staticmethod
def hash(s): # como el de Java (el hash() de Python cambia en cada ejecución)
h = 0
for c in s:
h = (31 * h + ord(c)) % 1_000_003
return h
def contar(self, clave):
"""Suma uno al valor de la clave (y la crea con 1 si no estaba)."""
i = self.hash(clave) % len(self.cubetas)
n = self.cubetas[i]
while n is not None:
if n.clave == clave: # ya estaba
n.valor += 1
return
n = n.siguiente
self.cubetas[i] = Nodo(clave, 1, self.cubetas[i]) # nueva, al principio de su cubeta
def mostrar(self):
for i, n in enumerate(self.cubetas):
partes = []
while n is not None:
partes.append(f"{n.clave}={n.valor}")
n = n.siguiente
print(f"cubeta {i}:" + "".join(" " + p for p in partes))
t = Tabla()
for p in "el gato y el perro y el raton".split():
t.contar(p)
t.mostrar()/** Una tabla hash de texto a número con encadenamiento: cada cubeta es una lista de nodos. */
class Tabla {
cubetas = new Array(7).fill(null);
static hash(s) { // como String.hashCode de Java, sin desbordarse
let h = 0;
for (const c of s) h = (31 * h + c.charCodeAt(0)) % 1000003;
return h;
}
/** Suma uno al valor de la clave (y la crea con 1 si no estaba). */
contar(clave) {
const i = Tabla.hash(clave) % this.cubetas.length;
for (let n = this.cubetas[i]; n !== null; n = n.siguiente) {
if (n.clave === clave) { n.valor++; return; } // ya estaba
}
this.cubetas[i] = { clave, valor: 1, siguiente: this.cubetas[i] }; // nueva, al principio de su cubeta
}
mostrar() {
this.cubetas.forEach((primero, i) => {
let linea = `cubeta ${i}:`;
for (let n = primero; n !== null; n = n.siguiente) linea += ` ${n.clave}=${n.valor}`;
console.log(linea);
});
}
}
const t = new Tabla();
for (const p of "el gato y el perro y el raton".split(" ")) t.contar(p);
t.mostrar();using System;
using System.Text;
// Una tabla hash de string a int con encadenamiento: cada cubeta es una lista de nodos.
class Tabla {
class Nodo {
public readonly string Clave;
public int Valor;
public Nodo? Siguiente;
public Nodo(string clave, int valor, Nodo? siguiente) { Clave = clave; Valor = valor; Siguiente = siguiente; }
}
private readonly Nodo?[] cubetas = new Nodo?[7];
static int Hash(string s) { // como el de Java (GetHashCode cambia en cada ejecución)
int h = 0;
foreach (char c in s) h = (31 * h + c) % 1_000_003;
return h;
}
// Suma uno al valor de la clave (y la crea con 1 si no estaba).
public void Contar(string clave) {
int i = Hash(clave) % cubetas.Length;
for (var n = cubetas[i]; n != null; n = n.Siguiente) {
if (n.Clave == clave) { n.Valor++; return; } // ya estaba
}
cubetas[i] = new Nodo(clave, 1, cubetas[i]); // nueva, al principio de su cubeta
}
public void Mostrar() {
for (int i = 0; i < cubetas.Length; i++) {
var sb = new StringBuilder(quot;cubeta {i}:");
for (var n = cubetas[i]; n != null; n = n.Siguiente) sb.Append(quot; {n.Clave}={n.Valor}");
Console.WriteLine(sb);
}
}
}
class Program {
static void Main() {
var t = new Tabla();
foreach (var p in "el gato y el perro y el raton".Split(' ')) t.Contar(p);
t.Mostrar();
}
}<?php
/** Una tabla hash de texto a entero con encadenamiento: cada cubeta es una lista de nodos. */
class Tabla {
private array $cubetas;
public function __construct() { $this->cubetas = array_fill(0, 7, null); }
public static function hash(string $s): int { // como String.hashCode de Java, sin desbordarse
$h = 0;
foreach (str_split($s) as $c) $h = (31 * $h + ord($c)) % 1000003;
return $h;
}
/** Suma uno al valor de la clave (y la crea con 1 si no estaba). */
public function contar(string $clave): void {
$i = self::hash($clave) % count($this->cubetas);
for ($n = $this->cubetas[$i]; $n !== null; $n = $n->siguiente) {
if ($n->clave === $clave) { $n->valor++; return; } // ya estaba
}
$this->cubetas[$i] = (object) ["clave" => $clave, "valor" => 1, "siguiente" => $this->cubetas[$i]]; // nueva, al principio
}
public function mostrar(): void {
foreach ($this->cubetas as $i => $n) {
$linea = "cubeta $i:";
for (; $n !== null; $n = $n->siguiente) $linea .= " {$n->clave}={$n->valor}";
echo $linea, "\n";
}
}
}
$t = new Tabla();
foreach (explode(" ", "el gato y el perro y el raton") as $p) $t->contar($p);
$t->mostrar();Salida al ejecutarlo (la misma en los 5 lenguajes)
cubeta 0: raton=1 cubeta 1: cubeta 2: perro=1 y=2 cubeta 3: cubeta 4: cubeta 5: el=3 cubeta 6: gato=1
Claves propias: equals y hashCode
Si dos objetos son iguales según equals, tienen que dar el mismo hashCode, o un HashMap nunca los encontrará.
1// Para usar objetos propios como clave, equals y hashCode tienen que ir de la mano:
2// dos objetos iguales DEBEN dar el mismo hash, o la tabla los buscará en cubetas distintas.
3record Punto(int x, int y) { } // un record ya trae equals y hashCode basados en sus campos
4
5class Alumno {
6 private final String dni;
7 private String nombre;
8
9 @Override public boolean equals(Object o) {
10 return o instanceof Alumno a && dni.equals(a.dni);
11 }
12
13 @Override public int hashCode() {
14 return dni.hashCode(); // los mismos campos que equals
15 }
16}# En Python, __eq__ y __hash__ van de la mano: dos objetos iguales deben dar el mismo hash.
from dataclasses import dataclass
@dataclass(frozen=True) # frozen: inmutable, y ya trae __eq__ y __hash__ con sus campos
class Punto:
x: int
y: int
class Alumno:
def __init__(self, dni, nombre):
self.dni, self.nombre = dni, nombre
def __eq__(self, otro):
return isinstance(otro, Alumno) and self.dni == otro.dni
def __hash__(self):
return hash(self.dni) # los mismos campos que __eq__// En JavaScript, Map y Set comparan los objetos por referencia: dos objetos con los mismos datos
// son claves distintas. Para usar «el mismo punto» como clave hay que construir una clave primitiva.
const visitados = new Set();
const clave = (p) => `${p.x},${p.y}`; // dos puntos iguales dan el mismo texto
visitados.add(clave({ x: 1, y: 2 }));
visitados.has(clave({ x: 1, y: 2 })); // true (con los objetos directamente sería false)// En C#, Equals y GetHashCode van de la mano: dos objetos iguales deben dar el mismo hash.
record Punto(int X, int Y); // un record ya trae Equals y GetHashCode con sus campos
class Alumno {
private readonly string dni;
public string Nombre { get; set; }
public Alumno(string dni, string nombre) { this.dni = dni; Nombre = nombre; }
public override bool Equals(object? o) => o is Alumno a && dni == a.dni;
public override int GetHashCode() => dni.GetHashCode(); // los mismos campos que Equals
}// En PHP, las claves de un array solo pueden ser enteros o textos: para usar un objeto como
// clave se construye un texto que lo identifique (o se usa SplObjectStorage, por identidad).
final class Punto {
public function __construct(public readonly int $x, public readonly int $y) { }
public function clave(): string { return "{$this->x},{$this->y}"; } // dos puntos iguales, la misma clave
}
$visitados = [];
$visitados[(new Punto(1, 2))->clave()] = true;
isset($visitados[(new Punto(1, 2))->clave()]); // trueTraza: insertar claves en 5 cubetas
| Clave | hash | Cubeta | ¿Colisión? |
|---|---|---|---|
| ana | 96724 | 96724 % 5 = 4 | cubeta vacía |
| luis | 333226 | 333226 % 5 = 1 | cubeta vacía |
| eva | 100816 | 100816 % 5 = 1 | colisión con luis |
| pablo | 421398 | 421398 % 5 = 3 | cubeta vacía |
| marta | 666454 | 666454 % 5 = 4 | colisión con ana |
hash(s) = (31·h + código de cada letra) % 1.000.003, la misma idea que String.hashCode de Java. Con 5 claves en 5 cubetas ya hay colisiones: por eso la tabla crece antes de llenarse.
Complejidad
| Operación | Media | Peor caso |
|---|---|---|
| Buscar | O(1) | O(n) |
| Insertar | O(1) amortizado | O(n) |
| Borrar | O(1) | O(n) |
| Recorrer todo | O(n + m) | O(n + m) |
El peor caso es que todas las claves caigan en la misma cubeta (una función hash mala o un atacante que elige claves con el mismo hash). Desde Java 8, las cubetas muy largas se convierten en árboles y el peor caso baja a O(log n).
- Mejor caso: O(1)
- Caso medio: O(1)
- Peor caso: O(n)
Buscar, insertar y borrar: O(1) de media; O(n) si todas las claves caen en la misma cubeta (una función hash mala). Las curvas grises son las demás clases, para comparar.
En la práctica
HashMap,HashSetyLinkedHashMapen Java;dictyseten Python;Map,Sety los objetos en JavaScript;Dictionaryen C#; los arrays asociativos de PHP.- Los índices hash de las bases de datos y las cachés como Redis o Memcached.
- Las contraseñas no se guardan, se guarda su hash (con funciones criptográficas lentas como bcrypt o PBKDF2, no con
hashCode). - Git identifica cada fichero y cada commit por un hash de su contenido.
Errores típicos
- Redefinir
equalssin redefinirhashCode: dos objetos «iguales» caen en cubetas distintas y el mapa no los encuentra. - Usar como clave un objeto mutable y cambiarle un campo después de meterlo: su hash cambia y queda perdido en la cubeta antigua.
- Calcular la cubeta con
hash % mcuando el hash puede ser negativo: sale un índice negativo. Hay que usarMath.floorModo quitar el signo. - Esperar que
HashMaprecorra las claves en el orden en que se metieron: no hay orden. Para eso estáLinkedHashMap. - Confundir una tabla hash con una función hash criptográfica:
hashCodeno sirve para guardar contraseñas.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Tu propia tabla hash
La primera línea es el número de cubetas m; después vienen palabras. Mételas en una tabla hash con encadenamiento hecha con una lista de listas: cada palabra va al final de la lista de su cubeta, salvo que ya estuviera. Muestra cómo queda cada cubeta, cuántas hay ocupadas y cuántas colisiones ha habido (una colisión es meter una palabra nueva en una cubeta que ya tenía alguna). La función hash ya está escrita: completa cubeta e insertar.
- Entrada:
5y luego palabras separadas por espacios o saltos de línea (se pasan a minúsculas). - Salida: una línea por cubeta,
2: luis, evao3: -, y al finalCubetas ocupadas: O de M · colisiones: C. - Errores:
Número de cubetas no válido: «…»(de 2 a 99) yClave no válida: «…»(solo letras, hasta 20).
Ejemplo
5 ana luis eva pablo marta sara
0: - 1: luis, eva 2: - 3: pablo 4: ana, marta, sara Cubetas ocupadas: 3 de 5 · colisiones: 3