ClubEnsayos.com - Ensayos de Calidad, Tareas y Monografias
Buscar

Tabla Hash


Enviado por   •  23 de Abril de 2013  •  4.453 Palabras (18 Páginas)  •  515 Visitas

Página 1 de 18

Tabla hash

Una tabla hash, mapa hash o tabla de dispersión 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 (teléfono y dirección, por ejemplo) almacenados a partir de una clave generada (usando el nombre o número de cuenta, por ejemplo). Funciona transformando la clave con una función hash en un hash, un número que identifica la posición (casilla o cubeta) donde la tabla hash localiza el valor deseado.

Ejemplo de tabla hash.

Las tablas hash se suelen implementar sobre vectores de una dimensión, aunque se pueden hacer implementaciones multi-dimensionales basadas en varias claves. Como en el caso de los arrays, las tablas hash proveen tiempo constante de búsqueda promedio O(1),1 sin importar el número de elementos en la tabla. Sin embargo, en casos particularmente malos el tiempo de búsqueda puede llegar a O(n), es decir, en función del número de elementos.

Comparada con otras estructuras de arrays asociadas, las tablas hash son más útiles cuando se almacenan grandes cantidades de información.

Las tablas hash almacenan la información en posiciones pseudo-aleatorias, así que el acceso ordenado a su contenido es bastante lento. Otras estructuras como árboles binarios auto-balanceables son más rápidos en promedio (tiempo de búsqueda O(log n)) pero la información está ordenada en todo momento

Funcionamiento

Las operaciones básicas implementadas en las tablas hash son:

inserción(llave, valor)

búsqueda(llave) que devuelve valor

La mayoría de las implementaciones también incluyen borrar(llave). También se pueden ofrecer funciones como iteración en la tabla, crecimiento y vaciado. Algunas tablas hash permiten almacenar múltiples valores bajo la misma clave.

Para usar una tabla hash se necesita:

Una estructura de acceso directo (normalmente un array).

Una estructura de datos con una clave

Una función resumen (hash) cuyo dominio sea el espacio de claves y su imagen (o rango) los números naturales.

[editar]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 arreglo 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 obtenido 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 unalista a cada posición de la tabla, aplicando otra función o buscando el siguiente elemento libre. Estas posibilidades han de considerarse a la hora de recuperar los datos.

[editar]Búsqueda

Para recuperar los datos, es necesario únicamente conocer la clave del elemento, a la cual se le aplica la función resumen.

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.

[editar]Prácticas recomendadas para las funciones hash

Una buena función hash es esencial para el buen rendimiento de una tabla hash. Las colisiones son generalmente resueltas por algún tipo de búsqueda lineal, así que si la función tiende a generar valores similares, las búsquedas resultantes se vuelven lentas.

En una función hash ideal, el cambio de un simple bit en la llave (incluyendo el hacer la llave más larga o más corta) debería cambiar la mitad de los bits del hash, y este cambio debería ser independiente de los cambios provocados por otros bits de la llave. Como una función hash puede ser difícil de diseñar, o computacionalmente cara de ejecución, se han invertido muchos esfuerzos en el desarrollo de estrategias para la resolución de colisiones que mitiguen el mal rendimiento del hasheo. Sin embargo, ninguna de estas estrategias es tan efectiva como el desarrollo de una buena función hash de principio.

Es deseable utilizar la misma función hash para arrays de cualquier tamaño concebible. Para esto, el índice de su ubicación en el array de la tabla hash se calcula generalmente en dos pasos:

1. Un valor hash genérico es calculado, llenando un entero natural de máquina.

2. Este valor es reducido a un índice válido en el vector encontrando su módulo con respecto al tamaño del array.

El tamaño del vector de las tablas hash es con frecuencia un número primo. Esto se hace con el objetivo de evitar la tendencia de que los hash de enteros grandes tengan divisores comunes con el tamaño de la tabla hash, lo que provocaría colisiones tras el cálculo del módulo. Sin embargo, el uso de una tabla de tamaño primo no es un sustituto a una buena función hash.

Un problema bastante común que ocurre con las funciones hash es el aglomeramiento. El aglomeramiento ocurre cuando la estructura de la función hash provoca que llaves usadas comúnmente tiendan a caer muy cerca unas de otras o incluso consecutivamente en la tabla hash. Esto puede degradar el rendimiento de manera significativa, cuando la tabla se llena usando ciertas estrategias de resolución de colisiones, como el sondeo lineal.

Cuando se depura el manejo de las colisiones en una tabla hash, suele ser útil usar una función hash que devuelva siempre un valor constante, como 1, que cause colisión en cada inserción.

Funciones Hash más usadas:

1. Hash de División:

Dado un diccionario D, se fija un número m >= |D| (m mayor o igual al tamaño del diccionario) y que sea primo no cercano a potencia de 2 o de 10. Siendo k la clave a buscar y h(k) la función hash, se tiene h(k)=k%m (Resto de la división k/m).

2. Hash de Multiplicación

Si por alguna razón, se necesita una tabla hash con tantos elementos o punteros como una potencia de 2 o de 10, será mejor usar una función hash de multiplicación, independiente del tamaño de la tabla. Se escoge un tamaño de tabla m >= |D| (m mayor o igual al tamaño del diccionario) y un cierto número irracional φ (normalmente se usa 1+5^(1/2)/2 o 1-5^(1/2)/2). De este modo se define h(k)= Suelo(m*Parte fraccionaria(k*φ))

[editar]Resolución de colisiones

Si dos llaves generan un hash apuntando al mismo índice, los registros correspondientes no pueden ser almacenados en la misma posición. En estos casos, cuando una casilla ya está ocupada, debemos encontrar otra ubicación donde almacenar el nuevo registro, y hacerlo de tal manera que podamos encontrarlo cuando se requiera.

Para dar una idea de la importancia de una buena estrategia de resolución de colisiones, considerese el siguiente resultado, derivado de la paradoja de las fechas de nacimiento. Aun cuando supongamos que el resultado de nuestra función hash genera índices aleatorios distribuidos uniformemente en todo el vector, e incluso para vectores de 1 millón de entradas, hay un 95% de posibilidades de que al menos una colisión ocurra antes de alcanzar los 2.500 registros.

Hay varias técnicas de resolución de colisiones, pero las más populares son encadenamiento y direccionamiento abierto.

[editar]Direccionamiento Cerrado, Encadenamiento separado o Hashing abierto

En la técnica más simple de encadenamiento, cada casilla en el array referencia una lista de los registros insertados que colisionan en la misma casilla. La inserción consiste en encontrar la casilla correcta y agregar al final de la lista correspondiente. El borrado consiste en buscar y quitar de la lista.

Ejemplo de encadenamiento.

La técnica de encadenamiento tiene ventajas sobre direccionamiento abierto. Primero el borrado es simple y segundo el crecimiento de la tabla puede ser pospuesto durante mucho más tiempo dado que el rendimiento disminuye mucho más lentamente incluso cuando todas las casillas ya están ocupadas. De hecho, muchas tablas hash encadenadas pueden no requerir crecimiento nunca, dado que la degradación de rendimiento es lineal en la medida que se va llenando la tabla. Por ejemplo, una tabla hash encadenada con dos veces el número de elementos recomendados, será dos veces más lenta en promedio que la misma tabla a su capacidad recomendada.

Las tablas hash encadenadas heredan las desventajas de las listas ligadas. Cuando se almacenan cantidades de información pequeñas, el gasto extra de las listas ligadas puede ser significativo. También los viajes a través de las listas tienen un rendimiento de caché muy pobre.

Otras estructuras de datos pueden ser utilizadas para el encadenamiento en lugar de las listas ligadas. Al usar árboles auto-balanceables, por ejemplo, el tiempo teórico del peor de los casos disminuye de O(n) a O(log n). Sin embargo, dado que se supone que cada lista debe ser pequeña, esta estrategia es normalmente ineficiente a menos que la tabla hash sea diseñada para correr a máxima capacidad o existan índices de colisión particularmente grandes. También se pueden utilizar vectores dinámicos para disminuir el espacio extra requerido y mejorar el rendimiento del caché cuando los registros son pequeños.

[editar]Direccionamiento abierto o Hashing cerrado

Las tablas hash de direccionamiento abierto pueden almacenar los registros directamente en el array. Las colisiones se resuelven mediante unsondeo del array, en el que se buscan diferentes localidades del array (secuencia de sondeo) hasta que el registro es encontrado o se llega a una casilla vacía, indicando que no existe esa llave en la tabla.

Ejemplo de direccionamiento abierto.

Las secuencias de sondeo más socorridas incluyen:

sondeo lineal

en el que el intervalo entre cada intento es constante (frecuentemente 1).

sondeo cuadrático

en el que el intervalo entre los intentos aumenta linealmente (por lo que los índices son descritos por una función cuadrática), y

doble hasheo

en el que el intervalo entre intentos es constante para cada registro pero es calculado por otra función hash.

El sondeo lineal ofrece el mejor rendimiento del caché, pero es más sensible al aglomeramiento, en tanto que el doble hasheo tiene pobre rendimiento en el caché pero elimina el problema de aglomeramiento. El sondeo cuadrático se sitúa en medio. El doble hasheo también puede requerir más cálculos que las otras formas de sondeo.

Una influencia crítica en el rendimiento de una tabla hash de direccionamiento abierto es el porcentaje de casillas usadas en el array. Conforme el array se acerca al 100% de su capacidad, el número de saltos requeridos por el sondeo puede aumentar considerablemente. Una vez que se llena la tabla, los algoritmos de sondeo pueden incluso caer en un círculo sin fin. Incluso utilizando buenas funciones hash, el límite aceptable de capacidad es normalmente 80%. Con funciones hash pobremente diseñadas el rendimiento puede degradarse incluso con poca información, al provocar aglomeramiento significativo. No se sabe a ciencia cierta qué provoca que las funciones hash generen aglomeramiento, y es muy fácil escribir una función hash que, sin querer, provoque un nivel muy elevado de aglomeramiento.

[editar]Ventajas e inconvenientes de las tablas hash

Una tabla hash tiene como principal ventaja que el acceso a los datos suele ser muy rápido si se cumplen las siguientes condiciones:

Una razón de ocupación no muy elevada (a partir del 75% de ocupación se producen demasiadas colisiones y la tabla se vuelve ineficiente).

Una función resumen que distribuya uniformemente las claves. Si la función está mal diseñada, se producirán muchas colisiones.

Los inconvenientes de las tablas hash son:

Necesidad de ampliar el espacio de la tabla si el volumen de datos almacenados crece. Se trata de una operación costosa.

Dificultad para recorrer todos los elementos. Se suelen emplear listas para procesar la totalidad de los elementos.

Desaprovechamiento de la memoria. Si se reserva espacio para todos los posibles elementos, se consume más memoria de la necesaria; se suele resolver reservando espacio únicamente para punteros a los elementos.

[editar]Implementación en pseudocódigo

El pseudocódigo que sigue es una implementación de una tabla hash de direccionamiento abierto con sondeo lineal para resolución de colisiones y progresión sencilla, una solución común que funciona correctamente si la función hash es apropiada.

registro par { llave, valor }

var vector de pares casilla[0..numcasillas-1]

function buscacasilla(llave) {

i := hash(llave) módulo de numcasillas

loop {

if casilla[i] esta libre or casilla[i].llave = llave

return i

i := (i + 1) módulo de numcasillas

}

}

function busqueda(llave)

i := buscacasilla(llave)

if casilla[i] está ocupada // llave está en la tabla

return casilla[i].valor

else // llave no está en la tabla

return no encontrada

function asignar(llave, valor) {

i := buscacasilla(llave)

if casilla[i] está ocupada

casilla[i].valor := valor

else {

if tabla casi llena {

hacer tabla más grande (nota 1)

i := buscacasilla(llave)

}

casilla[i].llave := llave

casilla[i].valor := valor

}

}

[editar]Implementación en Java

En este método no se requiere que los elementos estén ordenados.

El método consiste en asignar el índice a cada elemento mediante una transformación del elemento, esto se hace mediante una función de conversión llamada función hash. Hay diferentes funciones para transformar el elemento y el número obtenido es el índice del elemento.

La principal forma de transformar el elemento es asignarlo directamente, es decir al 0 le corresponde el índice 0, al 1 el 1, y así sucesivamente pero cuando los elementos son muy grandes se desperdicia mucho espacio ya que necesitamos arreglo grandes para almacenarlos y estos quedan con muchos espacios libres, para utilizar mejor el espacio se utilizan funciones más complejas.

La función de hash ideal debería ser biyectiva, esto es, que a cada elemento le corresponda un índice, y que a cada índice le corresponda un elemento, pero no siempre es fácil encontrar esa función, e incluso a veces es inútil, ya que puedes no saber el número de elementos a almacenar. La función de hash depende de cada problema y de cada finalidad, y se pueden utilizar con números o cadenas, pero las más utilizadas son:

[editar]Restas Sucesivas

Esta función se emplea con claves numéricas entre las que existen huecos de tamaño conocido, obteniéndose direcciones consecutivas. Un ejemplo serían los alumnos de ingeniería en sistemas que entraron en el año 2005 sus números de control son consecutivos y está definido el número de alumnos.

05210800 -05210800 → 0

05210801 -05210800 → 1

05210802 -05210800 → 2

05210899 -05210800 → 99

[editar]Aritmética Modular

El índice de un número es resto de la división de ese número entre un número N prefijado, preferentemente primo. Los números se guardarán en las direcciones de memoria de 0 a N-1. Este método tiene el problema de que dos o más elementos pueden producir el mismo residuo y un índice puede ser señalado por varios elementos. A este fenómeno se le llama colisión. Si el número N es el 7, los números siguientes quedan transformados en:

1679 → 6

4567 → 3

8471 → 1

0435 → 1

5033 → 0

Mientras más grande sea número de elementos es mejor escoger un número primo mayor para seccionar el arreglo en más partes. El número elegido da el número de partes en que se secciona el arreglo, y las cada sección está compuesta por todos los elementos que arrojen el mismo residuo, y mientras más pequeñas sean las secciones la búsqueda se agilizara mas que es lo que nos interesa.

[editar]Mitad del Cuadrado

Consiste en elevar al cuadrado la clave y coger las cifras centrales. Este método también presenta problemas de colisión.

709^2=502681 → 26

456^2=207936 → 79

105^2=011025 → 10

879^2=772641 → 26

619^2=383161 → 31

Nota: en caso de que la cifra resultante sea impar se toma el valor número y el anterior.

[editar]Truncamiento

Consiste en ignorar parte del número y utilizar los elementos restantes como índice. También se produce colisión. Por ejemplo, si un número de 7 cifras se debe ordenar en un arreglo de elementos, se pueden tomar el segundo, el cuarto y el sexto para formar un nuevo número:

5700931 → 703

3498610 → 481

0056241 → 064

9134720 → 142

5174829 → 142

[editar]Plegamiento

Consiste en dividir el número en diferentes partes, y operar con ellas (normalmente con suma o multiplicación). También se produce colisión. Por ejemplo, si dividimos el número de 7 cifras en 2, 2 y 3 cifras y se suman, dará otro número de tres cifras (y si no, se toman las tres últimas cifras):

5700931 »> 57 + 00 + 931 = 988

3498610 → 34 + 98 + 610 = 742

0056241 → 00 + 56 + 241 = 297

9134720 → 91 + 34 + 720 = 845

5174929 → 51 + 74 + 929 = 1054

Nota: Estas solo son sugerencias y que con cada problema se pude implementar una nueva función hash que incluso tu puedes inventar o formular.

[editar]Tratamiento de Colisiones

Hay diferentes maneras de solucionarlas pero lo más efectivo es en vez de crear un arreglo de número, crear un arreglo de punteros, donde cada puntero señala el principio de una lista enlazada. Así, cada elemento que llega a un determinado índice se pone en el último lugar de la lista de ese índice. El tiempo de búsqueda se reduce considerablemente, y no hace falta poner restricciones al tamaño del arreglo, ya que se pueden añadir nodos dinámicamente a la lista.

[editar]Prueba Lineal

Consiste en que una vez detectada la colisión se debe recorrer el arreglo secuencialmente a partir del punto de colisión, buscando al elemento. El proceso de búsqueda concluye cuando el elemento es hallado, o bien cuando se encuentra una posición vacía. Se trata al arreglo como a una estructura circular: el siguiente elemento después del último es el primero. La función de rehashing es, por tanto, de la forma: R(H(X)) = (H(X) + 1) % m (siendo m el tamaño del arreglo) Ejemplo: Si la posición 397 ya estaba ocupada, el registro con clave 0596397 es colocado en la posición 398, la cual se encuentra disponible. Una vez que el registro ha sido insertado en esta posición, otro registro que genere la posición 397 o la 398 es insertado en la posición siguiente disponible.

public class Principal {

public static void main(String[] args)

{

try

{

int m, i, n, resp;

Hash[] h;

m=Integer.parseInt(javax.swing.JOptionPane.showInputDialog("Ingrese el tamaño de la tabla"));

h=new Hash[m];

for (i = 0; i < m; i++)

{

h[i]=new Hash();

h[i].estado=0;

}

do

{

resp=Integer.parseInt(javax.swing.JOptionPane.showInputDialog("Menú Principal nn"

+ "Insertar (1)nBuscar (2)nEliminar (3)nSalir (4)"));

switch(resp)

{

case 1:

n=Integer.parseInt(javax.swing.JOptionPane.showInputDialog("Ingrese el número a ser insertado en la tabla:"));

Hash.insertaHash(h,m,n);

break;

case 2:

n=Integer.parseInt(javax.swing.JOptionPane.showInputDialog("Ingrese el número a ser buscado en la tabla:"));

i=Hash.buscaHash(h, m, n);

if(i==-1)

javax.swing.JOptionPane.showMessageDialog(null, "Número no encontrado");

else

javax.swing.JOptionPane.showMessageDialog(null, "Número encontrado");

break;

case 3:

n=Integer.parseInt(javax.swing.JOptionPane.showInputDialog("Ingrese el número a ser eliminado de la tabla:"));

i=Hash.eliminaHash(h, m, n);

if(i==-1)

javax.swing.JOptionPane.showMessageDialog(null, "Número no encontrado");

else

javax.swing.JOptionPane.showMessageDialog(null, "Número eliminado con éxito");

break;

case 4:

System.exit(0);

default:

}

}while(resp!=4);

}

catch(NumberFormatException nfe)

{

javax.swing.JOptionPane.showMessageDialog(null, "Está Saliendo del Programa");

}

catch(OutOfMemoryError ome)

{

javax.swing.JOptionPane.showMessageDialog(null, "No Hay Espacio");

}

}

}

public class Hash {

int dato;

int estado; //0 = Vacío, 1 = Eliminado, 2 = Ocupado

static int funcion(int n, int m)

{

return ((n+1)%m);

}

static void insertaHash(Hash[] h, int m, int n)

{

boolean i=false;

int j;

j=funcion(n,m);

do

{

if(h[j].estado==0||h[j].estado==1)

{

h[j].dato=n;

h[j].estado=2;

i=true;

}

else

j++;

}while(j<m&&!i);

if(i)

javax.swing.JOptionPane.showMessageDialog(null, "¡Elemento insertado con éxito!");

else

javax.swing.JOptionPane.showMessageDialog(null,"¡Tabla llena!");

}

static int buscaHash(Hash[] h, int m, int n)

{

int j;

j = funcion(n,m);

while(j<m)

{

if(h[j].estado==0)

return -1;

else

if(h[j].dato==n)

if(h[j].estado==1)

return -1;

else

return j;

else

j++;

}

return -1;

}

static int eliminaHash(Hash[] h, int m, int n)

{

int i;

i=buscaHash(h,m,n);

if(i==-1)

return -1;

else

{

h[i].estado=1;

return 1;

}

}

}

[editar]Nota

↑ La reconstrucción de la tabla requiere la creación de un array más grande y el uso posterior de la función asignar para insertar todos los elementos del viejo array en el nuevo array más grande. Es común aumentar el tamaño del array exponencialmente, por ejemplo duplicando el tamaño del array.

[editar]Enlaces externos

...

Descargar como  txt (22.4 Kb)  
Leer 17 páginas más »
txt