tabla hash - WordPress.com
Transcripción
tabla hash - WordPress.com
INFORMATICA 5260 TABLAS HASH TABLAS HASH Una tabla Hash es un contenedor asociativo (tipo Diccionario) que permite un almacenamiento y posterior recuperación eficientes de elementos (denominados valores) a partir de otros objetos, llamados claves. 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. Las tablas hash son estructuras de datos que se utilizan para almacenar un número elevado de datos sobre los que se necesitan operaciones de búsqueda e inserción muy eficientes. Una tabla hash almacena un conjunto de pares “(clave, valor)”. La clave es única para cada elemento de la tabla y es el dato que se utiliza para buscar un determinado valor. Un diccionario es un ejemplo de estructura que se puede implementar mediante una tabla hash. Para cada par, la clave es la palabra a buscar, y el valor contiene su significado. El uso de esta estructura de datos es tan común en el desarrollo de aplicaciones que algunos lenguajes las incluyen como tipos básicos. Ejemplo de Tabla Hash Ventajas 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. Desventajas de las Tablas Hash 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. Implementación de una Tabla Hash La implementación de una tabla hash está basada en los siguientes elementos: Una tabla de un tamaño razonable para almacenar los pares (clave, valor). Una función “hash” que recibe la clave y devuelve un índice para acceder a una posición de la tabla. Un procedimiento para tratar los casos en los que la función anterior devuelve el mismo índice para dos claves distintas. Esta situación se conoce con el nombre de colisión. Las posibles implementaciones de cada uno de estos tres elementos se traducen en una infinidad de formas de implementar una tabla hash. Funcionamiento de una Tabla Hash Las operaciones básicas implementadas en las tablas hash son: inserción(llave, valor). 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 obtenido en el paso anterior. búsqueda(llave) que devuelve valor. 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 Función Hash A cada elemento se le asigna una determinada posición de la tabla por medio de la función hash. Más concretamente, una función Hash debe transformar claves(normalmente enteros o cadenas de caracteres) en enteros en un rango [0..M-1], donde M es el número de registros que podemos manejar con la memoria de que dispongamos. Como factores a tener en cuenta para la elección de la función h(k) están que minimice las colisiones y que sea relativamente rápida y fácil de calcular, aunque la situación ideal sería encontrar una función h que generara valores aleatorios uniformemente sobre el intervalo [0..M-1]. 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. Funciones Hash más usadas Hash de 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. Hash de 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. Colisiones Cuando se trabaja con tablas hash es frecuente que se produzcan colisiones. Las colisiones se producen cuando para dos elementos de información distintos, la función de dispersión les asigna la misma clave. Como se puede suponer, esta solución se debe arreglar de alguna forma. Para ello las tablas hash cuentan con una función de resolución de colisiones. Tipos de Tablas Hash según la resolución de las colisiones Existen dos tipos de tablas hash, en función de cómo resuelven las colisiones: Direccionamiento Cerrado, Encadenamiento separado o Hashing abierto Direccionamiento abierto o Hashing cerrado Direccionamiento Cerrado, Encadenamiento separado o Hashing abierto Las colisiones se resuelven insertándolas en una lista. De esa forma tendríamos como estructura un vector de listas. Al número medio de claves por lista se le llama factor de carga y habría que intentar que esté próximo a 1. 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. Ventajas y desventajas del Direccionamiento Cerrado, Encadenamiento separado o Hashing abierto 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. Visión gráfica (hashing abierto) Desde un “gran” Universo sólo un número reducido de claves serán consideradas. Universo de Claves Claves usadas 16 Función de mapeo o Función de hash Lista Enlazada Direccionamiento abierto o Hashing cerrado Utilizamos un vector como representación y cuando se produzca una colisión la resolvemos reasignándole otro valor hash a la clave hasta que encontremos un hueco. Las tablas hash de direccionamiento abierto pueden almacenar los registros directamente en el array. Las colisiones se resuelven mediante un sondeo 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. Direccionamiento abierto o Hashing cerrado (cont.) Las secuencias de sondeo más socorridas incluyen: sondeo lineal en el que el intervalo entre cada intento es constante (frecuentemente 1). Ofrece el mejor rendimiento del caché, pero es más sensible al aglomeramiento 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). Se sitúa en medio del sondeo lineal y del doble hasheo. doble hasheo en el que el intervalo entre intentos es constante para cada registro pero es calculado por otra función hash. Tiene pobre rendimiento en el caché pero elimina el problema de aglomeramiento. También puede requerir más cálculos que las otras formas de sondeo. Ventajas y desventajas del Direccionamiento abierto o Hashing cerrado La gran ventaja de hashing cerrado es que elimina totalmente los punteros usados en la lista enlazada. Se libera así espacio de memoria, el que puede ser usado en más entradas de la tabla y menor número de colisiones. 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. Visión gráfica (hashing cerrado) Desde un “gran” Universo sólo un número reducido de claves serán consideradas. Universo de Claves Claves usadas 20 Función de mapeo Función de hash La “lista” se almacena en la misma tabla Implementación de una tabla Hash en JAVA Para esto se utiliza la clase hashtable la cual forma parte del paquete útil. La clase hashtable tiene como conveniencia que permite definir los tipos de datos de las claves y valores que contiene. Por ejemplo: import java.util.Hashtable; Hashtable<String, String> capitales = new Hashtable<String, String>(); capitales.put("España","Madrid"); capitales.put("Argentina","Buenos Aires"); Añadir, eliminar y consultar elementos de una Hashtable Como hemos visto en el ejemplo del apartado anterior, para añadir elementos a la Hashtable se utiliza el método “put”. Otros métodos para el manejo y consulta de Hashtables son: remove(clave) – Eliminar un par (clave, valor) identificado por su clave get(clave) – Obtener el valor asociado a una clave containsKey(clave) – Determinar si una clave existe en la hashtable contains(valor), containsValue(valor) – Estos dos métodos son equivalentes, y devuelven true si el argumento existe en la tabla como un valor asociado a una clave. A continuación vemos un ejemplo de uso de estos métodos: // Elimina una entrada de la hashtable capitales.remove("España"); // Obtener el número de entradas en la hashtable System.out.println("Tamaño de la tabla de capitales: " + capitales.size()); // Consultar la existencia de una clave en la hashtable String pais = "Argentina"; if (capitales.containsKey(pais)) { System.out.println("La capital de " + pais + " es: " + capitales.get(pais)); } else { System.out.println("La tabla no contiene la capital de " + pais); } Recorrer las entradas de una Hashtable El método keys() devuelve un objeto de la clase Enumeration que contiene todas las claves de la tabla. Con él, podemos recorrer las entradas existentes en la misma: java.util.Enumeration claves = capitales.keys(); while( claves.hasMoreElements() ) { Object clave = claves.nextElement(); Object valor = capitales.get(clave); System.out.println("Pais: "+clave.toString()+", capital: " +valor.toString()); } Del mismo modo, existe el método elements() que devuelve un objeto de la clase Enumeration que contiene todos los valores de la tabla. También hay un método values(), que devuelve un objeto de la clase Collection con todos los valores de la tabla. Recorrer ordenadamente las entradas de una Hashtable El método keySet() devuelve un objeto de la clase Set que contiene todas las claves de la Hashtable. Para recorrer la Hashtable en orden de clave ascencente, podemos convertir este Set en un Array, y ordenarlo con el método sort(): String[] claves = (String[]) capitales.keySet().toArray(new String[0]); java.util.Arrays.sort(claves); for(String clave : claves) { System.out.println(clave + " : " + capitales.get(clave)); } Ejemplo completo de una hashtable en JAVA import java.util.*; public class Direccion { public static void main(String[] args) { Hashtable direccion = new Hashtable(); Integer ocho = new Integer(8000); direccion.put("calle","Primavera"); direccion.put("numero", ocho); direccion.put("colonia"," La Silla "); direccion.put("ciudad"," Monterrey "); direccion.put("estado"," Nuevo Leon "); direccion.put("pais","Mexico"); String miciudad = (String) direccion.get("ciudad"); String miestado = (String) direccion.get("estado"); String micalle = (String) direccion.get("calle"); Integer minumero = (Integer) direccion.get("numero"); System.out.println("Direccion : " + micalle + " " + minumero); System.out.println("Lugar: " + miciudad + "," + miestado); } } Explicación del ejemplo Esta Clase únicamente contiene su método principal (main) el cual contiene las siguientes funcionalidades: Se define una Clase Hashtable con la referencia direccion. Posteriormente se define un número Integer; este es un de los casos donde deben ser utilizados "Wrappers" para primitivos, puesto que el elemento será integrado a un contenedor ("Hashtable") en forma de Objeto es necesario convertir ("Wrap") el primitivo como Objeto. A través del método put son colocados diversos juegos de datos en el "Hashtable", el primer elemento indica el nombre ("key") del elemento, mientras el segundo indica el Objeto/Valor de este nombre ("key"); nótese los Objetos/Valores pueden ser de cualquier tipo, en este ejemplo se colocan diversos Strings y la referencia del Objeto Integer. Explicación del ejemplo (2) Mediante el método get son extraídos algunos valores del "Hashtable" mediante el correspondiente nombre ("key"). Nótese que dicha extracción requiere que sea realizado un "Casting" explicito, lo cual es llevado acabo a través de paréntesis ( ) ; esto se debe a que la extracción es llevada acabo de una Clase genérica ("Hashtable") hacia una más especifica ya sea String o Integer ("Down-Casting"). Finalmente son impresos a pantalla algunos valores extraídos del "Hashtable".