Cátedra: Saubidet
Las funciones de hashing unidireccionales son, valga la redundancia, funciones que al aplicárselas a una clave o mensaje (M) retorna un valor dado. Además, debe cumplir con 3 características específicas:
El uso principal es para firmas digitales, encriptar datos y archivos.
El método de resolución de colisiones lineal, cíclico, con doble hashing y sin área de overflow mezcla los registros que se encuentran en su posición con los que buscaron posiciones alternativas por encontrarse su posición ocupada. Es decir, los datos se almacenan en el mismo espacio que los sinónimos. Además, al no usar buckets, solo se almacena un registro por posición.
Como el método es lineal, cíclico y con doble hashing lo primero que se hace al encontrar una colisión es buscar el registro siguiente y, en caso de estar ocupado, se aplica otra función de hashing. (o sería al revés?, al encontrar una colisión buscaría en otra posición con la segunda función de hashing y después en forma lineal).
Construyo 2 funciones de hashing para aplicarle a los strings, con un espacio de direcciones contenido en [0 - M-1]
![]() | ![]() | ![]() | ![]() | ![]() |
|
---|---|---|---|---|---|
S0 | 0 | 1 | |||
S1 | 5 | 3 | |||
S2 | 4 | 0 | |||
S3 | 3 | 2 | |||
S4 | 6 | 2 |
Armo un grafo con los M-1 nodos.
Uno los nodos del grafo según las funciones de hashing definidas anteriormente y nombro las aristas según el string al que corresponden esos valores.
Verifico que no tenga ciclos y rotulo un nodo a elección con un valor en particular; por ejemplo, el nodo 0 con el valor 0.
Luego, para cada nodo vecino le asigno el rótulo dado por la cuenta:
Para el nodo 1:
Para el nodo 4:
Como ya no tengo nodos vecinos, rotulo aleatoriamente un nuevo nodo. Por ejemplo, el nodo 2 con el valor 0.
Si rotulando como antes, para el nodo 6:
Para el nodo 3:
Y por último, para el nodo 5:
Con esto construyo una nueva función de hashing, G(x), la cual tendrá para cada nodo del grafo su rótulo asociado:
x | G(x) |
---|---|
0 | 0 |
1 | 0 |
2 | 0 |
3 | 3 |
4 | 2 |
5 | 3 |
6 | 4 |
Ahora sigo completando la tabla. Primero le aplico la función de hashing a los valores retornados por
y
:
![]() | ![]() | ![]() | ![]() | ![]() |
|
---|---|---|---|---|---|
S0 | 0 | 1 | 0 | 0 | |
S1 | 5 | 3 | 3 | 3 | |
S2 | 4 | 0 | 2 | 0 | |
S3 | 3 | 2 | 3 | 0 | |
S4 | 6 | 2 | 4 | 0 |
Y por último, sumo los valores de con
, terminando al aplicar módulo 5 al resultado.
![]() | ![]() | ![]() | ![]() | ![]() |
|
---|---|---|---|---|---|
S0 | 0 | 1 | 0 | 0 | 0 |
S1 | 5 | 3 | 3 | 3 | 1 |
S2 | 4 | 0 | 2 | 0 | 2 |
S3 | 3 | 2 | 3 | 0 | 3 |
S4 | 6 | 2 | 4 | 0 | 4 |
Lo que tengo que guardar es ,
y
. Para usarlo le aplico
y
al string, luego
a los resultados terminando con módulo 5:
Por ejemplo, para S3:
pos mod
mod
Existen varios métodos de resolución de colisiones entre los que se encuentran hashing lineal, hashing cuadrático y doble hashing. En estos 3 métodos la posición a la que queremos acceder viene dada por la función: mod
, con
.
Es decir, que la posición depende de una función de hashing y una función auxiliar
, donde
es el número de intento por encontrar la posición comenzando en
. Al final se le aplica mod
para que la suma caiga en el espacio de direcciones. Esa función
es la que va a variar en los 3 casos.
Para la resolución de colisiones lineal , lo que significa que en primer lugar accede a la posición dada por la función de hashing; y en caso de una colisión se accede al elemento siguiente, y si nuevamente hay una colisión busca en el siguiente, y así sucesivamente hasta encontrar una posición libre. Al llegar al final del archivo, comienza por la primer posición. La ventaja es que si existe un lugar vacío, el método lo va encontrar. Sin embargo, la desventaja es que debido a que los sinónimos y los registros vecinos producirán las mismas posiciones a probar se produce un aglomeramiento de registros llamado clústering. Esto es perjudicial porque hace que tengamos áreas densamente pobladas, y otras vacías.
Una forma de solucionar este problema es cambiar la función auxiliar por
, que es lo que usa el método de resolución de colisiones cuadrático . De esta forma eliminamos los clústering, ya que ahora las posiciones vecinas no producirán el mismo patrón de búsqueda. Ante la primer colisión accedemos a la siguiente posición (
), luego nos alejamos un poco más a 4 lugares de distancia (
), luego 9 lugares (
), y así alejándonos cada vez más. Como dijimos antes, la ventaja es que eliminamos el clústering, pero generamos clústering secundario, que son aglomeraciones de registros, pero menos densas y más distribuidas (cosa que perjudica a la cache). Además, es posible que existan posiciones libres y no las encuentre, ya que puedo ir saltando siempre entre las mismas posiciones. Para esto último hay un teorema que dice que si el factor de carga es menor a 0.5 y
es primo, entonces, si existe un lugar libre lo encuentro si o si.
Para solucionar el clústering secundario se usa el método de resolución de colisiones con doble hashing , que consiste en que sea otra función de hashing:
. De esta forma los sinónimos no necesariamente siguen la misma secuencia de posiciones a probar. Si la segunda posición a probar también estuviera ocupada podría continuarse en forma lineal, cuadrática, random o con otra función de hashing.
Entonces, la principal diferencia entres estos 3 métodos radica en la función , que puede ser
,
, o
. Esto tiene como efecto que el método lineal genera áreas localizadas muy densas de registros muchas otras con densidades casi nulas. Mientras que el método cuadrático produce clústering secundario que es aglomeramiento, pero no tanto y más disperso, y no siempre garantiza encontrar una posición libre. El método de doble hashing soluciona los problemas de ambos y puede combinarse con ellos.
En el caso de que las probabilidades de ocurrencia de los registros sean equivalente y el resultado de la primer función de hashing aplicado al espacio de claves de un resultado uniforme, elegiría el método lineal para tener los sinónimos cerca y aprovechar las optimizaciones de la cache. Si la distribución de la función de hashing es uniforme, pero es más probable que aparezcan ciertas claves, como podría ser al aplicar div 100 a los padrones de los alumnos que cursan una materia. Obviamente los más altos serán más probables y cuanto más chico sea el padrón , menor será la posibilidad de que aparezca un padrón de ese rango. En este caso usaría el de doble hashig, tal vez combinado con el método cuadrático.
Con y si:
pos | registro |
---|---|
0 | |
1 | r1 |
2 | |
3 | |
4 | |
5 |
pos | registro |
---|---|
0 | |
1 | r1 |
2 | r2 |
3 | |
4 | |
5 |
pos | registro |
---|---|
0 | |
1 | r1 |
2 | r2 |
3 | |
4 | |
5 | r3 |
pos | registro |
---|---|
0 | |
1 | r1 |
2 | r2 |
3 | |
4 | r4 |
5 | r3 |
intento | ![]() | ![]() | ![]() ![]() |
---|---|---|---|
0 | 0 | 1 | 1 (ocupado) |
1 | 1 | 2 | 2 (ocupado) |
2 | 4 | 5 | 5 (ocupado) |
3 | 9 | 10 | 4 (ocupado) |
4 | 16 | 17 | 5 (ocupado) |
5 | 25 | 26 | 2 (ocupado) |
6 | 36 | 37 | 1 (ocupado) |
7 | 49 | 50 | 2 (ocupado) |
8 | 64 | 65 | 5 (ocupado) |
9 | 81 | 82 | 4 (ocupado) |
Para corroborar que funciona con 10.000.000 de intentos, podemos correr el siguiente código python:
max = 10000000 valores = [1, 2, 4, 5] for i in range(0, max): if not ((i**2 + 1) % 6 in valores): print "Error %d" % i
En este caso vemos que el factor de carga (mayor a 0.5) y que la cantidad de registros totales (N) es un número par (6). Esas serían las cosas que pueden ayudar a darse cuenta si es posible no encontrar una posición libre, por más que exista. Lo primero que hay que modificar es tener siempre una cantidad de registros totales impar, en este caso podría ser 7. Luego, ante cada inserción hay que controlar que el factor de carga sea menor a 0.5; si en algún momento
pasa ese valor hay que extender el archivo hasta el siguiente número primo. Esto es así ya que esta demostrado que si N es primo y el factor de carga es menor a 0.5 el método encontrará un lugar libre en caso de existir.
En primer lugar analizaría el factor de carga del archivo; si es muy alto lo más recomendable es crear uno nuevo y reorganizarlo. Si el factor de carga es menor a 0.5 el error es del método de resolución de colisiones. Puede ajustarse mejor el método al caso de uso al cambiar una o ambas funciones de hashing, también puede cambiarse parcialmente (agregar un área de overflow distribuida y/o con encadenamiento de sinónimos) o en forma completa (pasar a usar un método lineal con encadenamiento de sinónimos).
El cústering es una gran aglomeración de registros en áreas particularmente definidas, dejando una densidad muy baja (casi nula) en el resto del archivo. Suele producirse cuando se usa el método de resolución de colisiones lineal y se insertan una gran cantidad de sinónimos.
El método cuadrático elimina el clústering al recorrer el archivo con saltos cada vez más grandes; pero provoca el clústering secundario que también es una aglomeración de registros pero no tantos y más dispersos en el archivo.
Para solucionar ambos problemas se usa el método de doble hashing al no seguir la serie de posiciones a probar para todos los sinónimos.
Cuando se quiere dar una alta en este tipo de archivos lo primero que se hace es aplicarle la función de hashing a la clave del registro y hacer un acceso directo a esa posición. Se observa si el bucket tiene espacio para un registro más, y en caso de ser así se lo coloca y actualiza la cantidad de registros en el bucket. Si el bucket estaba lleno y el offset es 0 significa que no hay más sinónimos en el archivo, por lo que se busca un bucket libre en el área de overflow, se lo almacena allí, se pone el contador de registros ocupados en 1 y se referencia desde la posición original al nuevo bucket. No se puede usar uno que ya se este usando pero tenga espacio ya que en un bucket solo pueden coexistir sinónimos y este no sería el caso ya que antes dijimos que no había más sinónimos en el archivo. Si el offset era distinto de cero, entonces accedo a la posición indicada y sigo la lista hasta encontrar el último bucket encadenado. En este momento, podemos encontrarnos con dos situaciones: que este bucket tenga espacio para el registro o que se encuentre lleno. Si tenía espacio lo guardo y actualizo el contador de registros ocupados. Si estaba lleno, busco un bucket libre en el cual guardo el registro y actualizo los datos administrativos. Si en alguno de los dos casos en que buscaba un bucket libre el área de overflow estaba llena, retorno un error.