88
8
8
Tabla de Hash
Estructura de Datos
Alexander Emanuel Turpo Luque
Roberto Carlos Pongo Condori
Resumen — Es una estructura de datos. Se basan en la asignación de una clave a cada elemento de forma fácil y rápido (par el operador) calculando su número resumen. Limitada en tamaño ya que están basadas en arreglo. Las claves son asignadas a elementos en una tabla hash usando una función hash. Donde es imposible reconstruir el valor a partir del número resumen. Evitando en menor cantidad las colisiones.
Introducción
U
na tabla Hash es un contenedor asociativo (tipo Diccionario) que permite un almacenamiento y posterior recuperación eficientes de elementos a partir de otros objetos, llamados claves. La forma ideal de realizar una búsqueda de un elemento en un contenedor seria aplicar una función matemática sobre el dato y que me devolviera directamente el lugar en el que se encuentra.
Tabla de Hash
Es una estructura de datos que asocia llaves o claves con valores. La operación principal que soporta de manera eficiente es la búsqueda: permite el acceso a los elementos almacenados a partir de una clave generada. Funciona transformando la clave con una función hash en un hash, un número que identifica la posición donde la tabla hash localiza el valor deseado.
Las tablas hash se suelen implementar sobre vectores de una dimensión, aunque se pueden hacer implementaciones multi-dimensionales basadas en varias claves.
Inserción
Para almacenar un elemento en la tabla hash se ha de convertir su clave a un número. Esto se consigue aplicando la función resumen (hash) a la clave del elemento.
El resultado de la función resumen ha de mapearse al espacio de direcciones del vector que se emplea como soporte, lo cual se consigue con la función módulo. Tras este paso se obtiene un índice válido para la tabla.
El elemento se almacena en la posición de la tabla obtenida en el paso anterior.
Si en la posición de la tabla ya había otro elemento, se ha producido una colisión. Este problema se puede solucionar asociando una lista a cada posición de la tabla, aplicando otra función o buscando el siguiente elemento libre.
Búsqueda
Para recuperar los datos, es necesario únicamente conocer la clave del elemento, a la cual se le aplica la función hash.
El valor obtenido se mapea al espacio de direcciones de la tabla.
Si el elemento existente en la posición indicada en el paso anterior tiene la misma clave que la empleada en la búsqueda, entonces es el deseado. Si la clave es distinta, se ha de buscar el elemento según la técnica empleada para resolver el problema de las colisiones al almacenar el elemento.
Funciones Hash
3.1 Hash por Multiplicación
Esta técnica trabaja multiplicando la clave k por sí misma o por una constante, usando después alguna porción de los bits del producto como una localización de la tabla hash.
Cuando la elección es multiplicar k por sí misma y quedarse con alguno de los bits centrales, el método se denomina el cuadrado medio. Este método aun siendo simple y pudiendo cumplir el criterio de que los bits elegidos para marcar la localización son función de todos los bits originales de k, tiene como principales inconvenientes el que las claves con muchos ceros se reflejarán en valores hash también con muchos ceros, y el que el tamaño de la tabla está restringido a ser una potencia de 2.
3.2 Hash por División
En este caso la función se calcula simplemente como h(k) = k mod M usando el 0 como el primer índice de la tabla hash de tamaño M.
Aunque la fórmula es aplicable a tablas de cualquier tamaño es importante elegir el valor de M con cuidado. Por ejemplo si M fuera par, todas las claves pares serían aplicadas a localizaciones pares, lo que constituiría un sesgo muy fuerte. Una regla simple para elegir M es tomarlo como un número primo. En cualquier caso existen reglas mas sofisticadas para la elección de M (ver Knuth), basadas todas en estudios teóricos de funcionamiento de los métodos congruenciales de generación de números aleatorios.
Resolución de Colisiones
El segundo aspecto importante a estudiar en el hashing es la resolución de colisiones entre sinónimos. Estudiaremos tres métodos básicos de resolución de colisiones, uno de ellos depende de la idea de mantener listas enlazadas de sinónimos, y los otros dos del cálculo de una secuencia de localizaciones en la tabla hash hasta que se encuentre que se encuentre una vacían. El análisis comparativo de los métodos se hará en base al estudio del número de localizaciones que han de examinarse hasta determinar donde situar cada nueva clave en la tabla.
Fig. 1 Ejemplo de una colisión
4.1 Encadenamiento separado o Hashing Abierto.
La manera más simple de resolver una colisión es construir, para cada localización de la tabla, una lista enlazada de registros cuyas claves caigan en esa dirección. Este método se conoce normalmente con el nombre de encadenamiento separado y obviamente la cantidad de tiempo requerido para una búsqueda dependerá de la longitud de las listas y de las posiciones relativas de las claves en ellas. Existen variantes dependiendo del mantenimiento que hagamos de las listas de sinónimo.
A veces y cuando el número de entradas a la tabla es relativamente moderado, no es conveniente dar a las entradas de la tabla hash el papel de cabeceras de listas, lo que nos conduciría a otro método de encadenamiento, conocido como encadenamiento interno. En este caso, la unión entre sinónimos está dentro de la propia tabla hash, mediante campos cursores (punteros) que son inicializados a -1 (NULL) y que irán apuntando hacia sus respectivos sinónimos.
En cualquier caso, si las listas se mantienen en orden esto puede verse como una generalización del método de búsqueda secuencial en listas. La diferencia es que en lugar de mantener una sola lista con un solo nodo cabecera se mantienen M listas con M nodos cabecera de forma que se reduce el número de comparaciones de la búsqueda secuencial en un factor de M (en media) usando espacio extra para M punteros.
4.2 Direccionamiento abierto o Hasing Cerrado
Otra posibilidad consiste en utilizar un vector en el que se pone una clave en cada una de sus casillas. En este caso nos encontramos con el problema de que en el caso de que se produzca una colisión no se pueden tener ambos elementos formando parte de una lista para esa casilla. Para solucionar ese problema se usa lo que se llama rehashing. El rehashing consiste en que una vez producida una colisión al insertar un elemento se utiliza una función adicional para determinar cuál será la casilla que le corresponde dentro de la tabla, a esta función la llamaremos función de rehashing.
A la hora de definir una función de rehashing existen múltiples posibilidades, la más simple consiste en utilizar una función que dependa del número de intentos realizados para encontrar una casilla libre en la que realizar la inserción, a este tipo de rehashing se le conoce como hashing lineal. De esta forma la función de rehashing quedaría de la siguiente forma:
Borrados y Rehashing
Cuando intentamos borrar un valor k de una tabla que ha sido generada por direccionamiento abierto, nos encontramos con un problema. Si k precede a cualquier otro valor k en una secuencia de pruebas, no podemos eliminarlo sin más, ya que si lo hiciéramos, las pruebas siguientes para k se encontrarían el "agujero" dejado por k por lo que podríamos concluir que k no está en la tabla, hecho que puede ser falso. La solución es que necesitamos mirar cada localización de la tabla hash como inmersa en uno de los tres posibles estados: vacía, ocupada o borrada, de forma que en lo que concierne a la búsqueda, una celda borrada se trata exactamente igual que una ocupada. En caso de inserciones, podemos usar la primera localización vacía o borrada que se encuentre en la secuencia de pruebas para realizar la operación. Observemos que este problema no afecta a los borrados de las listas en el encadenamiento separado. Para la implementación de la idea anterior podría pensarse en la introducción en los algoritmos de un valor etiqueta para marcar las casillas borradas, pero esto sería solo una solución parcial ya que quedaría el problema de que si los borrados son frecuentes, las búsquedas sin éxito podrían requerir O(M) pruebas para detectar que un valor no está presente.
Cuando una tabla llega a un desbordamiento o cuando su eficiencia baja demasiado debido a los borrados, el único recurso es llevarla a otra tabla de un tamaño más apropiado, no necesariamente mayor, puesto que como las localizaciones borradas no tienen que reasignarse, la nueva tabla podría ser mayor, menor o incluso del mismo tamaño que la original. Este proceso se suele denominar rehashing y es muy simple de implementar si el arca de la nueva tabla es distinta al de la primitiva, pero puede complicarse bastante si deseamos hacer un rehashing en la propia tabla.
Conclusiones
La Tabla de Hash permite que el coste medio de las operaciones insertar, buscar y eliminar sea constante. Hay que elegir correctamente la función de Hash, debe ser fácil calculable y tener una buena distribución.
ANEXOS
CODIGO
#include
#include
#include
#include
char e[25]="Vacio";
class Hash
{
private:
static const int tam=10;
struct item
{
char nom[25];
char bbd[25];
item *sgt;
};
item *TablaDeHash[tam];
public:
Hash();
int FunHash(char []);
void Insertar(char [],char []);
int Conteo(int );
void Imprimir(void);
int ImprimirItems(int);
void Busqueda(char []);
void Eliminacion(char []);
};
const int Hash::tam;
Hash::Hash()
{
//char e[25]="empty";
for(int i=0;i
{
TablaDeHash[i]=new(item);
strcpy(TablaDeHash[i]->nom,e);
strcpy(TablaDeHash[i]->bbd,e);
TablaDeHash[i]->sgt=NULL;
}
}
void main()
{
Hash Hashy;
char nom[25],bbd[25],ex[25]="exit",s;
int op;
do
{
cout<<"T A B L A D E H A S H\n";
cout<
for(int i=0;i<25;i++)
cout<
cout<
cout<<"\nFUNCIONES A REALIZAR\n";
cout<<" Insercion ..................(1)\n";
cout<<" Busqueda ...................(2)\n";
cout<<" Eliminacion ................(3)\n";
cout<<" Impresion ..................(4)\n";
cout<<" Impresion indice...........(5)\n";
cout<<" SALIR.......................(6)\n\n";
cout<<" >>Que funcion desea ejecutar? ";
cin>>op;
switch(op)
{
case 1:
clrscr();
do
{
cout<<"\n\n>> Usuario : ";cin>>nom;
cout<<"\n>> Bebida favorita : ";gets(bbd);
Hashy.Insertar(nom,bbd);
cout<<"\n\nDesea insertar mas datos?(s/n) ";
cin>>s;
clrscr();
}
while(s=='s'""s=='S');
break;
case 2:
clrscr();
do
{
cout<<"\n\n>> Usuario : ";cin>>nom;
Hashy.Busqueda(nom);
cout<<"\n\nDesea buscar otro usuario?(s/n) ";
cin>>s;
clrscr();
}
while(s=='s'""s=='S');
break;
case 3:
clrscr();
do
{
cout<<"\n\n>> Usuario : ";cin>>nom;
Hashy.Eliminacion(nom);
cout<<"\n\nDesea eliminar otro usuario?(s/n) ";
cin>>s;
clrscr();
}
while(s=='s'""s=='S');
break;
case 4:
clrscr();
Hashy.Imprimir();
getch();
clrscr();
break;
case 5:
clrscr();
int ind,i;
do
{
cout<<"\n\n>> Indice referencial : ";
cin>>ind;
i=Hashy.ImprimirItems(ind);
cout<<"\n\nDesea buscar otro indice?(s/n) ";
cin>>s;
clrscr();
}
while(s=='s'""s=='S');
break;
default:
clrscr();
cout<<"\n\nEl dato ingresado no corresponde
a ninguna funcion. Intente denuevo.";
getch();
break;
}
}
while(op!=6);
}
void Hash::Eliminacion(char nom[])
{
int ind=FunHash(nom);
item *delptr,*p1,*p2;
//case 0 - Cuando el casillero esta vacio
if(strcmp(TablaDeHash[ind]->nom,e)==0 && strcmp(TablaDeHash[ind]->bbd,e)==0 )
{
cout<<"\n>> "<
}
//case 1 - La casilla contiene solo un elemento, y ese es el buscado
else
{
if(strcmp(TablaDeHash[ind]->nom,nom)==0 && TablaDeHash[ind]->sgt==NULL)
{
strcpy(TablaDeHash[ind]->nom,e);
strcpy(TablaDeHash[ind]->bbd,e);
cout<<"\n>> "<
}
else
{
//case 2 - El elemento buscaso se localiza en el primer elemento, pero hay mas datos
if(strcmp(TablaDeHash[ind]->nom,nom)==0)
{
delptr=TablaDeHash[ind];
TablaDeHash[ind]=TablaDeHash[ind]->sgt;
delete (delptr);
cout<<"\n>> "<
}
//case 3 - Las casillas contienen items pero el elemento buscado no es el primero
else
{
p1=TablaDeHash[ind]->sgt;
p2=TablaDeHash[ind];
while(p1!=NULL&&strcmp(p1->nom,nom)!=0)
{
p2=p1;
p1=p1->sgt;
}
//case 3.1 - No se hallo el elemento
if(p1==NULL)
{
cout<<"\n>> "<
}
//case 3.2 - Se halla el elemento buscado
else
{
delptr=p1;
p1=p1->sgt;
p2->sgt=p1;
delete (delptr);
cout<<"\n>> "<
}
}
}
}
}
void Hash::Busqueda(char nom[])
{
int ind=FunHash(nom);
bool NE=false;
char bbd[25];
item *ptr=TablaDeHash[ind];
while(ptr!=NULL)
{
if(strcmp(ptr->nom,nom)==0)
{
NE=true;
strcpy(bbd,ptr->bbd);
}
ptr=ptr->sgt;
}
if(NE==true)
{
cout<<"\n>> Bebida favorita : "<
}
else
{
cout<<"\n\n>> "<
}
}
int Hash::ImprimirItems(int ind)
{
item *ptr=TablaDeHash[ind];
int i=0;
if(strcmp(ptr->nom,e)==0)
{
cout<<"\n\n>> Indice["<
}
else
{
//cout<<"Indice = "<
while(ptr!=NULL)
{
cout<<"\n\nIndice "<
cout<<">> Usuario : "<nom<
cout<<">> Bebida favorita : "<nom;
ptr=ptr->sgt;i++;
}
}
return i;
}
void Hash::Imprimir(void)
{
int num;
for(int i=0;i
{
num=Conteo(i);
cout<<"Indice = "<
cout<<">> Usuario : "<nom<
cout<<">> Bebida favorita : "<bbd<
cout<<">> #de items : "<
}
}
int Hash::Conteo(int ind)
{
int c=0;
if(strcmp(TablaDeHash[ind]->nom,e)==0)
{
return c;
}
else
{
c++;
item *ptr=TablaDeHash[ind];
while(ptr->sgt!=NULL)
{
c++;
ptr=ptr->sgt;
}
}
return c;
}
void Hash::Insertar(char nom[],char bbd[])
{
int ind=FunHash(nom);
if(strcmp(TablaDeHash[ind]->nom,e)==0)
{
strcpy(TablaDeHash[ind]->nom,nom);
strcpy(TablaDeHash[ind]->bbd,bbd);
}
else
{
item *ptr=TablaDeHash[ind];
item *n=new(item);
strcpy(n->nom,nom);
strcpy(n->bbd,bbd);
n->sgt=NULL;
while(ptr->sgt!=NULL)
{
ptr=ptr->sgt;
}
ptr->sgt=n;
}
}
int Hash::FunHash(char key[])
{
int hash=0;
int ind;
for(int i=0;i
{
hash=hash+int(key[i]);
}
ind=hash%tam;
return (ind);
}
99
9
9