Métodos de ordenamiento
Por Martín Lebuchorskyj Algoritmos y Programación I (75.40) Cátedra Ing.Guarna
Algoritmos y Programación I
Cátedra Ing.Guarna
Índice 1.
Métodos internos .............................................................. 3 1.1. Algunos métodos iterativos ........................................... 3 Burbujeo (bubble sort) ......................................................... 3 Inserción directa (insertion sort) ............................................ 4 Inserción binaria (binary insertion sort) .................................. 5 Selección (selection sort) ...................................................... 6 Stupid Sort ......................................................................... 6 Gnome Sort ........................................................................ 6 Shake Sort ......................................................................... 7 Shell Sort ........................................................................... 8 Comb Sort .......................................................................... 8 Heap Sort ........................................................................... 9 1.2. Algunos métodos recursivos........................................ 10 Quick Sort ........................................................................ 10 Stooge Sort ...................................................................... 11 Merge Sort ....................................................................... 12 1.3. Estabilidad ............................................................... 13 1.4. Eficiencia.................................................................. 13 1.5. Análisis fáctico de la eficiencia..................................... 13 Caso de prueba ................................................................. 13 Resultados........................................................................ 14 Observaciones................................................................... 14 Conclusiones..................................................................... 15 1.6. Análisis fáctico del tiempo de proceso........................... 15 Caso de prueba ................................................................. 15 Resultados........................................................................ 15 Observaciones................................................................... 16 Conclusiones..................................................................... 16 1.7. Conclusión derivada de las pruebas.............................. 17 2. Métodos externos ........................................................... 17 2.1. Merge ...................................................................... 17 Implementación ................................................................ 17 Eficiencia .......................................................................... 19 Conclusiones..................................................................... 20 2.2. Transformando métodos internos................................. 20 Consideraciones ................................................................ 20 Ejemplo: Bubble Sort externo.............................................. 21 Ejemplo: Quick Sort externo ............................................... 21 2.3. Análisis fáctico de acceso a disco y tiempo de proceso .... 23 Caso de prueba ................................................................. 23 Resultados........................................................................ 23 Observaciones................................................................... 24 Conclusiones..................................................................... 24
Por Martín Lebuchorskyj
All right reserved
Página 2
Algoritmos y Programación I
Cátedra Ing.Guarna
Métodos de ordenamiento Los métodos de ordenamiento pueden dividirse en dos grandes grupos: • Internos • Externos
1. Métodos internos Los métodos internos son aquellos que se realizan exclusivamente en memoria RAM. La característica principal de estos métodos es que el acceso es rápido y el tiempo que requiere es el mismo para cualquier elemento de la serie. Si bien se pueden implementar sobre distintas estructuras de datos, la más común es el vector.
1.1. Algunos métodos iterativos Los ejemplos incluidos en esta sección trabajan con un vector de enteros (de tipo tv_int) cuyos elementos comienzan en la posición 1. El valor N corresponde a la cantidad de elementos contenidos en dicho vector. Es decir, un vector con posiciones enteras válidas en el rango 1..N Burbujeo (bubble sort) También se lo conoce como método de intercambio directo. Existen distintas variantes. Funciona revisando cada elemento de la lista que va a ser ordenada con el siguiente, intercambiándolos de posición si están en el orden equivocado. La siguiente versión corresponde a una de las formas tradicionales: La comparación de elementos consecutivos (pares adyacentes). procedure bubble_sort1(var vec:tv_int; n:integer); var i,j:integer; aux:integer; begin for i := 1 to n-1 do for j := 1 to n-i do if vec[j] > vec[j+1] then begin aux := vec[j]; vec[j] := vec[j+1]; vec[j+1] := aux; end; end;
Por Martín Lebuchorskyj
All right reserved
Página 3
Algoritmos y Programación I
Cátedra Ing.Guarna
La siguiente versión es idéntica a la anterior, pero incorpora una optimización. Los ciclos se detienen si se detecta que el vector ya está ordenado. procedure bubble_sort1_opt(var vec:tv_int; n:integer); var i,j:integer; aux:integer; sorted:boolean; begin i := 1; sorted := false; while (i <= n-1) and not sorted do begin sorted := true; j := 1; while (j <= n-i) do begin if vec[j] > vec[j+1] then begin aux := vec[j]; vec[j] := vec[j+1]; vec[j+1] := aux; sorted := false; end; inc(j); end; inc(i); end; end;
La siguiente versión, en lugar de comparar adyacentes, fija cada posición y la compara con todas las demás posiciones. procedure bubble_sort2(var vec:tv_int; n:integer); var i,j:integer; aux:integer; begin for i := 1 to n-1 do for j := i+1 to n do if vec[i] > vec[j] then begin aux := vec[i]; vec[i] := vec[j]; vec[j] := aux; end; end;
Inserción directa (insertion sort) Es una de las maneras más naturales e intuitivas de ordenamiento. Es el método que generalmente se utiliza cuando tomamos cartas de un mazo y la posicionamos en orden entre otras ya ordenadas que tenemos en la mano. Consiste en tomar un elemento, buscar su lugar dentro de los elementos ordenados e insertarlo.
Por Martín Lebuchorskyj
All right reserved
Página 4
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure insertion_dir_sort(var vec:tv_int; n:integer); var i,j:integer; aux:integer; begin for i := 2 to n do begin aux := vec[i]; j := i - 1; while (j >= 1) and (vec[j] > aux) do begin vec[j+1] := vec[j]; j := j - 1; end; vec[j+1] := aux; end; end;
Inserción binaria (binary insertion sort) Mantiene el mismo principio que el método de inserción directa pero optimiza la búsqueda de la posición de inserción. Dado que los elementos donde debe insertarse el nuevo elemento ya están ordenados, la búsqueda se realiza con el método binario en lugar del secuencial. procedure insertion_bin_sort(var vec:tv_int; n:integer); var i,j:integer; aux:integer; low,med,high:integer; begin for i := 2 to n do begin aux := vec[i]; low := 1; high := i - 1; while low <= high do begin med := (low + high) div 2; if aux < vec[med] then high := med - 1 else low := med + 1; end; j := i - 1; while j >= low do begin vec[j+1] := vec[j]; j := j - 1; end; vec[low] := aux; end; end;
Por Martín Lebuchorskyj
All right reserved
Página 5
Algoritmos y Programación I
Cátedra Ing.Guarna
Selección (selection sort) Este algoritmo se basa en la búsqueda del mínimo elemento en cada recorrido y luego realizar el intercambio entre la posición actual y dicho valor. Es similar a una de las versiones del método de burbujeo, pero minimiza los intercambios. procedure selection_sort(var vec:tv_int; n:integer); var i,j,low:integer; aux:integer; begin for i := 1 to n - 1 do begin low := i; for j := i + 1 to n do if vec[j] < vec[low] then low := j; aux := vec[i]; vec[i] := vec[low]; vec[low] := aux; end; end;
Stupid Sort Este algoritmo debe su nombre a su ineficiencia. Si bien no se basa en una mala idea hay otros métodos igual de simples pero más eficientes. Es uno de los pocos métodos no recursivos que utilizan un solo ciclo iterativo, aunque éste se reinicia constantemente. Se basa en recorrer el vector comparando elementos adyacentes. Ante la necesidad de intercambio, el ciclo se reinicia. La condición de fin del ciclo consiste en llegar al final del vector. procedure stupid_sort(var vec:tv_int; n:integer); var i:integer; aux:integer; begin i := 1; while i < n do if vec[i] > vec[i+1] then begin aux := vec[i]; vec[i] := vec[i+1]; vec[i+1] := aux; i := 1; end else inc(i); end;
Gnome Sort Este algoritmo es similar en forma al Stupid Sort, con la diferencia de que en lugar de comenzar desde el inicio ante un intercambio de valores, retrocede una posición. Si los intercambios son consecutivos el efecto es de retroceso del elemento hasta su lugar, con lo cual la sensación final es de vaivén. Esto mejora en mucho la performance respecto de Stupid Sort. Y se lo considera uno de los algoritmos de ordenamiento más simples que existe.
Por Martín Lebuchorskyj
All right reserved
Página 6
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure gnome_sort(var vec:tv_int; n:integer); var i:integer; aux:integer; begin i := 2; while (i <= n) do if vec[i-1] <= vec[i] then inc(i) else begin aux := vec[i-1]; vec[i-1] := vec[i]; vec[i] := aux; dec(i); if i = 1 then i := 2; end; end;
Shake Sort También llamado Double bubble (Burbujeo doble) o también Cocktail Sort. Aplica el método de burbuja pero en ambas direcciones. Lleva simultáneamente los valores menores hacia el inicio y los mayores hacia el final. procedure shake_sort(var vec:tv_int; n:integer); var low,high,last:integer; i,j:integer; aux:integer; begin low := 2; high := n; last := n; repeat { bubble to left (low) } for i := high downto low do if vec[i-1] > vec[i] then begin aux := vec[i-1]; vec[i-1] := vec[i]; vec[i] := aux; last := i; end; low := last + 1; { bubble to right (high) } for j := low to high do if vec[j-1] > vec[j] then begin aux := vec[j]; vec[j] := vec[j-1]; vec[j-1] := aux; last := j; end; high := last - 1; until low > high; end;
Por Martín Lebuchorskyj
All right reserved
Página 7
Algoritmos y Programación I
Cátedra Ing.Guarna
Shell Sort Es una mejora del método de inserción directa. El método se denomina “shell” en honor de su inventor Donald shell. También se lo llama “método de inserción con incrementos decrecientes”. Shell modifico los saltos contiguos resultantes de las comparaciones del método de selección por saltos de mayor tamaño obteniendo así una clasificación más rápida. procedure shell_sort(var vec:tv_int; n:integer); var i,j,med:integer; aux:integer; sorted: boolean; begin med := n; while med > 0 do begin med := med div 2; repeat sorted := true; for j := 1 to (n - med) do begin i := j + med; if vec[j] > vec[i] then begin aux := vec[j]; vec[j] := vec[i]; vec[i] := aux; sorted := false; end; end; until sorted; end; end;
Comb Sort Es un algoritmo de ordenamiento relativamente simple diseñado por Stephen Lacey y Richard Box, quienes lo describieron en la revista Byte en abril de 1991. El algoritmo Comb sort mejora el algoritmo de ordenamiento de burbuja comparándose en velocidad con algoritmos más complejos como el Quicksort. La idea básica es eliminar los pequeños valores cerca del final de la lista (estos valores hacen que el método de burbuja se haga lento). En el ordenamiento de burbuja siempre compara elementos adyacentes. La gran modificación que da nacimiento al Comb sort es que el espacio entre los elementos a comparar puede ser mucho mayor que uno.
Por Martín Lebuchorskyj
All right reserved
Página 8
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure comb_sort(var vec:tv_int; n:integer); var gap:integer; swap:boolean; i:integer; aux:integer; begin gap := n; swap := false; while (gap > 1) or swap do begin if gap > 1 then gap := (10 * gap) div 13; if (gap = 9) or (gap = 10) then gap := 11; swap := false; for i := 1 to n-gap do if vec[i] > vec[i+gap] then begin aux := vec[i]; vec[i] := vec[i+gap]; vec[i+gap] := aux; swap := true; end; end; end;
Heap Sort Este algoritmo consiste en almacenar todos los elementos del vector a ordenar en un montículo (heap), y luego extraer el nodo que queda como nodo raíz del montículo (cima) en sucesivas iteraciones obteniendo el conjunto ordenado. Basa su funcionamiento en una propiedad de los montículos, por la cual, la cima contiene siempre el menor elemento (o el mayor, según se haya definido el montículo) de todos los almacenados en él. procedure heap_sort(var vec:tv_int; n:integer); var fin:integer; begin heapify(vec,n); fin := n; while fin > 1 do begin swap(vec[fin],vec[1]); dec(fin); siftdown (vec,1,fin); end; end;
Por Martín Lebuchorskyj
All right reserved
Página 9
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure heapify(var vec:tv_int; n:integer); var start:integer; begin start := n div 2; while start >= 1 do begin siftdown(vec,start,n); dec(start); end; end; procedure siftdown(var vec:tv_int; low,high:integer); var root,child:integer; finish:boolean; begin finish := false; root := low; while (root * 2 <= high) and not finish do begin child := root * 2; if (child < high) and (vec[child] < vec[child+1]) then inc(child); if (vec[root] < vec[child]) then begin swap (vec[root],vec[child]); root := child; end else finish := true; end; end; procedure swap(var a,b:integer); var aux:integer; begin aux := a; a := b; b := aux; end;
1.2. Algunos métodos recursivos Quick Sort Quicksort es un algoritmo de ordenamiento considerado entre los más rápidos y eficientes. Fue diseñado en los años sesenta por el científico en computación Hoare. El algoritmo usa la técnica divide y vencerás que básicamente se basa en dividir un problema en sub-problemas y luego juntar las respuestas de estos sub-problemas para obtener la solución al problema central. Se tiene una vector de n elementos, se toma un valor del array como pivote (usualmente el primero), se separan los elementos menor a este pivote a la izquierda y los mayores a la derecha, es decir, se divide el vector en 2 subvectores. Con estos sub-vectores se repite el mismo proceso de forma recursiva.
Por Martín Lebuchorskyj
All right reserved
Página 10
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure quick_sort(var vec:tv_int; min,max:integer); var i,j:integer; piv,aux:integer; begin i := min; j := max; piv := (vec[min] + vec[max]) div 2; repeat while vec[i] < piv do inc(i); while vec[j] > piv do dec(j); if i <= j then begin aux := vec[j]; vec[j] := vec[i]; vec[i] := aux; inc(i); dec(j); end; until i > j; if min < j then quick_sort(vec,min,j); if max > i then quick_sort(vec,i,max); end;
Stooge Sort El tiempo de ejecución de este algoritmo es muy bajo, comparado con otros algoritmos eficientes como el Merge Sort. Realiza llamadas recursivas en tres ramas: La primera para tratar las dos terceras partes iniciales del vector, luego las dos terceras últimas, y por último nuevamente los dos tercios iniciales. Este algoritmo debe su nombre a “Los tres chiflados” (Three Stooges), debido a las escenas donde cada personaje golpeaba a los otros dos. procedure stooge_sort(var vec:tv_int; min,max:integer); var t:integer; aux:integer; begin if vec[min] > vec[max] then begin aux := vec[min]; vec[min] := vec[max]; vec[max] := aux; end; if (max - min) > 1 then begin t := (max - min + 1) div 3; stooge_sort(vec,min,max-t); stooge_sort(vec,min+t,max); stooge_sort(vec,min,max-t); end; end;
Por Martín Lebuchorskyj
All right reserved
Página 11
Algoritmos y Programación I
Cátedra Ing.Guarna
Merge Sort Fue desarrollado en 1945 por John Von Neumann, y funciona de la siguiente manera: Si la longitud del vector es 0 ó 1, entonces ya está ordenado. De otro modo, divide el vector en dos sub-vectores de aproximadamente la mitad del tamaño. Luego ordena cada sub-vector de manera recursiva aplicando el ordenamiento por mezcla. Por último mezcla los dos sub-vectores en un solo vector ordenado. Este método también se utiliza para ordenamientos externos. El siguiente modelo implementa un ordenamiento de tipo interno. El método interno utiliza una copia del vector en cada instancia del procedimiento (en forma recursiva). La cantidad máxima de instancias utilizadas es igual a Sup (log 2 N ) (Número entero superior o igual al logaritmo en base dos de la cantidad de elementos), por lo que la cantidad de memoria máxima utilizada es de T (r ) × N × Sup(log 2 N ) (Donde T(r) es el tamaño en bytes de un registro). Por ejemplo, para el caso de 1000 elementos, tenemos hasta diez instancias, y para el caso de 30000 elementos necesitaremos quince. Es decir, mientras los demás métodos utilizan solamente el vector original, Merge Sort utilizará el vector original más los vectores auxiliares creados en cada instancia activa en memoria. Por lo que el consumo de memoria es considerablemente mayor al resto de los métodos. procedure merge_sort(var vec:tv_int; min,max:integer); var med:integer; i,j,k:integer; aux:tv_int; begin if max > min then begin med := (min + max) div 2; merge_sort(vec,min,med); merge_sort(vec,med+1,max); for i := min to med do aux[i] := vec[i]; for j := med + 1 to max do aux[max+med-j+1] := vec[j]; i := min; j := max; for k := min to max do if aux[i] < aux[j] then begin vec[k] := aux[i]; inc(i); end else begin vec[k] := aux[j]; dec(j); end; end; end;
Por Martín Lebuchorskyj
All right reserved
Página 12
Algoritmos y Programación I
Cátedra Ing.Guarna
1.3. Estabilidad Los métodos de ordenamiento también pueden clasificarse según su estabilidad en: • Algoritmos estables • Algoritmos inestables Los algoritmos estables mantienen un relativo pre-orden total. Es decir, que si existen dos elementos A y B con la misma clave en el vector original y aparecen en el orden A y luego B, un algoritmo estable asegura que en el nuevo vector ordenado aparecerán los elementos A y B (idénticos en su clave) en el mismo orden A y luego B. Cuando elementos con la misma clave son indistinguibles entre sí, como números enteros, la estabilidad no representa un problema. Los métodos estudiados en este documento presentan la siguiente clasificación: Método Burbujeo (caso 1) Burbujeo (caso 1 optimizado) Burbujeo (caso 2) Inserción directa Inserción binaria Selección Stupid Sort Gnome Sort Shake Sort Shell Sort Comb Sort Heap Sort Quick Sort (*) Stooge Sort (*) Merge Sort (*)
Estable √ √ √ √ √
Inestable
√ √ √ √ √ √ √ √ √ √
1.4. Eficiencia La eficiencia de un método de ordenamiento puede medirse por los siguientes indicadores: • C(N) = Número de comparaciones • M(N) = Número de movimientos de los elementos Estos indicadores obtienen su valor en función del número de elementos a ordenar N. Se dice que un buen algoritmo de ordenamiento debe estar en el orden de N × log 2 N comparaciones.
1.5. Análisis fáctico de la eficiencia Caso de prueba Se realizó la prueba empírica de todos estos métodos de ordenamiento interno utilizando un vector de 1000 posiciones. Dichas posiciones fueron cargadas con un número al azar entre 0 y 999.
Por Martín Lebuchorskyj
All right reserved
Página 13
Algoritmos y Programación I
Cátedra Ing.Guarna
Se utilizaron tres series aleatorias distintas, repitiéndolas de manera idéntica (la misma serie) en cada método de ordenamiento. Luego se calculó el promedio entre las tres pruebas para cada método, obteniendo los siguientes resultados: Resultados Método Burbujeo (caso 1) Burbujeo (caso 1 optimizado) Burbujeo (caso 2) Inserción directa Inserción binaria Selección Stupid Sort Gnome Sort Shake Sort Shell Sort Comb Sort Heap Sort Quick Sort (*) Stooge Sort (*) Merge Sort (*) C M s (*) 0,0 0,0
= = = = = =
C 500 499 500 245 9 500 108855 490 326 59 22 17 6 64570 10
M 244 244 179 244 244 1 244 244 244 7 4 9 3 227 10
C+M 744 743 679 490 253 500 109099 734 571 66 26 26 9 64797 20
s(C) 0,0 0,6 0,0 5,1 0,0 0,0 3001,2 10,1 6,4 1,0 0,0 0,0 0,1 0,0 0,0
s(M) 5,1 5,1 3,1 5,1 5,1 0,0 5,1 5,1 5,1 0,3 0,2 0,0 0,0 4,2 0,0
Número de comparaciones promedio (en miles) Número de movimientos promedio (en miles) Desviación estándar del valor para las tres muestras Método recursivo Desviación menor a cien Desviación cero (sin diferencia alguna en ninguno de los casos)
Observaciones Del cuadro se desprenden las siguientes observaciones: 1) El valor de N × log 2 N = 1000 × log 2 1000 = 9966
2)
3)
4)
5)
comparaciones indica el
orden de comparaciones que debe esperarse de un buen algoritmo. El único algoritmo que está por debajo de este indicador es el Quick Sort (con algo más de 6000 comparaciones). Merge Sort prácticamente lo iguala, mientras que Heap Comb y Shell Sort presentan un orden aceptable. El método de inserción binaria presentó muy pocas comparaciones. Más aún, presentó una cantidad del orden de aceptabilidad mencionado. Esto se debe a que utiliza la búsqueda binaria para localizar el lugar donde debe insertar el nuevo elemento a tratar. Sin embargo presentó una gran cantidad de movimientos por lo que, en líneas generales, no se lo puede considerar entre los mejores métodos de ordenamiento. El método de selección fue el que presentó la menor cantidad de movimientos. Esto se debe a que cada movimiento deja el elemento en su lugar definitivo. Sin embargo el número de comparaciones es alto, por lo que tampoco se lo puede considerar entre los mejores métodos. Stupid Sort y Stooge Sort son métodos extremadamente lentos debido al gran número de comparaciones que realizan. Stupid Sort realiza casi el doble de comparaciones que Stooge Sort, y éste último más de 100 veces más comparaciones que los métodos de burbuja, y 10000 veces más que Quick Sort. Entre otras cosas se pudo verificar la justicia en el nombre de ambos algoritmos. Los casos de desviación cero presentaron el mismo valor independientemente del grado de orden inicial, incluso aplicados sobre un vector inicialmente ordenado. Por ejemplo, los métodos de Selección y
Por Martín Lebuchorskyj
All right reserved
Página 14
Algoritmos y Programación I
Cátedra Ing.Guarna
Merge realizaron la misma cantidad de comparaciones y movimientos tanto cuando trabajaron con vectores desordenados como ordenados. En el caso del método de selección, esto permite identificar un punto de mejora, ya que podría evitarse el intercambio cuando el menor elemento encontrado es el mismo con el que fue comparado (ver el algoritmo citado, cuando i = low) 6) Se debe tener en cuenta que estos datos resultan de aplicar los métodos a vectores cargados con valores aleatorios, es decir, con un alto grado de desorden, y que valdría la pena repetir la prueba sobre vectores levemente desordenados, como sucede por ejemplo cuando se añade un nuevo valor aleatorio al final de un vector ordenado y luego se lo vuelve a ordenar. Conclusiones Los mejores métodos de ordenamiento resultaron ser: • Quick Sort • Merge Sort • Heap Sort • Comb Sort • Shell Sort Y los peores: • Stupid Sort • Stooge Sort
1.6. Análisis fáctico del tiempo de proceso Caso de prueba Se realizó la prueba empírica de todos estos métodos de ordenamiento interno con vectores de distinta longitud N. Las posiciones del vector fueron cargadas con un número al azar entre 0 y N-1. Se realizó la medición de tiempos dentro del entorno Free Pascal, en una máquina con procesador Pentium de 1300 Mhz, sobre cada uno de los métodos utilizando el mismo vector de datos. Los tiempos obtenidos (en centésimas de segundo) son: Resultados Cuadro 1: 500 a 4000 elementos Método Burbujeo (caso 1) Burbujeo (caso 1 optimizado) Burbujeo (caso 2) Inserción directa Inserción binaria Selección Stupid Sort Gnome Sort Shake Sort Shell Sort Comb Sort Heap Sort Quick Sort (*) Stooge Sort (*) Merge Sort (*)
Por Martín Lebuchorskyj
N 500 1 27 1 78 -
1000 2 1 1 1 1 205 1 1 237 -
1500 3 4 3 1 1 2 709 2 2 710 -
2000 6 6 5 2 1 3 1720 5 4 1 2122 -
All right reserved
2500 9 10 8 3 2 4 3302 7 7 1 6366 -
3000 13 14 12 4 4 6 5766 11 9 1 6389 -
3500 17 19 17 6 5 8 9118 15 14 1 6382 -
4000 23 25 22 8 6 10 13825 20 18 1 19034 -
Página 15
Algoritmos y Programación I
Cátedra Ing.Guarna
Cuadro 2: 3500 a 30000 elementos Método Burbujeo (caso 1) Burbujeo (caso 1 optimizado) Burbujeo (caso 2) Inserción directa Inserción binaria Selección Stupid Sort Gnome Sort Shake Sort Shell Sort Comb Sort Heap Sort Quick Sort (*) Stooge Sort (*) Merge Sort (*) (*) +
= = =
3500 17 19 17 6 5 8 9118 15 14 1 6382 -
4000 23 25 22 8 6 10 13825 20 18 1 19034 -
5000 35 40 35 12 9 18 + 32 28 1 + -
10000 144 175 141 48 36 69 + 126 115 3 1 1 + 1
N 15000 320 377 314 108 83 152 + 284 255 5 1 2 + 1
20000 572 645 563 191 144 274 + 508 465 7 1 2 1 + 2
25000 893 1006 877 302 227 442 + 797 719 10 1 2 1 + 3
30000 1286 1448 1263 431 325 612 + 1132 1019 12 2 2 1 + 4
Método recursivo Tiempo menor a una centésima de segundo Tiempo mayor a dos minutos
0 a 1 segundo 1 a 5 segundos 5 a 15 segundos 15 segundos a 1 minuto Más de 1 minuto
Observaciones De ambos cuadros se desprenden las siguientes observaciones: 1) Debido a la diferencia de tiempos, y dado que no presentan ninguna ventaja respecto al resto, los métodos Stupid Sort y Stooge Sort no se recomiendan en ningún caso. Por lo que podemos descartarlos directamente, o proponerlos meramente como casos de estudio. 2) Hasta los 5000 elementos no se detectó una diferencia de tiempos apreciable entre los métodos. 3) A partir de los 10000 elementos los cinco mejores métodos (Shell, Comb, Heap, Quick, Merge Sort) presentaron una diferencia apreciable respecto del resto. 4) Merge Sort utiliza una copia del vector en cada instancia del procedimiento (en forma recursiva). La cantidad máxima de instancias utilizadas es igual a Sup (log 2 N ) (Número entero superior o igual al logaritmo en base dos de la cantidad de elementos). Por lo que la memoria máxima utilizada es de: T (r ) × N × Sup(log 2 N ) (Donde T(r) es el tamaño en bytes de un registro). Para el caso de 1000 elementos, tenemos hasta diez instancias, para el caso de 30000 elementos necesitaremos quince. Esto va en detrimento de la utilización de memoria. Para el caso estudiado en 30000 elementos, mientras los otros métodos utilizaron alrededor de 60Kb en el vector, Merge Sort necesitó aproximadamente ¡960Kb! En el caso de Free Pascal fue necesario extender la memoria Stack a 1Mb para que no se produjera un Stack Overflow. Conclusiones Los mejores métodos de ordenamiento resultaron ser: • Quick Sort (con un relativo consumo de memoria) • Heap Sort Por Martín Lebuchorskyj
All right reserved
Página 16
Algoritmos y Programación I • • •
Cátedra Ing.Guarna
Comb Sort Merge Sort (con un gran consumo de memoria) Shell Sort
Y los peores: • Stupid Sort • Stooge Sort
1.7. Conclusión derivada de las pruebas El análisis sobre la cantidad de comparaciones e intercambios, y el que resulta de la medición de tiempos para vectores de distinta longitud marcaron resultados similares: • En todos los casos Quick Sort es el método más recomendable. • Para el caso de los métodos iterativos: o Tanto Comb Sort como Heap Sort resultaron excelentes y muy competitivos respecto de Quick Sort. o Los métodos Stupid Sort y Stooge Sort no se recomiendan en ningún caso.
2. Métodos externos 2.1. Merge Implementación El método de Merge externo funciona de manera similar al Merge interno. La diferencia fundamental es que en lugar de ordenar vectores se utiliza para ordenar archivos sin importar su tamaño. Para su implementación es necesario incorporar alguno de los métodos de ordenamiento interno, ya que mientras la mezcla la realiza sobre disco, el ordenamiento de las partes se realiza en un vector. La implementación que mostraré utiliza el archivo original, un vector y un archivo destino. Del archivo original se toman registros hasta completar el vector, luego se ordena el vector y se realiza un merge entre el vector y el archivo destino, generando un nuevo archivo destino. El antiguo archivo destino se elimina, dejando el nuevo para el próximo ciclo. Ejemplo: Método Sort externo aplicado al ordenamiento de un archivo de texto conteniendo números enteros en sus líneas. Descripción: Vec es un vector de enteros, con posiciones válidas de 1 a maxv El procedimiento Merge recibe dos parámetros: • Nom_arch_i es el nombre del archivo de texto conteniendo las líneas a ordenar u origen. • Nom_arch_o es el nombre del archivo de texto donde dejará las líneas ordenadas o destino.
Por Martín Lebuchorskyj
All right reserved
Página 17
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure merge(nom_arch_i, nom_arch_o:string); var i,n:integer; num:integer; arch_i,arch_o:text; arch_aux:text; vec:tvec; begin assign(arch_i,nom_arch_i); reset(arch_i); assign(arch_o,nom_arch_o); rewrite(arch_o); close(arch_o); n := 0; while not eof(arch_i) do begin readln(arch_i,num); inc(n); vec[n] := num; if n = maxv then begin sortv(vec,n); merge_vec_arch(vec,n,nom_arch_o); n := 0; end; end; if n > 0 then begin sortv(vec,n); merge_vec_arch(vec,n,nom_arch_o); end; end;
El procedimiento SortV(vec,n) se implementa utilizando cualquiera de los métodos de ordenamiento interno, preferentemente Quick Sort. Recibe el vector en el parámetro vec y la longitud del mismo en el parámetro n. Devuelve el vector ordenado. El procedimiento Merge_vec_arch realiza el merge entre el vector y el archivo destino, generando un nuevo destino que incorpora los nuevos registros de manera ordenada.
Por Martín Lebuchorskyj
All right reserved
Página 18
Algoritmos y Programación I
Cátedra Ing.Guarna
procedure merge_vec_arch(var vec:tvec; n:integer; nom_arch:string); var arch,aux:text; i:integer; num:integer; begin assign(arch,nom_arch); assign(aux,nom_arch_aux); reset(arch); rewrite(aux); i := 1; while not eof(arch) do begin readln(arch,num); while (i <= n) and (vec[i] < num) do begin writeln(aux,vec[i]); inc(i); end; writeln(aux,num); end; while i <= n do begin writeln(aux,vec[i]); inc(i); end; close(arch); close(aux); erase(arch); rename(aux,nom_arch); end;
Eficiencia Respecto a la eficiencia de este método se puede observar lo siguiente: • La cantidad de ciclos de merge entre vector y archivo (o cantidad de llamadas al procedimiento Merge_vec_arch) es igual a la cantidad de líneas en el archivo origen (a ordenar) dividido por la longitud del vector auxiliar (redondeando a entero hacia arriba). Es decir, cuanto mayor sea el vector, menor será la cantidad de ciclos de merge. • El grado de orden o desorden del archivo original no afecta la eficiencia de la mezcla que realiza este método externo. Sólo puede tener algún impacto sobre el método de ordenamiento interno utilizado para el vector auxiliar. Sin embargo, utilizando un buen método de ordenamiento interno (como Quick Sort) estos tiempos no son significativos dentro del tiempo total de proceso, debido a que la lectura y escritura en disco llevan tiempos significativamente mayores. • La cantidad de lecturas sobre el archivo origen es igual a la cantidad de registros del mismo, es decir que no varía en función de la longitud del vector auxiliar, por lo que no puede ser mejorada. En cambio, la cantidad de lecturas y escrituras para generar el archivo destino depende de la cantidad de registros originales y de la longitud del vector. • Analizaremos la relación entre la cantidad de lecturas y escrituras totales y el total de ciclos de mezcla (o Merge).
Por Martín Lebuchorskyj
All right reserved
Página 19
Algoritmos y Programación I
Cátedra Ing.Guarna
Definimos: M = Cantidad de registros a ordenar N = Longitud del vector auxiliar C = M / N = Cantidad de ciclos de mezcla Analizando la cantidad de accesos sobre el archivo origen y destino en función de M y N, tenemos: Lecturas totales = Escrituras totales =
⎛ C ⎞ ⎜∑i⎟ ⎝ i =1 ⎠ × M C ⎛ C ⎞ ⎜∑i⎟ ⎝ i =1 ⎠ aumenta al aumentar C. El valor del término C La cantidad de escrituras y lecturas depende directamente de este término, por lo que podemos ver como aumentan dichas cantidades al aumentar la cantidad de ciclos para una misma cantidad de de registros iniciales C = M / N. Es decir, al disminuir la longitud del vector auxiliar N. Conclusiones La menor cantidad posible de lecturas y escrituras en disco se produce cuando la cantidad de elementos del vector auxiliar es igual o mayor a la cantidad de registros del archivo origen. Si no podemos determinar dicha cantidad, la solución más eficiente consiste en definir el vector auxiliar del mayor tamaño posible. Es importante destacar que éste método puede ser utilizado tanto para archivos de texto (puramente secuenciales) como binarios (de acceso tanto directo como secuencial). Ya que el método utiliza solamente el acceso secuencial.
2.2. Transformando métodos internos Algunos métodos internos pueden ser utilizados como externos. Esto requiere el reemplazo completo del manejo de vectores por su equivalente en el manejo de archivos. Consideraciones Cuando transformamos un método interno en externo hay varios puntos importantes a considerar: 1) La velocidad de acceso sobre los elementos de un vector es la misma independientemente de la posición de los elementos. 2) La velocidad de acceso sobre los elementos de un archivo es distinta cuando se accede de manera secuencial o de manera directa. También cambia de acuerdo a la posición del elemento al que se debe acceder. 3) Es importante tener en cuenta que estos métodos se basan en el acceso directo (debido a que fueron creados para ordenar elementos de un vector), y al ser aplicados sobre archivos necesitan poder acceder a éstos de manera
Por Martín Lebuchorskyj
All right reserved
Página 20
Algoritmos y Programación I
Cátedra Ing.Guarna
directa. Por esta razón no pueden ser aplicados a archivos de texto (puramente secuenciales) y sólo se aplican a archivos binarios. 4) Por otro lado, también es importante destacar que estos métodos no necesitan archivos auxiliares ya que toda modificación se realiza sobre el archivo original. Esto constituye una ventaja sobre el método de Merge, ya que en éste último, parte del tiempo se consume en extender archivos (agregando nuevos registros al final del archivo), crear, borrar y renombrar los archivos auxiliares. Ejemplo: Bubble Sort externo El siguiente es un ejemplo de la transformación del método de burbujeo estándar (interno) a externo: procedure bubble_sort(nom_arch:string); var arch:ta_data; i,j,n:longint; rec_j1,rec_j2:tr_data; begin assign(arch,nom_arch); reset(arch); n := filesize(arch)-1; for i := 1 to n-1 do for j := 0 to n-i do begin seek(arch,j); read(arch,rec_j1); inc(i_rs); seek(arch,j+1); read(arch,rec_j2); inc(i_rs); if rec_j1.key > rec_j2.key then begin seek(arch,j); write(arch,rec_j2); inc(i_ws); seek(arch,j+1); write(arch,rec_j1); inc(i_ws); end; end; close(arch); end;
Ejemplo: Quick Sort externo El siguiente es un ejemplo de la transformación del método Quick Sort estándar (interno) a externo. Con el procedimiento principal: procedure quick_sort(nom_arch_i:string); var arch:ta_data; begin assign(arch,nom_arch_i); reset(arch); quick_sort_rec(arch,0,filesize(arch)-1); close(arch); end;
Por Martín Lebuchorskyj
All right reserved
Página 21
Algoritmos y Programación I
Cátedra Ing.Guarna
Y el procedimiento recursivo: procedure quick_sort_rec(var arch:ta_data; low,high:longint); var i,j:longint; piv:longint; aux:tr_data; rec_low,rec_high:tr_data; rec_i,rec_j:tr_data; begin i := low; j := high; seek(arch,i); read(arch,rec_i); inc(i_rs); seek(arch,j); read(arch,rec_j); inc(i_rs); piv := (rec_i.key + rec_j.key) div 2; repeat while rec_i.key < piv do begin inc(i); seek(arch,i); read(arch,rec_i); inc(i_rs); end; while rec_j.key > piv do begin dec(j); seek(arch,j); read(arch,rec_j); inc(i_rs); end; if i <= j then begin aux := rec_j; rec_j := rec_i; rec_i := aux; seek(arch,i); write(arch,rec_i); inc(i_ws); seek(arch,j); write(arch,rec_j); inc(i_ws); inc(i); seek(arch,i); read(arch,rec_i); inc(i_rs); dec(j); seek(arch,j); read(arch,rec_j); inc(i_rs); end; until i > j; if low < j then quick_sort_rec(arch,low,j); if high > i then quick_sort_rec(arch,i,high); end;
Por Martín Lebuchorskyj
All right reserved
Página 22
Algoritmos y Programación I
Cátedra Ing.Guarna
2.3. Análisis fáctico de acceso a disco y tiempo de proceso Caso de prueba Se realizó la prueba empírica de los tres métodos externos expuestos: • Bubble Sort externo • Quick Sort externo • Merge Sort (Con Quick Sort interno para ordenar el vector auxiliar) Los métodos fueron probados con archivos binarios de distinta longitud R. Se definió un registro conteniendo un entero largo (Longint). Los registros fueron cargados con números al azar entre 0 y 999999. Se realizó la medición de tiempos dentro del entorno Free Pascal, en una máquina con procesador Pentium de 1300 Mhz, sobre cada uno de los métodos utilizando el mismo archivo de datos. Para el caso de Merge Sort se probó el mismo archivo utilizando distintos tamaños para el vector auxiliar, que dieron mezclas de 1, 2, 5, 10 y 100 ciclos de mezcla. Se midió tanto el tiempo de procesamiento (en centésimas de segundo), como la cantidad de accesos de lectura y escritura. Resultados R
Método Bubble Sort T Re Wr Quick Sort T Re Wr Merge (1) N T Re/Wr Merge (2) N T Re/Wr Merge (5) N T Re/Wr Merge (10) N T Re/Wr Merge (100) N T Re/Wr R T N Re Wr Re/Wr K M (*) nnn
= = = = = = = = = = =
100
500
1000
2500
5K
10K
100K
1M
3 9898 5232
78 249K 126K
305 999K 481K
1902 6247K 3078K
7609 24995K 12376K
30660 99990K 49980K
-
-
0 924 348
2 6214 2158
4 13983 4734
11 36976 13800
22 80321 29720
48 172K 65K
608 2101K 820K
7379 24599K 10282K
100 0 100
500 1 500
1000 1 1000
2500 3 2500
5K 6 5000
10K 14 10K
100K 106 100K
1M (*) -
50 1 150
250 1 750
500 2 1500
1250 4 3750
2500 9 7500
5K 17 15K
50K 161 150K
500K 1598 1500K
20 1 300
100 2 1500
200 6 3000
500 8 7500
1000 20 15000
2000 33 30K
20K 317 300K
200K 3211 3000K
10 2 550
50 5 2750
100 7 5500
250 16 13750
500 30 27500
1000 59 55K
10K 604 550K
100K 5861 5500K
-
-
10 66 50500
25 149 126250
50 284 252500
100 550 505K
1000 5323 5050K
10K 53261 50500K
Cantidad de registros a ordenar Tiempo (en centésimas de segundo) Longitud del vector auxiliar (en cantidad de posiciones) Cantidad de lecturas en disco Cantidad de escrituras en disco Cantidad de lecturas en disco = Cantidad de lecturas en disco x 1000 (miles) x 1000000 (millones) Escenario no probado Free Pascal no soportó el tamaño del vector Escenarios con tiempos entre 0.5 y 5 segundos
Por Martín Lebuchorskyj
All right reserved
Página 23
Algoritmos y Programación I
Cátedra Ing.Guarna
Observaciones Del cuadro se desprenden las siguientes observaciones: 1) Bubble Sort vs Quick Sort: a. Mientras Bubble Sort demoró unos 3 segundos en ordenar 1000 registros, Quick Sort lo hizo en 4 centésimas de segundo. b. Bubble Sort demoró poco más de 5 minutos en ordenar 10000 registros, mientras Quick Sort realizó el mismo trabajo en poco menos de medio segundo. c. La diferencia de tiempos entre ambos métodos es abrumadora. 2) Quick Sort vs Merge Sort: a. Los tiempos del Merge Sort varían considerablemente en relación directa a la cantidad de ciclos de mezcla, es decir, en relación inversa de la longitud del vector auxiliar. b. Con 1000 registros Quick Sort demora 4 centésimas de segundo, mientras Merge puede demorar entre 1 y 66 centésimas (con un vector que varía su longitud de 1000 a 10). c. Los tiempos de ambos métodos son comparables cuando el Merge utiliza entre 5 y 10 ciclos de mezcla. Sin embargo, a medida que la cantidad de registros a ordenar aumenta, Merge Sort acepta una mayor cantidad de ciclos para equiparar el tiempo de Quick Sort. d. Resulta notable que en las pruebas donde los tiempos son comparables, Quick Sort realiza una mayor cantidad de lecturas y escrituras que Merge. Esto sucede debido a que parte del tiempo ahorrado por Merge en lecturas y escrituras se consume en la creación, extensión, borrado y renombrado de archivos. e. Cuando la cantidad de registros a ordenar es baja, Merge Sort permite tiempos menores debido a que resulta relativamente sencillo disminuir la cantidad de ciclos de mezcla (ya que es posible utilizar vectores de tamaño cercano al del archivo). Esto podría dejar de ser válido si los registros tienen un tamaño suficiente como para no disponer de la memoria adecuada para el vector auxiliar. f. Cuando la cantidad de registros es grande, Merge presenta buenos tiempos incluso con vectores más chicos (mayor cantidad de ciclos de mezcla). Nuevamente, esto dependerá de cuántas posiciones se puedan reservar para el vector auxiliar, que a su vez depende en gran medida del tamaño de cada registro. Conclusiones 1) Para el ordenamiento de archivos de texto, el único método aceptable y a la vez con buena performance es Merge Sort. 2) Cuando el archivo a ordenar es binario, los mejores métodos resultan ser: a. Quick Sort externo b. Merge Sort (con un vector auxiliar de tamaño máximo) 3) Siendo Quick Sort el mejor método interno, al transformarlo en externo es prácticamente el único que puede competir con Merge Sort. 4) En ningún caso se recomienda adaptar y utilizar Bubble Sort, o cualquiera de los métodos internos con una eficiencia similar a éste.
Por Martín Lebuchorskyj
All right reserved
Página 24