Tablas (Diccionarios). Modelo matemático

Documentos relacionados
Algoritmos y Estructuras de Datos Curso 04/05. Ejercicios

Algoritmos y Estructuras de Datos Ingeniería en Informática

Tema 05: Tablas hash. M. en C. Edgardo Adrián Franco Martínez edgardoadrianfrancom

Hashing (Funciones de Dispersión)

Tablas de dispersión (hash tables)

FICHEROS Y BASES DE DATOS (E44) 3º INGENIERÍA EN INFORMÁTICA. Tema 4. Técnicas de Dispersión. Definición y Manejo.

Estructura de datos y Algoritmos. Tema III Clasificación en memoria secundaria

Programación TADs Colecciones Conjuntos, Diccionarios y Tablas

Árboles balanceados (AVL) Tablas de dispersión (Hash) Colas de prioridad (Heap)

Capítulo 3. Clasificación en Memoria Secundaria

El TAD tabla y las tablas dispersas (o tablas hash) Lección 19

Programación. Tema 8: Tablas Hash. Apuntes elaborados por: Eduardo Quevedo, Aaron Asencio y Raquel López Revisado por: Javier Miranda el????

Estructuras de datos: Tablas de dispersión

Indexación y Asociación

Esquema básico de una Tabla de Dispersión

Análisis y Diseño de Algoritmos Tablas de Hash

Algoritmos y Estructuras de Datos I Ejercicios. Tema 3. Árboles

Alonso Ramirez Manzanares Computación y Algoritmos 03.05

TEMA 3 TRANSFORMACIÓN DE CLAVES (HASHING)

Tabla de Símbolos. Programación II Margarita Álvarez

1. Características de la organización direccionada 2. Tipos de organización direccionada 3. Funciones de Transformación 4. Gestión de desbordamientos

Tablas Asociativas (Hash) Tablas: filas & columnas de información Especificación algebraica

Tabla de Símbolos. Programación II Margarita Álvarez

Archivos Indice. Indexación y. Asociación. Conceptos Básicos Indices Ordenados Arboles. Asociación. Docente: Albert A.

Tablas de Dispersión

El nivel Interno. Índice Tema 3

Eduardo Mosqueira Rey Bertha Guijarro Berdiñas Mariano Cabrero Canosa

FICHEROS Y BASES DE DATOS (E44) 3º INGENIERÍA EN INFORMÁTICA. Tema 3. Estructuras de Almacenamiento. Básicas. Definición y Manejo.

Estructuras de Datos y Algoritmos Tema 3: Arrays y listas enlazadas

El TAD Árbol. El TAD Árbol

Segundo Parcial de Programación 2 7 de junio de 2017

TEMA 2 Estructuras de datos lineales

Examen de Estructuras de Datos y Algoritmos. (Modelo 2)

Examen de Estructuras de Datos y Algoritmos. (Modelo 1)

Tablas de Dispersión (Hashing Tables)

ESTRUCTURAS DE DATOS Y ALGORITMOS

Lógica: Algoritmo: Archivo: Base de datos: Bit:

Estructura de datos y de la información Boletín de problemas - Tema 10

Diseño de Conjuntos y Diccionarios con Hashing

Tema 2. Conjuntos y Diccionarios

Francisco J. Hernández López

ESTRUCTURA DE DATOS Y ALGORITMOS Titulación: Ingeniero Técnico en Informática de Gestión Curso: 2º Nombre y apellidos: Nota:

2. Desarrolla una especificación informal genérica para el TAD árbol binario. Incluir operaciones para crear y modificar el árbol.

Bases de Datos Multimedia

Es común al crear estructuras de datos y luego trabajar sobre las mismas, tener la necesidad de realizar búsquedas en forma más frecuente que la

Procesadores de lenguaje Tema 6 La tabla de símbolos

Algorítmica y Lenguajes de Programación. Eficiencia y notación asintótica (i)

ÍNDICE 1. Índice Listas ordenadas Operaciones permitidas Pilas Operaciones permitidas...

Este método de diseño de algoritmos en etapas, yendo de los conceptos generales a los de detalle, se conoce como método descendente (top-down).

Estructuras Dinámicas de datos.

Procesadores de lenguaje Tema 6 La tabla de símbolos

ESTRUCTURA DE DATOS Y ALGORITMOS Titulación: Ingeniero Técnico en Informática de Gestión Curso: 2º Nombre y apellidos: Nota:

TEMA 0 REPASO DE CONCEPTOS BÁSICOS TEST PARA AUTOEVALUACIÓN DEL ALUMNO

OCW-V.Muto Sistemas de numeración Cap. III CAPITULO III. SISTEMAS DE NUMERACION 1. REPRESENTACION DE LA INFORMACION

Tema 6. Ordenación, búsqueda e intercalación interna

95.12 Algoritmos y Programación II Práctica 7: árboles

Estructura de Datos. Índice

GENERACIÓN DE ÍNDICES ANALÍTICOS DE DOCUMENTOS

Tema 10. Indexación y asociación

Tema 8. Listas. José M. Badía, Begoña Martínez, Antonio Morales y José M. Sanchiz

BUSQUEDA SECUENCIAL Y DIRECTA, MARCELA MARQUEZ REBECA NAVARRO FONSECA GUADALUPE RUIZ ANGULO JONATHAN ALEXIS TOPETE ESTRUCTURA Y OPERACIONES

Estructuras dinámicas lineales (i)

Tema 04: TAD Lista. M. en C. Edgardo Adrián Franco Martínez edgardoadrianfrancom

Introducción a los árboles. Lección 11

Algoritmos. Medios de expresión de un algoritmo. Diagrama de flujo

Registros Un campo: Registro:

Unidad III: Estructuras lineales

Estructuras de Datos y Algoritmos

Tema 1. Ordenación, búsqueda e intercalación interna

APUNTADORES. Un apuntador es un objeto que apunta a otro objeto. Es decir, una variable cuyo valor es la dirección de memoria de otra variable.

1. Diseñe algoritmos que permitan resolver eficientemente el problema de la mochila 0/1 para los siguientes casos:

ASIGNATURA: (TIS-106) Estructuras de Datos II DOCENTE: Ing. Freddy Melgar Algarañaz TEMA 4. Montículos binarios (heaps)

Asignatura: Estructura de la Información. Curso: 2009/2010 primer semestre

Funcionamiento de las computadoras

Tema 10. Árboles. José M. Badía, Begoña Martínez, Antonio Morales y José M. Badía

Tarea 5 Gestión de Archivos

Ingeniera de Sistemas: Luz Esperanza Espitia Tutora de Estructura de datos.

A) PREORDEN B) INORDEN C) POSTORDEN D) NIVELES

Lista Simple con Puntero al Principio y Puntero al Final

DEFINICIONES BÁSICAS DE LAS ESTRUCTURAS DE DATOS

SEMINARIO DE ESPECIFICACIONES ALGEBRAICAS

Análisis del caso promedio El plan:

Profesor: José Miguel Rubio L.

La eficiencia de los programas

Tema 05: Tablas hash. M. en C. Edgardo Adrián Franco Martínez edgardoadrianfrancom

Otras estructuras de datos

1.2.4 Listas enlazadas

Árboles n-arios de búsqueda. Lección 16

1.3 Tipos de datos elementales, operadores y comandos utilitarios

Estructura de Datos y de la Información

Algoritmos y Programación II Curso 2006

ANÁLISIS SEMÁNTICO LA TABLA DE SÍMBOLOS

Tablas Hash y árboles binarios

Tema 2. Conjuntos y Diccionarios.

Un árbol binario T se define como un conjunto finito de elementos, llamados nodos, de forma que:

Programa de teoría. AED I. Estructuras de Datos. 2. Conjuntos y diccionarios. AED II. Algorítmica. 1. Abstracciones y especificaciones

Árboles Binarios de Búsqueda. Lección 13

ARBOLES BINARIOS ORDENADOS. REPRESENTACIÓN Y OPERACIONES

Transcripción:

Tablas (Diccionarios). Modelo matemático Asumimos un conjunto de claves C y un conjunto de valores V. Matemáticamente una tabla es una aplicación t : C V. El grafo de dicha función es un conjunto de pares clave/valor {(c, v) c C, v V, t(c) = v}. Las operaciones típicas son: inserción de un par (c, v), obtención del valor asociado a una clave y borrado de un par (c, v) dada la clave c En general, el dominio de esta función t no tiene porque ser igual a C, es decir, puede haber claves que no tengan asociado un valor. Para resolver este problema técnico hay dos alternativas: pensar en t como una función parcial que no está definida para estos valores ajenos al dominio, o bien, considerar que el dominio en todo C pero que en V hay un elemento especial (indefinido) tal que t(c) = para aquellas claves c que no están en la tabla. 230 Las tablas aparecen en programación en distintos contextos: tablas de símbolos de los compiladores (y diccionarios en general): para las variables (y para las funciones) se pueden almacenar pares (nombre de la variable, valor) (también puede almacenarse información de tipo); bases de datos: la localización de los registros suele hacerse a través de tablas hash; directorios de ficheros en un sistema operativo; en general, no son adecuadas para operaciones que dependen del orden relativo de los elementos: extraer el máximo o el mínimo, o hacer un recorrido en orden. Las técnicas para implementar tablas y conjuntos tienen mucho en común. Pero para los conjuntos son interesantes operaciones de unión, intersección, diferencia, etc, que no tienen mucho sentido en tablas. La operación esencial es la consulta que es la que trataremos de mejorar, aun a costa de empeorar algo el rendimiento de las restantes. 231

Operaciones en tablas crear una tabla vacía; insertar una pareja (clave, valor); comprobar si una clave está en la tabla; consultar el valor asociado a una clave; borrar el valor asociado a una clave; comprobar si la tabla está vacía; Algunas notas sobre estas operaciones: En principio asumimos que la clave es única, es decir, dada una clave sólo puede haber un valor asociado a ella (aunque distintas claves pueden tener asociado el mismo valor). En ciertas aplicaciones puede ser conveniente admitir repeticiones, pero entonces la función t quedaría definida t : C P(V ), es decir, para una clave devuelve un conjunto de valores. 232 Asumimos que la inserción de un par (c, v) en una tabla que ya contiene otro par con esa clave reemplaza el valor antiguo con el nuevo. También puede ser interesante admitir la inserción con una clave ya existente combinando el valor antiguo con el nuevo (por ejemplo, sumando los valores para acumular un resultado). La operación de consulta será una operación parcial que producirá un error cuando no exista valor asociado a la clave de búsqueda. Por ello se incluye la operación esta que permite determinar si una clave está en el dominio de las claves de la tabla. En la práctica ambas operaciones pueden combinarse en una sola que devuelva un par (b, v): si b = true (la clave existe en la tabla) en v se devuelve el valor asociado; si b = false el valor de v carece de significado. 233

Especificación algebraica TAD TTabla<Clave::EQ, Valor::ANY> USA Bool OPERACIONES crea: -> TTabla<Clave,Valor> // gen inserta: (TTabla<Clave,Valor>,Clave,Valor) -> TTabla<Clave,Valor> // gen consulta: (TTabla<Clave,Valor>,Clave) -> Valor // obs parc borra: (TTabla<Clave,Valor>,Clave) -> TTabla<Clave,Valor> // mod esta: (TTabla<Clave,Valor>,Clave) -> Bool // obs esvacia: TTabla<Clave,Valor> -> Bool // obs ECUACIONES: pt t<-ttabla<clave,valor>, i,j<-clave; x,y<-valor // purificadoras para evitar repeticiones // reemplazamiento de un valor inserta(inserta(t,c1,v1),c2,v2)=inserta(t,c1,v2) <== si c1==c2 // conmutatividad inserta(inserta(t,c1,v1),c2,v2)=inserta(inserta(t,c2,v2), c1, v1) <== c1/=c2 234 esvacia(crea)=true esvacia(inserta(t,c,v))=false esta(crea,c)=false esta(inserta(t,c1,v),c2)= (c1==c2) esta(t,c2) consulta(inserta(t,c1,v),c2)=v <== c1==c2 consulta(inserta(t,c1,v),c2)=consulta(t,c2) <== c1/=c2 borra(crea,c)=crea // no damos error, simplemente lo dejamos como esta borra(inserta(t,c1,v),c2)=borra(t,c2) <== c1==c2 borra(inserta(t,c1,v),c2)=inserta(borra(t,c2),c1,v) <== c1/=c2 ERRORES: consulta(crea,c)=error 235

Implementación(es). En busca de la eficiencia La forma más elemental de implementar una tabla es mediante un vector v[0..n 1] de modo que la clave se asocie al propio índice del vector y el valor se almacena en la componente correspondiente de dicho vector. Pero: se necesita una función inyectiva que asocie cada clave un valor 0..N 1 además este rango 0..N 1 debe ser razonablemente pequeño Si todo esto es posible se tiene complejidad constante para todas las operaciones!!... pero no es habitual que sea posible (p.e. tabla de símbolos de un compilador). Si las claves son de un tipo ordenado puede implementarse como: listas (o secuencias) de pares ordenadas por la clave. Complejidad lineal para las operaciones que requieren búsqueda. árboles binarios de búsqueda (preferiblemente equilibrados: AVL o similar). Basta con modificar ligeramente la estructura de los nodos para almacenar pares clave-valor. Complejidad logarítmica. Normalmente es posible definir una relación de orden para las claves. 236 Por eficiencia, sería bueno poder llevar a cabo la primera implementación basada en un vector... pero no es habitual disponer de una función inyectiva que: 1. transforme cada clave en un valor del rango 0..N 1, y que además 2. dicho rango sea relativamente pequeño 3. y además que sea una función muy rápida de calcular Sin embargo, casi siempre es posible encontrar una función h que cumpla los requisitos 1), 2) y 3) si permitimos que no sea inyectiva!! La idea es trabajar con una función h que distribuya las claves en el rango 0..N 1 (h : C {0..N 1}) del modo más uniforme posible, i.e., que disperse las claves en ese rango: función hash o función de dispersión. dadas dos claves k1 y k2 la probabilidad de que h(k1) = h(k2) será bastante baja; pero la coincidencia puede ocurrir y en este caso diremos que k1 y k2 son claves sinónimas y que se ha producido una colisión. Se necesitará una política de resolución de colisiones. 237

Claves Subrango [0..N 1] Funcion Hash 0 1 2 3 N 2 N 1 La función hash transforma un domino de claves potencialmente enorme en un pequeño subrango de los enteros (va de muchos a pocos). Por ejemplo, para guardar información de los alumnos de una clase en un vector de tamaño N: podemos tomar como clave el DNI del alumno (el valor será un agregado con nombre, apellidos, dirección, etc) la función hash puede obtener el resto de la división del DNI entre N 238 En el ejemplo anterior, esta función hash hará una dispersión de claves razonablemente buena. Con ello conseguimos: esta estructura en el caso medio (bajo ciertos supuestos) se comporta esencialmente como un array, i.e., como una estructura de acceso directo con tiempos de acceso constantes!! en el peor caso, esa complejidad puede empeorar considerablemente (lineal) que estemos en uno u otro caso depende fundamentalmente de la función hash, que es la que tendremos que codificar cuidadosamente En definitiva, las tablas hash o tablas de dispersión tienen un enfoque muy pragmático (se trata de que en la práctica los accesos sean muy rápidos). Aun tenemos un tema pendiente: la resolución de colisiones. En el ejemplo que estamos trabajando es fácil ver como puede producirse una de estas colisiones: suponemos N = 100 (tamaño del vector) y las claves k1 = 30567043, k2 = 5237643. Estas claves son sinónimas y la función hash devuelve para ambas el índice 43. 239

Resolución de colisiones Supongamos una la tabla contiene el par (k1, v1) tal que h(k1) = i y queremos insertar el par (k2, v2) tal que h(k2) = i, i.e., hay una colisión de claves. Hay varias formas de resolver la situación: buscar otra posición libre en el vector para almacenar el nuevo par (hash cerrado); reubicar el nuevo par en un área de desbordamiento disjunta del vector principal. A su vez, hay varias formas de implementar esta idea. La más inmediata es hacer que las componentes del vector no almacenen los pares clave-valor, sino la entrada a una estructura secundaria (por ejemplo una lista o un árbol de búsqueda) en donde se almacenan dichos pares (hash abierto). Ambas soluciones tienen sus ventajas e inconvenientes. Nótese además que la decisión de seguir uno u otro camino determina también los algoritmos de búsqueda y borrado. 240 Hash abierto & cerrado en ambos casos es necesario almacenar el par clave-valor (no basta con el valor) porque a una misma posición del vector pueden ir a parar distintas claves mediante la función hash; también en ambos casos es necesario determinar a priori el tamaño N del vector de almacenamiento, aunque este tamaño tiene distintas consecuencias en uno y otro tipo de hash: definimos la tasa de ocupación o factor de carga de la tabla como número de pares clave-valor almacenados en la tabla tamaño del vector (N) si la tabla es cerrada la tasa de ocupación a lo sumo es 1, es decir, el número de pares almacenados está acotado por el tamaño del vector; si el hash es abierto y se utiliza una lista enlazada como estructura secundaria, la tasa de ocupación puede crecer tanto como se quiera: no se limita el número de pares almacenados. 241

desde el punto de vista de la programación la resolución de colisiones en el hash abierto es inmediata: en realidad se encarga el TAD secundario de la inserción o la búsqueda (o el borrado) la resolución de colisiones en el hash cerrado plantea dificultades adicionales como veremos. 242 Funciones de dispersión Este es el punto clave para el rendimiento de las tablas hash. Las funciones de dispersión deben: poder calcularse muy eficientemente minimizar el número de colisiones: debe hacer una distrución uniforme del dominio de las claves en el rango 0..N 1 El dominio de las claves en principio puede ser de cualquier tipo numérico o alfanumérico, pero es muy sencillo convertir o codificar cualquiera de estos tipos como un valor entero: si las claves ya son de tipo entero no hay más que hacer; si son de tipo real, se pueden truncar o redondear (incluso tomando algunos decimales si se estima que puede mejorar la uniformidad de la función hash); si son de tipo carácter se puede tomar el código ASCII asociado; si son cadenas de texto se puede tomar por ejemplo la suma de los ASCII de cada carácter 243

Una vez que se tiene una codificación numérica k IN para la clave se aplica la función hash. Hay distintos métodos para implementar una función hash: método división: h(k) = k %N. La elección de N es crítica para el rendimiento de la tabla (es buena idea tomar un número primo); método de la multiplicación... más complicado (ver Heileman). Según lo que hemos dicho una clave c se transforma en un índice i {0..N 1} en dos pasos: c codificación código IN hash índice {0..N 1} En lo que sigue, por simplicidad, asumimos que la función hash hace todo el proceso codificación+hash propiamente dicho. 244 Hash Abierto. Implementación De cara a la implementación haremos algunas consideraciones prácticas: Implementamos una función privada sobrecargada, cod, para codificar (convertir a entero) claves numéricas y alfanuméricas. La función hash invocará a esta codificación y luego calculará el resto de la división entera entre N (una función hash sencilla). El tamaño del vector de almacenamiento podrá especificarse en la constructora (en caso contrario se toma uno por defecto, p.e. 17). Esto implica que el vector de almacenamiento será un vector dinámico de punteros, i.e., un puntero a puntero!! De cada componente del vector colgará una lista enlazada (implementada ad hoc). En realidad podríamos colgar cualquiera de las estructuras de almacenamiento/búsqueda que hemos visto. 245

Hash cerrado Todos los elementos (pares clave-valor) se almacenan en el propio vector, sin estructuras adicionales. Para insertar un par (c, v) el índice h(c) que devuelve la función hash para un par (c, v) se interpreta como la entrada primaria a la tabla: la posición que le corresponde en primera instancia. si dicha posición está libre se copia el par en la misma si está ocupada se aplica una función de rehash (redispersión) para calcular una entrada secundaria y se repite el proceso: si esta libre se copia ahí; si no se vuelve a aplicar la función de rehash hasta encontrar una posición libre. En general produciremos una ruta i0 = h(c), i1, i2,..., im tal que la posición im está libre La operación de búsqueda sigue un camino similar: va probando índices (primero con hash y luego con rehash) hasta encontrar la clave o decidir que no está presente en la tabla. 246 La función de rehash debe cumplir: Rehash (redispersión) para toda clave c debe poder calcularse la secuencia de pruebas i0, i1,..., in 1 {0..N 1} esta secuencia de pruebas debe ser una permutación de (0, 1, 2,..., N 1): de este modo al insertar un elemento, si hay un hueco libre, eventualmente se encontrará. Dada un vector v[0..n 1] y una clave c, la ruta de c es un segmento inicial i0, i1,..., ik de la sucesión de pruebas i0, i1,..., in 1 {0..N 1} asociada a c de modo que: las posiciones v[i0]..v[ik 1] no están vacias y no contienen la clave c v[ik] está vacía o contiene un elemento con clave c. 247

Que hacer cuando se borra un elemento? Las rutas han de cumplir estas propiedades, i.e., no pueden dejar huecos libres (se truncaría la ruta). se podría hacer un desplazamiento de todos los elementos de la ruta posteriores al borrado a la posición anterior en la ruta muy costoso!! marcar el hueco con un valor especial: posición borrada. Incluiremos un campo adicional en cada componente del vector para indicar si esa dicha componente está: ocupada, libre o borrada. 248 Para las funciones de redispersión se busca que sean rápidas de calcular y además que las rutas sean lo más cortas posibles (en promedio). Lo más sencillo es hacer redispersión lineal: dada una clave c definimos su secuencia de pruebas como: i0 = h(c) im = (im 1 + 1) %N, para 1 m < N O de modo equivalente im = (h(c) + m) %N Pero hay un problema: supongamos N = 17; insertamos un par de clave 6 que irá a la componente 6; luego un elemento de clave 23 que va a la misma componente; luego otro de clave 40, a la misma componente... se produce un agrupamiento (primario) debido a las colisiones. Estos agrupamientos empeoran el rendimiento de la tabla. 249

Algoritmos de redispersión Según hemos visto, la redispersión lineal produce un agrupamiento de los elementos, i.e., hay solapamiento entre las secuencias de pruebas correspondientes a claves distintas. Formalmente, un método de redispersión produce agrupamiento k-ario si el número de series de pruebas distintas módulo rotaciones (permutaciones circulares) es Θ(N k 1 ). Así: la redispersión lineal produce un agrupamiento primario (1-ario) porque hay N 1 1 = N 0 = 1 posible secuencia de pruebas: hay N posibles secuencias que son en realidad rotaciones de la secuencia 0, 1, 2..., N 1. Es un mal método de redispersión En la redispersión lineal hay N posibles secuencias de pruebas (1 módulo rotaciones). Lo ideal es que un método de redispersión produzca las N! posibles secuencias de prueba, pero esto no es fácil de conseguir. 250 Hay otros métodos más sofisticados de redispersión (véase Heileman): redispersión cuadrática: soluciona el problema del agrupamiento primario, pero tiene agrupamiento secundario; redispersión doble: se utilizan dos funciones de dispersión (ver Heileman). Soluciona el agrupamiento primario y secundario. redispersión coalescente. Es un enfoque distinto, parecido a la dispersión abierta: cada componente del vector almacena un elemento y un índice a otra componente de la tabla. una variante de este sistema es dividir la tabla en zona principal y zona de excedentes. La función hash produce índices de la zona principal y cuando se produce colisión, el par se almacena en la zona de excedentes. 251

Otra posibilidad A pesar de versatilidad que pueda tener el hash abierto, el cerrado puede ser especialmente útil cuando la tabla deba mantenerse en almacenamiento secundario. Hay una alternativa muy interesante para hacer la redispersión: trabajar siempre con la misma secuencia de pruebas para la redispersión. Idea: las componentes de la tabla contienen un campo adicional: un índice 0..N 1 que indica el siguiente en la secuencia de pruebas (-1 si es el último); la secuencia de pruebas (una permutación aleatoria de los índices 0..N 1) se genera al principio: partimos de un vector v[0..n 1] tal que v[i] = i para i {0..N 1}; se hace un recorrido intercambiando la componente i con otra aleatoria; la permutación resultante será la secuencia de pruebas, que se almacena en una lista enlazada l; 252 para insertar un elemento se aplica la función hash. Si la componente resultante está vacía se inserta ahí; si no, se recorre la secuencia dada por los índices del campo siguiente hasta encontrar el elemento o llegar al final. En este caso se toma el primer índice i de l (y se quita); si la componente v[i] está libre se inserta ahí y si no, se toma el siguiente de la lista l. Además se actualiza el campo siguiente de la tabla para poder reconstruir la búsqueda. la búsqueda se hace haciendo dispersión y luego recorriendo los campos siguiente de la tabla; para borrar también se sigue la secuencia dada por el campo siguiente. Al borrar un elemento hay que actualizar dicho campo y reinsertar la posición liberada en la lista l. 253