Está en la página 1de 11

Tablas Hash

Algoritmos y
Estructura de
Datos I

1
Tablas hash
En las lecturas anteriores, se vieron distintas estructuras, tales como los
arreglos, las listas enlazadas, los árboles (binarios y AVL), etcétera.

En cuanto al problema de búsqueda, se mencionó, para cada uno de ellos,


su rendimiento. Así, se vio que en una estructura como un arreglo o una
lista enlazada donde sus elementos no están ordenados, una búsqueda
tiene un rendimiento bastante bajo, O(n), por lo cual se puede llegar a
necesitar transitar por todos los elementos hasta lograr encontrar el que se
busca. Luego, se vio que era posible aplicar el método de búsqueda binaria
en un arreglo que tuviera sus elementos ordenados y se descartó parte del
arreglo, sin necesidad de realizar las comparaciones en todos los
elementos para llegar a encontrar el elemento buscado. Así, se obtuvo un
rendimiento mejor, O(Log(n)). En la última lectura, se pudo abordar una
nueva estructura de datos no lineal (los árboles binarios de búsqueda),
que nos permitió realizar búsquedas descartando partes de la estructura
de a mitades insumiendo también O(Log(n)).

En la presente lectura, se abordará una nueva estructura de datos que, en


la práctica, performa extremadamente bien. Permitirá realizar búsquedas
de una manera mucho más eficiente y sencilla.

Conceptos iniciales
Se necesita recordar qué es un arreglo, un vector o array. En lecturas
anteriores, se mencionó que es un tipo de estructura de datos estática
debido a que debe indicarse su tamaño en el momento de ser creado.

Los arreglos son estructuras que permiten almacenar un conjunto de


elementos, en general del mismo tipo, de manera contigua, permitiendo que el
acceso a dichos elementos se realice a través de un índice.

Cabe aclarar que los arreglos, en general, son estructuras que almacenan
elementos del mismo tipo, pero existen lenguajes que no son fuertemente
tipados, que permiten almacenar elementos de distintos tipos dentro de
los arreglos. Asimismo, la longitud de un array, en general, no es dinámica,
sino que permanece del mismo tamaño en que se creó. Sin embargo,
también existen lenguajes que modifican dinámicamente su tamaño. En
esta lectura, se trabajará considerando que los arreglos son estáticos y del
mismo tipo.

2
No importa cuán grande sea el vector y cuántos elementos se tengan
almacenados. Se podrá acceder a un elemento a través del índice en un
tiempo constante O(1).

Los índices comienzan en 0, por lo que acceder al último elemento significa


acceder a la posición n-1.

Sea v un arreglo de n elementos,

𝑆𝑖 0 ≤ 𝑖 ≤ 𝑛, 𝑒𝑛𝑡𝑜𝑛𝑐𝑒𝑠 𝑣[𝑖 − 1] = 𝑒𝑙𝑒𝑚𝑒𝑛𝑡𝑜 𝑖

Figura 1: Representación gráfica de un vector genérico

Fuente: elaboración propia.

Los arreglos son estructuras unidimensionales; sin embargo, se pueden


encontrar estructuras bidimensionales. Estas son llamadas matrices. El
acceso a los elementos de una matriz se realiza a través del índice de fila y
de columna.

Tablas hash
Se comenzará con un ejemplo usualmente usado para explicar esta
temática. Se supone que se tiene un arreglo v, donde, en cada posición de
este, se encuentran los datos de una persona, y donde se realizó la
asignación de un elemento en el arreglo usando de índice el DNI. Así, por
ejemplo, si el DNI de una persona es 33.555.777, se podrá acceder, de
manera unívoca, a los datos de esa persona usando la expresión:

v[33.555.777] = persona buscada

3
Lo que se acaba de describir es una tabla hash.

Una tabla hash es una estructura de datos que permite almacenar elementos
en la misma y accederlos a través de relacionar una clave o key con un
elemento.

Funciones hash
Si se continúa con el ejemplo, la clave usada en este caso será el DNI de la
persona. Esto permite estimar que el vector deberá tener millones de
posiciones para que puedan almacenarse allí todas las personas.

Para resolver esta problemática, surgen las funciones de dispersión,


también llamadas funciones hash. Estas usarán la clave para calcular un
nuevo valor que será usado como índice para encontrar al elemento
buscado.

Las funciones hash son funciones deterministas propuestas por el


desarrollador que permiten calcular un índice de tamaño pequeño a través de
una clave.

Se dice que son deterministas porque, para una clave, la función dará por
resultado siempre el mismo valor.

Es común encontrar que la función hash sea calcular el resto de dividir la


clave por el tamaño de la tabla.

Colisiones

Una colisión ocurre Ahora bien, se puede encontrar con una nueva problemática: ¿qué sucede
cuando la función hash si se obtiene el mismo índice para dos claves distintas? Esta problemática
arroja el mismo valor es conocida como colisión.
para dos claves
distintas o en
presencia de claves
Una colisión podría ocurrir cuando se tienen dos claves iguales. Si se usa el
idénticas. ejemplo anterior, se podría haber usado como clave el nombre de la
persona, en vez de su DNI. En ese caso, una persona con el mismo nombre
que otra persona tendrá la misma clave y, por ende, la función hash
calculará el mismo índice y generará una colisión, también. Las colisiones
son bastante comunes, porque es más la cantidad de elementos que la
cantidad de posiciones disponibles.

4
No es posible almacenar a dos personas en una misma posición de la tabla,
por lo que debe buscarse una alternativa. Estas alternativas se conocen
como métodos de resolución de colisiones. Existen varias y,
afortunadamente, son muy efectivas.

Sondeo lineal o direccionamiento abierto


Este método también se puede encontrar en inglés con el nombre de open
addressing. Si al aplicar la función hash se obtiene un valor de índice que ya
está ocupado, se transitará de manera secuencial por las siguientes
posiciones, hasta localizar una posición vacía.

La técnica es llamada direccionamiento abierto porque, cuando una


posición está vacía, se dice que está abierta. En cuanto almacene a un
elemento, dicha posición queda cerrada. Cuando una posición está
cerrada, es cuando se comienza con el proceso de sondeo lineal hasta
encontrar una posición libre.

Inserción

Una vez que se localiza la posición libre, ya sea directamente en el índice


que indicó la función hash o en una posición encontrada por sondeo lineal,
se procede a almacenar el elemento en dicha posición.

En general, habrá una posición disponible, siempre y cuando la tabla sea lo


suficientemente grande. Aquella circunstancia donde la mayoría de las
posiciones queden ocupadas será un problema, ya que puede llegar a ser
posible tener que transitar por casi toda la estructura hasta lograr localizar
aquella posición libre.

Se hará uso del ejercicio que el autor Weiss propone, para explicar cómo
funcionan las inserciones en una tabla hash:

5
Figura 2: Representación gráfica de inserciones en una tabla hash usando
sondeo lineal

Fuente: Weiss, 2013, p. 779.

En este ejemplo se insertan en la tabla los valores: 89, 18, 49, 58 y 9. Al


aplicar la función hash a la clave 89, se obtiene el índice 9. Por esto se
procede a visitar la posición 9. Dicha posición se encuentra abierta, así que
se lo inserta sin problemas. Con el siguiente elemento, la clave 18, la
función hash indica que debe ser almacenado en la posición 8 de la tabla, la
cual también estaba abierta. Pero, al calcular la función hash de la clave 49,
se obtiene, nuevamente, el índice 9, el cual se puede ver que está cerrado.
Según la técnica de sondeo lineal, se debe buscar la siguiente posición
libre. Si bien es el fin de la tabla, se procede a comenzar el sondeo desde el
inicio, como si fuera circular. Dado que la posición 0 está libre, se almacena
allí. Este proceso continúa hasta terminar de hacer las inserciones.

Búsqueda

El proceso de búsqueda de un elemento se realiza de manera similar a la


inserción. Es decir, se calcula la función hash para el elemento a buscar y se
accede al índice para verificar si el elemento que se encuentra en dicha
posición corresponde al buscado. En caso negativo, se procede a realizar el
sondeo lineal hasta encontrar el elemento buscado o hasta encontrar una
posición vacía, lo cual significaría que el elemento buscado no se encuentra
almacenado en la tabla.

6
Si se continúa con el ejemplo anterior, en el caso de buscar al valor 49, se
calcula su función hash obteniendo el índice 9. En este, vemos que no está
el valor buscado, por lo que se procede a buscar en la siguiente posición.
Como termina la estructura en la posición 9, se procede a inspeccionar en
la posición 0. Allí, se encuentra el valor buscado, por lo que termina el
proceso.

Ahora bien, si se buscara un elemento que no está almacenado, se


transitaría posición tras posición, dentro de la tabla, hasta encontrar una
posición abierta (vacía) y se concluiría en que dicho elemento no está
almacenado en esta.

Una consideración particular es que, al realizar una inserción,


posiblemente se debe cuidar de no hacer un duplicado. Por esto,
primeramente, se deberá realizar una búsqueda y, luego, la inserción en sí
misma, en caso de que la búsqueda arroje que dicho elemento no se
encontraba en la tabla.

Eliminación

Si se elimina un elemento, se altera el proceso de búsqueda posterior,


debido a que quedaría la posición libre. Por ende, el proceso de búsqueda
se cortaría y se concluiría en que el elemento buscado no se encuentra
cuando, muy factiblemente, puede estar ubicado en las siguientes
posiciones. Por esto, se procede a realizar una eliminación perezosa, que
consiste en realizar una eliminación lógica, pero no física, a través de un
marcado y se indica que ese elemento se eliminó. De esta forma, ahora, las
posiciones tendrán tres estados: abierto, cerrado o eliminado.

Análisis del método

Este método permite insertar o buscar un elemento en un tiempo casi


constante, debido a que se accederá al índice arrojado por la función hash
y, en caso de ser necesario, se hará una pequeña exploración o sondeo
lineal. Ahora bien, a medida que la tabla se vaya llenando, las
exploraciones serán más largas, debido a que será más difícil localizar una
posición vacía. En promedio, se transitará por la mitad de la estructura.

Para evitar esa problemática, será necesario controlar el porcentaje de


ocupación de la tabla, con el objetivo de que, una vez superado un
porcentaje umbral (en general un 60 % o un 70 %), se la deberá
redimensionar.

7
Aunque el porcentaje de ocupación de la tabla esté por debajo del 60 %, se
podrá observar un comportamiento no deseado, el cual es nombrado
como agrupamiento primario y agrupamiento secundario.

El agrupamiento primario es un fenómeno en el que los elementos


insertados en la tabla empiezan a formar islas de agrupamiento y
ocasionan excesivos intentos de resolución de las colisiones. Ante las
nuevas inserciones, las exploraciones lineales serán largas, lo cual generará
que sean más ineficientes y costosas.

Sondeo cuadrático
Esta técnica surge para resolver la problemática del agrupamiento
primario. Consiste en que ante un valor H arrojado por la función hash, en
vez de hacer una exploración lineal ante una colisión, es decir, a través del
tránsito una a una de cada posición (H+1, H+2, ..., H+i), se realice una
exploración siguiendo la forma H+1, H+4, …, H+i2.

Se usará, nuevamente, el ejemplo propuesto por Weiss:

Figura 3: Representación gráfica de las inserciones en una tabla hash


usando el sondeo cuadrático

Fuente: Weiss, 2013, p. 774.

8
Si se analiza la primera inserción, la clave 89 será insertada en la posición 9,
ya que se encuentra disponible. La clave 18 podrá ser insertada en la
posición 8 sin problema, ya que no hay colisión. La clave 49 presenta una
colisión en la posición 9, por lo que se procede a evaluar la disponibilidad
de la siguiente posición. Como está libre, se lo asigna en la posición 0.
Ahora bien, ante la clave 58, la situación es distinta: el índice arrojado por
la función es 9, pero ocurre una colisión, por lo que se procede a verificar la
siguiente posición y se obtiene una nueva colisión. Se evalúa la posición
8+i^2 = 8+4. Entonces, si se considera que el recorrido es circular, se
cuenta a partir de la posición 8, 4 posiciones más, y se llega a la posición 2.
Dicha posición está disponible, por lo que se almacena el elemento allí. Así,
se continúa el proceso para almacenar a todos los elementos. Esta técnica
resuelve el agrupamiento primario, pero produce un agrupamiento
secundario.

“En el sondeo cuadrático, los elementos cuyo valor hash corresponde a una
misma posición provocan un sondeo de las mismas celdas alternativas,
fenómeno que se conoce con el nombre de agrupamiento secundario”
(Weiss, 2013, p. 787).

Esto genera una nueva problemática: puede suceder que no se logre


insertar un elemento en la tabla, aunque haya espacio disponible.

Afortunadamente, se logró que este problema quede resuelto, si se


garantiza el cumplimiento de dos condiciones: que la tabla no supere un
porcentaje de ocupación umbral del 50 % y que el tamaño de la tabla sea
primo.

Encadenamiento separado
Esta técnica resuelve las colisiones a través del uso de las listas en cada
posición de la tabla. Por lo que, cuando un elemento colisione, no se
buscará otra posición libre en la tabla, sino que se la añadirá en una lista
enlazada, almacenada en dicha posición.

De esta manera, se puede interpretar al valor brindado por la función hash,


como un valor que indica en cuál lista almacenar un elemento en caso de
inserciones o en qué lista se puede encontrar a un elemento, en el caso de
una búsqueda.

9
Si bien se sabe que las búsquedas en las listas son operaciones lineales, si la
lista es pequeña, no se incurrirá en mucho tiempo y se mantendrá un
tiempo casi constante.

Es evidente notar que el porcentaje de carga de la tabla no es una


problemática en esta técnica. El problema resulta ser el tamaño de las
listas en cada posición de la tabla. Por esto, se debe buscar elegir un
tamaño de tabla acorde, que garantice que las listas no tendrán más de 10
elementos y que no sea tan grande como para desperdiciar mucho espacio.

10
Referencias
Cormen, T., Leiserson, C., Rivest, R., & Stein, C. (2009). Hash Tables. En Autores,
Introduction to Algorithms (pp. 253-285). Massachusetts, USA: The MIT Press.

Sznajdleder, P. A. (2012). Estructuras de datos dinámicas lineales en Java. En D.


Fernández (Ed.), Algoritmos a fondo con implementación en C y JAVA (pp. 449-
459). Buenos Aires, AR: Alfaomega.

Weiss, M. A. (2013). Tablas hash. En M. Martín-Romo (Ed.), Estructuras de datos


en Java (pp. 763-792). Madrid, ES: Pearson.

11

También podría gustarte