Capítulo 3. Clasificación en Memoria Secundaria INTRODUCCIÓN Las memorias secundarias (cintas magnéticas, discos duros, ) se caracterizan en general porque el acceso es secuencial. Es decir, en un instante dado sólo se tiene acceso al primer elemento. Por lo tanto el TDA que se considerará será la secuencia implementada mediante una cinta. En un disco duro la secuencia se implementará lógicamente sobre un archivo. En primer lugar se tratarán los métodos para clasificar archivos secuenciales. Los más representativos son: Mezcla directa Mezcla natural Mezcla balanceada múltiple Clasificación polifásica El término mezcla debe entenderse no como sinónimo de clasificación, sino como una estrategia específica en la que se obtiene una secuencia de salida a partir de dos o más secuencias de entrada. La mezcla es una operación auxiliar utilizada como estrategia para desarrollar la tarea de clasificación, y es previa a ésta. Se entiende por fase cada operación que trata un conjunto completo de datos, y se entenderá por pase (o etapa) al proceso más corto que por repetición constituye el proceso de clasificación. Cinta: Cada una de las secuencias necesarias en el proceso de clasificación. CLASIFICACIÓN EXTERNA BASADA EN MEZCLA Mezcla Directa Se divide la cinta a ordenar c1 (cinta fuente) en dos mitades c2 y c3 (cintas destino), c2 contiene los elementos de las posiciones impares y c3 los de las posiciones pares. El algoritmo puede resumirse de la siguiente forma: Tomar como fuente la secuencia original c1 Dividir la fuente en dos mitades, en las cintas destino c2 y c3 Mezclar c2 y c3 combinando cada elemento accesible en pares ordenados en c1 Repetir el proceso: se obtiene una cinta con cuádruplos ordenados Repetir el proceso hasta que toda la cinta esté ordenada Cada pase o etapa consta de dos fases: una de división y otra de mezcla, por esto se denomina mezcla de dos fases o mezcla de tres cintas. 1
Se puede observar que la fase de división no aporta nada a la clasificación ya que no se combinan los elementos de ninguna manera, y sin embargo tienen un coste significativo. Por tanto, sería interesante reducir esta fase. Las fases de división pueden eliminarse completamente combinando la fase de división con la mezcla. En vez de mezclar en una sola cinta, c, que posteriormente será dividida, puede irse separando en dos, de manera que en el siguiente pase ya estarán divididas. Este método se conoce como mezcla de una fase, o mezcla directa balanceada. Mezcla Natural La mezcla natural aprovecha el hecho de que entre los elementos de la secuencia original, algunos elementos consecutivos ya se encontrarán ordenados entre sí. La mezcla natural se basa en la combinación de subsecuencias ordenadas. Las subsecuencias ordenadas de la cinta fuente, se distribuyen en dos cintas destino auxiliares a y b. Seguidamente se mezcla una subsecuencia ordenada de cada cinta auxiliar. Por reiteración de este proceso se obtiene la secuencia ordenada. Así, el algoritmo se puede resumir de la siguiente forma: Fuente c Distribuir las subsecuencias ordenadas de la fuente en las cintas destino a y b. Mezclar a y b en c, combinando subsecuencias ordenadas de cada cinta auxiliar. Cada pase en la mezcla natural consta de dos fases, una de distribución y otra de mezcla. En el peor caso, el número de movimientos es de orden n log n, en inferior en el caso promedio. El número de comparaciones es mucho mayor, pero al ser el coste de una comparación muy inferior al de un movimiento, este incremento no resulta significativo. 2
Mezcla Balanceada Múltiple En la mezcla directa la operación de copia es la más costosa y por tanto, una disminución del número de copias reducirá significativamente el coste. El algoritmo de mezcla natural copia tanto en la fase de distribución como en la de mezcla. La mezcla balanceada múltiple persigue reducir el número de movimientos, reducir el número de pases. Esto se consigue realizando la distribución entre dos o más cintas (en la mezcla se combinan varias, más de dos subsecuencias ordenadas). Además podemos eliminar la fase de distribución mediante la c o p i a d e l a s s u b s e c u e n c i a s ordenadas, durante el proceso de mezcla en cintas destino auxiliares que harán de fuente en el siguiente pase, y así las cintas fuente y destino se alternan consecutivamente. Análisis de la mezcla balanceada: Supóngase que se utilizan N cintas destino en la fase de distribución. Mezclar m subsecuencias ordenadas que están distribuidas uniformemente en N cintas origina, en una primera fase de mezcla, una subsecuencia ordenada de m/n subsecuencias ordenadas. En la segunda fase de mezcla se tendrán m/n 2 y en la k-ésima una subsecuencia ordenada de m/n k. El número total de pases necesarios para clasificar n subsecuencias ordenadas con N cintas será en el peor de los casos: k = [log N n] Como en cada pase se necesitan n copias, el número total de copias vendrá dado por: M = n [log N n] Clasificación Polifásica Mejora el rendimiento de la mezcla balanceada. Las cintas fuente y destino no son establecidas a priori, sino como consecuencia de la mezcla realizada. En el proceso de mezcla, dos hacen de fuente y una tercera hace de destino, al finalizar las combinaciones hará de cinta destino aquella que se haya agotado, la cual sólo podrá se identificada tras la mezcla. La clasificación polifásica aprovecha al máximo las cintas, ya que con N cintas realiza mezclas de N-1 subsecuencias ordenadas. Lo que se pretende es que al final haya una sola subsecuencia ordenada en una cinta y las demás estén agotadas. Esto no siempre es posible. 3
En el ejemplo de la derecha hay dos cintas vacías, la cinta C 1 tiene todavía dos subsecuencias ordenadas, con lo que no se ha clasificado la secuencia. Construcción de la clasificación polifásica satisfactoria de tres cintas Para n=0 c 1(0) = 1 c 2(0) = 0 y y para cada nivel n > 0 se observa que: c 2(n + 1) = c 1(n) c 1(n + 1) = c 1(n) + c 2(n) Así: c 1(n + 1) = c 1(n) + c 1(n - 1) c 2(n + 1) = c 1(n) Por tanto, c 1(n) son números de Fibonacci, en los que cada uno se obtiene sumando los dos predecesores, y además c 2(n) es el predecesor de c 1(n). ARCHIVOS INDEXADOS Los métodos de clasificación anteriores son secuenciales. Sin embargo, los discos duros, por ejemplo, permiten además un acceso casi aleatorio. Al ser dispositivos de acceso directo, si se conoce una dirección física dada, se puede acceder a ella mediante un acceso aleatorio al sector seguido de un acceso secuencial dentro de él (offset). La información almacenada no suele consistir únicamente en la llave, consta de un registro con múltiples campos, siendo uno de ellos la llave que se utiliza para la clasificación. El método de clasificación mediante archivos indexados se basa en considerar, asociada a cada llave, la dirección física del registro que caracteriza. Así, se crea un segundo archivo denominado archivo de índices, en el que se almacenan pares (llave, dirección). Las operaciones de clasificación y búsqueda suelen realizarse en memoria principal sobre el archivo de índices y no sobre el archivo de datos. Para recuperar un registro con una llave dada se busca en el archivo de índices la llave deseada, y entonces se accede a la dirección física que tiene asociada. La idea básica de los archivos indexados suele refinarse agrupando las llaves en bloques. En este caso, al archivo de índices se le llama de índice disperso y está formado por grupos de pares (x, b) donde b es la dirección física del bloque en el cual el primer registro tiene la llave de valor x. 4
TABLAS DE DISPERSIÓN (HASHING) La idea de estas tablas no es ordenar, sino saber donde guardar la información y por tanto donde se puede buscar rápidamente. El acceso a los sectores de un disco de la memoria secundaria es directo, con lo cual, cada vez se se tenga un dato habrá que tener una función de dispersión que lo mande a un sector. Definiciones Definición 1.- Dado un conjunto de llaves posibles X, y un conjunto de direcciones de memoria D, una función de transformación H(x) es una aplicación suprayectiva del conjunto de llaves posible en el conjunto de direcciones de memoria: H : X D Definición 2.- El TDA tabla de dispersión es un tipo de datos homogéneo, denominadas celdas, de tamaño fijo Ttabla, compuesto por un número fijo de componentes a las que se accede mediante una dirección de memoria resultante de una función de transformación. Sobre este TDA se definen los operadores Insertar, Buscar y Eliminar. Definición 3.- Se dice que dos llaves distintas x 1 y x 2 son sinónimas para una función de transformación H(x) si H(x 1) = H(x 2). Definición 4.- Se dice que se ha producido desbordamiento cuando una nueva llave se aplica a una dirección de memoria completamente ocupada, y se dice que se ha producido una colisión cuando dos llaves distintas se aplican sobre la misma celda. Definición 5.- Se denomina densidad de llaves al cociente entre el número de llaves en uso m y el número total de llaves posibles n x. Se denomina factor de carga (densidad de carga) α, al cociente entre el número de llaves en uso y el número total de registros almacenables en la tabla de dispersión: α = m/s*b, donde s es el número de registros por bloque y b el número de bloques que hay en la tabla de dispersión. La función de dispersión tiene que ser eficiente (sencilla) y uniforme, es decir, que reparta más o menos igual los datos entre todos los sectores. La función de transformación más conocida es el resto de la división entera: f(x) = x mod M. Así, el espacio de direcciones de los bloques será [0, M-1] y la tabla tendrá como mínimo M bloques. Un ejemplo de una función de dispersión podría ser.- Tenemos alumnos que llegan a con un número cada uno de ellos determinado, tenemos también que M=15. La función podría ser: (nº alumno) mod 15 si tuviéramos el alumno 715 será >>> 715 mod 15= 10 por lo que iría al sector 10 Supongamos que en cada sector sólo cabe un dato, si nuestra función mandara a una posición ocupada un dato, éste no cabría, por lo que se produce lo que se denomina colisión. Otra función hashing clásica es la conocida como plegado: la dirección se obtiene dividiendo la llave en partes iguales y sumando todas ellas. La suma de las partes puede realizarse directamente (plegado por desplazamiento) o plegar el identificador por las fronteras de las partes y sumar los dígitos coincidentes, dejando igual las posiciones impares e invirtiendo los de las posiciones pares (plegado por las fronteras). 5
Ejemplo 1.- Cadena de ocho caracteres representados por los números de orden dentro de la secuencia de cotejo correspondiente: 68 75 82 71 63 93 102 75 Plegado por desplazamiento: 75+102+93+63+71+82+75+68 = 629 >>> Dirección = 629 Plegado por las fronteras (en base decimal): 75+201+93+36+71+28+75+86 = 665 >>> Dirección = 665 Plegado por las fronteras (en base binaria): 102 >>>>> 01100110 >> invertir >> 01100110 >>>> 102 63 >>>>>> 00111111 >> invertir >> 11111100 >>>>>> 252 82 >>>>>> 01010010 > invertir >> 01001010 >>>> 74 68 >>>>>> 01000100 > invertir > 00100010 >>>>> 34 75+102+93+252+71+74+75+34 = 776 >>> Dirección = 776 Manejo de desbordamiento o sobrecarga Cuando se ha de insertar una nueva llave, si la celda que le corresponde está ocupada, se produce una colisión, y si todo el bloque está lleno, se produce un desbordamiento o sobrecarga. El método de manejo de sobrecargas más evidente es la exploración lineal. Consiste en buscar en los bloques siguientes hasta encontrar una celda libre. En general la exploración puede expresarse así: dirección = f(x) + g(x), donde f(x) es la función de dispersión y g(x) es la función de desbordamientos o sobrecargas. Considerando la tabla circular, la dirección vendrá dada por: dirección = (f(x) + g(x)) mod TamañoTabla En el caso de la exploración lineal: g(x) = i, donde i = 1 TamañoTabla La exploración lineal tiende a colocar las llaves de manera poco uniforme a lo largo de la tabla, de manera que las sobrecargas producidas irán llenando los bloques cercanos a los ocupados. Una técnica que mejora este comportamiento es la exploración cuadrática, definida tal que: g(x) = i 2, donde i = 1 TamañoTabla Con esta función de exploración, la búsqueda se realiza examinando los bloques: f(x), (f(x) + i 2 ) mod TamañoTabla y (f(x) - i 2 ) mod TamañoTabla La técnica rehashing generaliza los conceptos anteriores, de forma que la función de exploración será una familia de funciones de dispersión que se examinan sucesivamente en un orden dado. Así: g(x) = f i (x), donde i = 1 m y donde cada f i (x) es una función de dispersión 6
Existen otras estrategias de tratamiento de colisiones. La más elemental consiste en mantener una lista asociada a cada bloque de la tabla hash, de manera que en ella se almacenan todos los sinónimos que no se pueden insertar en el bloque correspondiente. La implementación más adecuada de esta lista es mediante una lista enlazada. Esta estrategia se denomina encadenamiento y requiere disponer en cada bloque del espacio necesario para almacenar un enlace (por ejemplo, un puntero). Ejemplo 1 Se dispone de una tabla vacía con 5 bloques que utiliza la función de dispersión h(k) = 2k mod 5, resolviendo las colisiones por encadenamiento directo. Mostrar la situación de la tabla durante la inserción sucesiva de las llaves 2, 3 y 12. ------------------------------------------- La aplicación de la función hash a las llaves es la siguiente: h(2) = 2(2) mod 5 >>> h(2) = 4 mod 5 >>> h(2) = 4 h(3) = 2(3) mod 5 >>> h(3) = 6 mod 5 >>> h(3) = 1 h(12) = 2(12) mod 5 >>> h(12) = 24 mod 5 >>> h(12) = 4 Ejemplo 2 Mostrar el resultado final tras la inserción sucesiva de las llaves 14, 35, 21, 42 y 17 en una tabla de dispersión (hash) vacía de tamaño 7. Las colisiones se resuelven mediante rehashing. La función hash primaria es h(k) = k mod 7 y la secundaria h (k) = 5 - (k mod 5). ------------------------------------------- Posiciones de la tabla hash: Posiciones = (0, 1, 2, 3, 4, 5, 6) >> vacía >> (#, #, #, #, #, #, #) Inserción del 14 >>> h(14) = 14 mod 7 = 0 >>> Hash = (14, #, #, #, #, #, #) Inserción del 35 >>> h(35) = 35 mod 7 = 0 >>> colisión Por tanto: h (35) = 5 - (35 mod 5) = 5-0 = 5 >>> Hash = (14, #, #, #, #, 35, #) Inserción del 21 >>> h(21) = 21 mod 7 = 0 >>> colisión Por tanto: h (21) = 5 - (21 mod 5) = 5-1 = 4 >>> Hash = (14, #, #, #, 21, 35, #) Inserción del 42 >>> h(42) = 42 mod 7 = 0 >>> colisión Por tanto: h (42) = 5 - (42 mod 5) = 5-2 = 3 >>> Hash = (14, #, #, 42, 21, 35, #) Inserción del 17 >>> h(17) = 17 mod 7 = 3 >>> colisión Por tanto: h (17) = 5 - (17 mod 5) = 5-2 = 3 >>> colisión Así pues usamos h(k) + h (k) = 3 + 3 = 6 >>> Hash = (14, #, #, 42, 21, 35, 17) Posiciones = (0, 1, 2, 3, 4, 5, 6) = (14, #, #, 42, 21, 35, 17) 7