El encadenamiento resuelve colisiones almacenando, en cada bucket del array, una estructura enlazada (usualmente una lista). Cuando varias claves apuntan al mismo indice, se agregan como nodos dentro de la lista de ese bucket.
Cada bucket apunta al inicio de una lista de pares clave-valor. Insertar implica agregar un nodo en esa lista; buscar y eliminar consisten en recorrerla hasta hallar la clave.
Una lista simple por bucket mantiene el orden de insercion y simplifica altas y bajas sin reubicar otros elementos.
#include <stdlib.h>
#include <string.h>
typedef struct HashNode {
const char *clave;
int valor;
struct HashNode *sig;
} HashNode;
typedef struct {
HashNode **buckets;
unsigned long capacidad;
unsigned long cantidad;
} HashTable;
HashTable *hash_crear(unsigned long capacidad) {
HashTable *t = malloc(sizeof(HashTable));
t->capacidad = capacidad;
t->cantidad = 0;
t->buckets = calloc(capacidad, sizeof(HashNode *));
return t;
}
Cada entrada del array buckets almacena el puntero al primer nodo de la lista correspondiente. El campo cantidad facilita calcular el factor de carga.
static unsigned long hash_cadena(const char *clave, unsigned long cap) {
unsigned long h = 5381;
int c;
while ((c = *clave++) != 0) {
h = ((h << 5) + h) + (unsigned long)c; /* djb2 */
}
return h % cap;
}
int hash_insertar(HashTable *t, const char *clave, int valor) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *nodo = t->buckets[idx];
while (nodo != NULL) {
if (strcmp(nodo->clave, clave) == 0) {
nodo->valor = valor; /* actualiza */
return 1;
}
nodo = nodo->sig;
}
HashNode *nuevo = malloc(sizeof(HashNode));
nuevo->clave = clave;
nuevo->valor = valor;
nuevo->sig = t->buckets[idx]; /* insercion al frente */
t->buckets[idx] = nuevo;
t->cantidad++;
return 1;
}
Se recorre la lista del bucket buscando la clave; si ya existe se actualiza el valor, de lo contrario se agrega un nodo al inicio.
HashNode *hash_buscar(HashTable *t, const char *clave) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *nodo = t->buckets[idx];
while (nodo != NULL) {
if (strcmp(nodo->clave, clave) == 0) return nodo;
nodo = nodo->sig;
}
return NULL;
}
La busqueda queda acotada a la longitud de la lista dentro del bucket, en lugar de revisar toda la tabla.
int hash_eliminar(HashTable *t, const char *clave) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *actual = t->buckets[idx];
HashNode *anterior = NULL;
while (actual != NULL) {
if (strcmp(actual->clave, clave) == 0) {
if (anterior == NULL) {
t->buckets[idx] = actual->sig;
} else {
anterior->sig = actual->sig;
}
free(actual);
t->cantidad--;
return 1;
}
anterior = actual;
actual = actual->sig;
}
return 0; /* clave no encontrada */
}
Eliminar requiere llevar un puntero previo para ajustar el enlace cuando se quita el nodo objetivo.
La clave para sostener O(1) promedio es mantener el factor de carga bajo y una funcion hash que disperse bien.
Copia el siguiente programa en main.c, compila y ejecuta. Inserta, busca y elimina claves usando encadenamiento, y muestra el estado final de la tabla.
La clave de la tabla de hash es el nombre y su valor asociado es la edad de dicha persona.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct HashNode {
const char *clave;
int valor;
struct HashNode *sig;
} HashNode;
typedef struct {
HashNode **buckets;
unsigned long capacidad;
unsigned long cantidad;
} HashTable;
static unsigned long hash_cadena(const char *clave, unsigned long cap) {
unsigned long h = 5381;
int c;
while ((c = *clave++) != 0) {
h = ((h << 5) + h) + (unsigned long)c; /* djb2 */
}
return h % cap;
}
HashTable *hash_crear(unsigned long capacidad) {
HashTable *t = malloc(sizeof(HashTable));
t->capacidad = capacidad;
t->cantidad = 0;
t->buckets = calloc(capacidad, sizeof(HashNode *));
return t;
}
int hash_insertar(HashTable *t, const char *clave, int valor) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *nodo = t->buckets[idx];
while (nodo != NULL) {
if (strcmp(nodo->clave, clave) == 0) {
nodo->valor = valor;
return 1;
}
nodo = nodo->sig;
}
HashNode *nuevo = malloc(sizeof(HashNode));
nuevo->clave = clave;
nuevo->valor = valor;
nuevo->sig = t->buckets[idx];
t->buckets[idx] = nuevo;
t->cantidad++;
return 1;
}
HashNode *hash_buscar(HashTable *t, const char *clave) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *nodo = t->buckets[idx];
while (nodo != NULL) {
if (strcmp(nodo->clave, clave) == 0) return nodo;
nodo = nodo->sig;
}
return NULL;
}
int hash_eliminar(HashTable *t, const char *clave) {
unsigned long idx = hash_cadena(clave, t->capacidad);
HashNode *actual = t->buckets[idx];
HashNode *anterior = NULL;
while (actual != NULL) {
if (strcmp(actual->clave, clave) == 0) {
if (anterior == NULL) {
t->buckets[idx] = actual->sig;
} else {
anterior->sig = actual->sig;
}
free(actual);
t->cantidad--;
return 1;
}
anterior = actual;
actual = actual->sig;
}
return 0;
}
void hash_imprimir(HashTable *t) {
for (unsigned long i = 0; i < t->capacidad; i++) {
printf("[%lu]: ", i);
HashNode *n = t->buckets[i];
while (n != NULL) {
printf("(%s -> %d) ", n->clave, n->valor);
n = n->sig;
}
printf("\n");
}
}
void hash_destruir(HashTable *t) {
for (unsigned long i = 0; i < t->capacidad; i++) {
HashNode *n = t->buckets[i];
while (n != NULL) {
HashNode *tmp = n->sig;
free(n);
n = tmp;
}
}
free(t->buckets);
free(t);
}
int main(void) {
HashTable *tabla = hash_crear(8);
hash_insertar(tabla, "juan", 50);
hash_insertar(tabla, "ana", 23);
hash_insertar(tabla, "luis", 19);
hash_insertar(tabla, "pedro", 90);
hash_insertar(tabla, "maria", 41);
printf("Tabla final:\n");
hash_imprimir(tabla);
HashNode *resultado = hash_buscar(tabla, "pedro");
printf("Buscar 'pedro': %s\n", resultado ? "encontrado" : "no encontrado");
hash_eliminar(tabla, "pedro");
resultado = hash_buscar(tabla, "pedro");
printf("Buscar 'pedro': %s\n", resultado ? "encontrado" : "no encontrado");
hash_destruir(tabla);
return 0;
}
En CLion, crea un proyecto de consola, reemplaza main.c con el codigo anterior y ejecuta. Observa como las colisiones se guardan en listas dentro de cada bucket.
Pulsa el boton para ver como se insertan juan, ana, luis, pedro y maria usando el hash djb2 sobre 8 buckets. Cada colision se encadena al frente, igual que en el codigo en C.