3A. Reporte escrito. Experimentos y análisis de algoritmos de ordenamiento.
Author
José Alberto Villegas Díaz Disciplina.
Lista de cambios:
Sin cambios. Versión original.
Introducción.
El ordenamiento es el proceso computacional de reorganizar una secuencia dada de elementos en orden ascendente o descendente. (Estivill-Castro & Wood, 1992).
En este reporte, se implementarán y analizarán varios algoritmos clásicos de ordenamiento por comparación, a saber: Heapsort, Mergesort, Quicksort, y Bubblesort. Cada uno de estos algoritmos se caracteriza por distintas complejidades de tiempo y estrategias de partición de los datos, lo que los hace más o menos eficientes según las características del conjunto de datos a ordenar.
Para hacerlo se usarán diferentes archivos proporcionados cuyos niveles de desorden son variables. Se realizará la métrica del número de comparaciones así como del tiempo necesario para ordenarlos.
El análisis se llevará a cabo utilizando archivos de entrada con niveles de desorden variados, lo que permitirá observar cómo se comportan los algoritmos en distintos tipos de distribuciones de los datos. Se emplearán tres métricas principales para evaluar el desempeño de cada algoritmo: el número de comparaciones realizadas, el número de intercambio y el tiempo total de ejecución.
Método heapsort.
De acuerdo con Cormen et al.(2022) Heapsort introduce el uso de una estructura de datos llamada heap (montículo), que no solo es útil para el algoritmo de ordenación. El algoritmo funciona construyendo el heap a partir de la lista y extrae repetidamente el elemento máximo (o mínimo) del montículo para construir la lista ordenada. Una de sus ventajas respecto a otros algoritmos de ordenamiento por comparación, es que realiza el mismo in situ lo que significa que solo requiere un número constante de elementos fuera del array de entrada en cualquier momento (Cormen et al. 2022).
Complejidad temporal:
\(O(n \log n)\) en todos los casos.
Esto significa que sin importar el orden preexistente en los arreglos, su complejidad permanecerá constante.
usingJSONusingDates# Función para ajustar el montículo (heapify) - Índices basados en 1functionheapify!(arr, n, i, counters) largest = i left =2* i # Hijo izquierdo (ajustado para base 1) right =2* i +1# Hijo derecho (ajustado para base 1)# Comparar con el hijo izquierdo counters[:comparisons] +=1if left <= n && arr[left] > arr[largest] largest = leftend# Comparar con el hijo derecho counters[:comparisons] +=1if right <= n && arr[right] > arr[largest] largest = rightend# Si el mayor no es la raíz, intercambiar y seguir ajustandoif largest != i arr[i], arr[largest] = arr[largest], arr[i] counters[:swaps] +=1heapify!(arr, n, largest, counters)endend# Implementación de HeapSort - Índices basados en 1functionheapsort!(arr) n =length(arr) counters =Dict(:comparisons =>0, :swaps =>0) # Contador de operaciones# Construir el max-heapfor i in n ÷2:-1:1heapify!(arr, n, i, counters)end# Extraer elementos del heap uno por unofor i in n:-1:2 arr[1], arr[i] = arr[i], arr[1] # Mover la raíz al final counters[:swaps] +=1heapify!(arr, i -1, 1, counters)endreturn countersend# Función para cargar y ordenar el archivo JSON con múltiples clavesfunctioneval_heap(p::String)# Construir la ruta del archivo con el argumento p ruta_archivo =joinpath(homedir(), raw"C:\Users\josea\Downloads\listas-posteo-con-perturbaciones", "listas-posteo-con-perturbaciones-p="* p *".json")# Cargar el archivo JSON datos = JSON.parsefile(ruta_archivo) contador =0 contador2=0# Procesar cada clave del JSONfor (clave, arr) in datos counters =heapsort!(arr) contador += counters[:comparisons] contador2 += counters[:swaps]endprintln("Comparaciones: ", contador)println("Intercambios: ", contador2)end
eval_heap (generic function with 1 method)
usingJSONusingDates# Función para ajustar el montículo (heapify) - Índices basados en 1functionheapify!(arr, n, i, counters) largest = i left =2* i # Hijo izquierdo (ajustado para base 1) right =2* i +1# Hijo derecho (ajustado para base 1)# Comparar con el hijo izquierdo counters[:comparisons] +=1if left <= n && arr[left] > arr[largest] largest = leftend# Comparar con el hijo derecho counters[:comparisons] +=1if right <= n && arr[right] > arr[largest] largest = rightend# Si el mayor no es la raíz, intercambiar y seguir ajustandoif largest != i arr[i], arr[largest] = arr[largest], arr[i] counters[:swaps] +=1heapify!(arr, n, largest, counters)endend# Implementación de HeapSort - Índices basados en 1functionheapsort!(arr) n =length(arr) counters =Dict(:comparisons =>0, :swaps =>0) # Contador de operaciones# Construir el max-heapfor i in n ÷2:-1:1heapify!(arr, n, i, counters)end# Extraer elementos del heap uno por unofor i in n:-1:2 arr[1], arr[i] = arr[i], arr[1] # Mover la raíz al final counters[:swaps] +=1heapify!(arr, i -1, 1, counters)endreturn countersend# Función para cargar y ordenar el archivo JSON con múltiples clavesfunctioneval_heap(p::String)# Construir la ruta del archivo con el argumento p ruta_archivo =joinpath(homedir(), raw"C:\Users\josea\Downloads\listas-posteo-con-perturbaciones", "listas-posteo-con-perturbaciones-p="* p *".json")# Cargar el archivo JSON datos = JSON.parsefile(ruta_archivo) contador =0 contador2=0# Procesar cada clave del JSONfor (clave, arr) in datos counters =heapsort!(arr) contador += counters[:comparisons] contador2 += counters[:swaps]endprintln("Comparaciones: ", contador)println("Intercambios: ", contador2)end
eval_heap (generic function with 1 method)
# Ejecutar el programa para p=016@timebegineval_heap("016")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("016")end
# Ejecutar el programa para p=032@timebegineval_heap("032")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("032")end
# Ejecutar el programa para p=064@timebegineval_heap("064")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("064")end
# Ejecutar el programa para p=128@timebegineval_heap("128")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("128")end
# Ejecutar el programa para p=256@timebegineval_heap("256")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("256")end
# Ejecutar el programa para p=512@timebegineval_heap("512")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_heap("512")end
El algoritmo Mergesort se basa en una operación simple llamada mezcla (merging), que consiste en combinar dos arreglos ordenados para formar un único arreglo ordenado. Este proceso da lugar a un método recursivo sencillo: para ordenar un arreglo, se divide en dos mitades, se ordenan recursivamente ambas mitades y luego se combinan los resultados mediante la operación de mezcla (Sedgewick & Wayne, 2011). Uno de los principales atractivos de Mergesort es que garantiza ordenar cualquier arreglo de \(n\) elementos en un tiempo proporcional a \(O(n \log n)\) Sin embargo, su desventaja principal es que requiere espacio adicional proporcional a \(n\) (Sedgewick & Wayne, 2011).
Complejidad temporal:
\(O(n \log n)\) en todos los casos.
usingJSONusingDates# Función para combinar dos subarreglos ordenadosfunctionmerge!(arr, left, mid, right, counters)# Tamaños de los subarreglos n1 = mid - left +1 n2 = right - mid# Crear arreglos temporales L =copy(arr[left:left+n1-1]) R =copy(arr[mid+1:mid+n2]) i =1# Índice para L j =1# Índice para R k = left # Índice para arr# Combinar los subarregloswhile i <= n1 && j <= n2 counters[:comparisons] +=1if L[i] <= R[j] arr[k] = L[i] i +=1else arr[k] = R[j] j +=1end counters[:movements] +=1 k +=1end# Copiar elementos restantes de L, si los haywhile i <= n1 arr[k] = L[i] counters[:movements] +=1 i +=1 k +=1end# Copiar elementos restantes de R, si los haywhile j <= n2 arr[k] = R[j] counters[:movements] +=1 j +=1 k +=1endend# Implementación de MergeSortfunctionmergesort!(arr, left, right, counters)if left < right mid = (left + right) ÷2mergesort!(arr, left, mid, counters)mergesort!(arr, mid +1, right, counters)merge!(arr, left, mid, right, counters)endend# Wrapper para inicializar MergeSort con contadoresfunctionmergesort_wrapper!(arr) counters =Dict(:comparisons =>0, :movements =>0)mergesort!(arr, 1, length(arr), counters)return countersend# Cargar y ordenar el archivo JSON con múltiples clavesfunctioneval_merge(p::String)# Construir la ruta del archivo con el argumento p ruta_archivo =joinpath(homedir(), raw"C:\Users\josea\Downloads\listas-posteo-con-perturbaciones", "listas-posteo-con-perturbaciones-p="* p *".json")# Cargar el archivo JSON datos = JSON.parsefile(ruta_archivo) contador =0 contador2=0# Procesar cada clave del JSONfor (clave, arr) in datos counters =mergesort_wrapper!(arr) contador += counters[:comparisons] contador2 += counters[:movements]endprintln("Comparaciones: ", contador)println("Intercambios: ", contador2)end
eval_merge (generic function with 1 method)
# Ejecutar el programa para p=016@timebegineval_merge("016")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("016")end
# Ejecutar el programa para p=032@timebegineval_merge("032")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("032")end
# Ejecutar el programa para p=064@timebegineval_merge("064")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("064")end
# Ejecutar el programa para p=128@timebegineval_merge("128")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("128")end
# Ejecutar el programa para p=256@timebegineval_merge("256")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("256")end
# Ejecutar el programa para p=512@timebegineval_merge("512")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_merge("512")end
Quicksort es un algoritmo de ordenación ampliamente utilizado que, a pesar de tener un tiempo de ejecución en el peor caso de \(O(n^2)\) para un array de n elementos, es una de las opciones más eficientes en la práctica. Esto se debe a que su tiempo de ejecución promedio es \(O(n \log n)\), con constantes ocultas en la notación \(O(n \log n)\) muy pequeñas, lo que lo hace extremadamente rápido en la mayoría de los casos (Cormen et al. 2022). Además, Quicksort tiene la ventaja de ser un algoritmo in situ, es decir, ordena los elementos directamente en el array original sin requerir espacio adicional significativo. Esta característica lo hace especialmente adecuado para entornos de memoria virtual Funcionamiento (Cormen et al. 2022). Al igual que Mergesort, también utiliza un enfoque “divide y vencerás”. Selecciona un elemento pivote y reorganiza la lista de manera que los elementos menores que el pivote estén a su izquierda y los mayores a su derecha. Luego, repite el proceso recursivamente para las sublistas (Cormen et al. 2022).
Aunque Quicksort no es adaptativo por diseño (es decir, no aprovecha el orden existente en la entrada), se han desarrollado versiones adaptativas de Quicksort, como CKsort, que utiliza una técnica de división específica (Cook-Kim division) para mejorar su rendimiento en secuencias casi ordenadas(Estivill-Castro & Wood, 1992).
Complejidad temporal:
Peor caso:
\(O(n^2)\) ( depende del pivote)
Caso promedio:
\(O(n \log n)\)
usingJSONusingDates# Función para particionar el arreglo usando el último elemento como pivotefunctionpartition!(arr, low, high, counters) pivot = arr[high] i = low -1# Índice del elemento menorfor j in low:high-1 counters[:comparisons] +=1if arr[j] <= pivot i +=1 arr[i], arr[j] = arr[j], arr[i] counters[:swaps] +=1endend# Colocar el pivote en su posición correcta arr[i +1], arr[high] = arr[high], arr[i +1] counters[:swaps] +=1return i +1end# Implementación de QuickSortfunctionquicksort!(arr, low, high, counters)if low < high# Obtener el índice de particiónpi=partition!(arr, low, high, counters)# Ordenar las particiones recursivamentequicksort!(arr, low, pi-1, counters)quicksort!(arr, pi+1, high, counters)endend# Wrapper para inicializar QuickSort con contadoresfunctionquicksort_wrapper!(arr) counters =Dict(:comparisons =>0, :swaps =>0)quicksort!(arr, 1, length(arr), counters)return countersend# Cargar y ordenar el archivo JSON con múltiples clavesfunctioneval_quick(p::String)# Construir la ruta del archivo con el argumento p ruta_archivo =joinpath(homedir(), raw"C:\Users\josea\Downloads\listas-posteo-con-perturbaciones", "listas-posteo-con-perturbaciones-p="* p *".json")# Cargar el archivo JSON datos = JSON.parsefile(ruta_archivo) contador =0 contador2=0# Procesar cada clave del JSONfor (clave, arr) in datos counters =quicksort_wrapper!(arr) contador += counters[:comparisons] contador2 += counters[:swaps]endprintln("Comparaciones: ", contador)println("Intercambios: ", contador2)end
eval_quick (generic function with 1 method)
# Ejecutar el programa para p=016@timebegineval_quick("016")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("016")end
# Ejecutar el programa para p=032@timebegineval_quick("032")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("032")end
# Ejecutar el programa para p=064@timebegineval_quick("064")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("064")end
# Ejecutar el programa para p=128@timebegineval_quick("128")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("128")end
# Ejecutar el programa para p=256@timebegineval_quick("256")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("256")end
# Ejecutar el programa para p=512@timebegineval_quick("512")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_quick("512")end
Funcionamiento: Compara pares de elementos adyacentes y los intercambia si están en el orden incorrecto. Este proceso se repite hasta que no se necesiten más intercambios. Es un algortimo adaptativo ya que es sensible al orden preexistente del arreglo.
Complejidad temporal:
Peor caso:
\(O(n^2)\)
Mejor caso:
\(O(n)\) (si la lista ya está ordenada)
usingJSONusingDates# Implementación de BubbleSortfunctionbubblesort!(arr) n =length(arr) counters =Dict(:comparisons =>0, :swaps =>0) # Contador de operaciones swapped =falsefor i in1:n-1 swapped =falsefor j in1:n-i counters[:comparisons] +=1if arr[j] > arr[j+1] arr[j], arr[j+1] = arr[j+1], arr[j] counters[:swaps] +=1 swapped =trueendend# Si no hubo intercambios en esta pasada, el arreglo ya está ordenadoif !swappedbreakendendreturn countersend# Cargar y ordenar el archivo JSON con múltiples clavesfunctioneval_bubble(p::String)# Construir la ruta del archivo con el argumento p ruta_archivo =joinpath(homedir(), raw"C:\Users\josea\Downloads\listas-posteo-con-perturbaciones", "listas-posteo-con-perturbaciones-p="* p *".json")# Cargar el archivo JSON datos = JSON.parsefile(ruta_archivo) contador =0 contador2=0# Procesar cada clave del JSONfor (clave, arr) in datos counters =bubblesort!(arr) contador += counters[:comparisons] contador2 += counters[:swaps]endprintln("Comparaciones: ", contador)println("Intercambios: ", contador2)end
eval_bubble (generic function with 1 method)
# Ejecutar el programa para p=016@timebegineval_bubble("016")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("016")end
# Ejecutar el programa para p=032@timebegineval_bubble("032")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("032")end
# Ejecutar el programa para p=064@timebegineval_bubble("064")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("064")end
# Ejecutar el programa para p=128@timebegineval_bubble("128")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("128")end
# Ejecutar el programa para p=256@timebegineval_bubble("256")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("256")end
# Ejecutar el programa para p=512@timebegineval_bubble("512")end# Se ejecuta dos veces para no considerar el tiempo de compilación @timebegineval_bubble("512")end
De la tabla 1 podemos observar que tanto Mergesort como Heapsort, mantuvieron números de comparaciones muy similares, debemos recordar que estos dos algoritmos de ordenamiento tienen complejidades de \(O(n \log n)\) en todos los casos, por lo que sin importar el nivel de perturbación en cada archivo las comparaciones que realizó el algoritmo fueron prácticamente iguales en cada caso.
En el caso de Quicksort, su complejidad depende del pivote seleccionado, para el experimento se eligió el último elemento de cada lista de tal forma que durante la partición, el arreglo se reorganiza de manera que los elementos menores que el pivote queden a la izquierda y los elementos mayores queden a la derecha, por lo que si el arreglo está casi ordenado, el pivote será el elemento más grande. Como resultado, la partición no será eficiente y hará que la complejidad del algortimo tienda a \(O(n^2)\). Esto es verificable en la tabla de resultados donde se muestra que mientras menos desordenados estaban los arreglos el número de comparaciones fue mayor.
Finalmente para Bubblesort, se puede apreciar que de todos los algoritmos fue el que mayor número de comparaciones requirió, debemos también recordar que su complejidad varía entre \(O(n)\) y \(O(n^2)\), y al ser un algortimo adaptativo se vuelve sensible al orden preexistente en los arreglos a ordenar. Esto se puede verificar en la tabla en la que podemos apreciar la tendencia clara a aumentar el número de comparaciones conforme aumenta el desorden
Tabla 2. Número de intercambios por algoritmo de ordenamiento
La tabla 2 se puede interpretar bajo la misma lógica de la tabla anterior los algoritmos de complejidad constante \(O(n \log n)\), Heapsort y Mergesort manttuvieron valores de intercambios constantes, en particular Mergesort que mantuvo exactamente el mismo número para cada caso. Quicksort al haber elegido el último elemento como pivote muestra un descenso de los intercambios necesarios conforme las entradas son más desordenadas. Finalmente Bubblesort al ser un algoritmo adaptativo muestra un marcado incremento en el número de intercambios necesarios conforme las entradas se tienen un mayor grado de perturbación.
Tabla 3. Tiempos de ejecución por algoritmo de ordenamiento en segundos
Finalmente de la tabla 3 podemos observar que los mejores tiempos de ejecución fueron para los algortimos de complejidad constante; Heapsort y Mergesort. Quicksort mostró tiempos de ejecución de un orden superior e la mayoría de los casos, pero en los casos de mayor desorden los tiempos son comparables. Finalmente Bubblesort mostró tiempos de ejecución de hasta dos ordenes de magnitud mayores, lo que lo convierte en el algoritmo menos eficiente para este experimento.
Conclusiones
En conclusión, los resultados obtenidos y expresados en las tablas comparativas ofrecen una visión clara del rendimiento de los algoritmos evaluados en términos de comparaciones, intercambios y tiempos de ejecución. Tanto Mergesort como Heapsort mostraron un rendimiento bastante constante en cuanto al número de comparaciones, dado que son de complejidad constante \(O(n \log n)\) esto refleja su eficiencia al manejar diferentes niveles de desorden en los datos. Esta eficiencia también se refleja en los tiempos de ejecución observados en la tabla 3, donde ambos algoritmos demostraron ser los más rápidos, con tiempos de ejecución estables y comparables a lo largo de los distintos niveles de desorden.
Por otro lado, Quicksort mostró una mayor variabilidad en su número de comparaciones, especialmente cuando los arreglos estaban casi ordenados. En estos casos, debido a la elección del último elemento como pivote, la partición resultó ineficiente para los casos en los que los arreglos estaaban más ordenados, pero eficiente para aquellos más desordenados, esto también se vio reflejado en sus tiempos de ejecución. Aunque en los casos con mayor desorden, los tiempos de ejecución de Quicksort fueron comparables a los de Mergesort y Heapsort, su desempeño sigue siendo menos predecible y más dependiente del estado inicial de los datos.
Finalmente, Bubblesort fue el algoritmo que requirió el mayor número de comparaciones en todos los casos, lo que también se reflejó en sus tiempos de ejecución. En los experimentos, Bubblesort mostró tiempos de ejecución de hasta dos órdenes de magnitud mayores que los de los otros algoritmos, lo que lo convierte en el algoritmo menos eficiente de todos en este experimento.
En resumen, los resultados destacan la superioridad de Mergesort y Heapsort tanto en términos de eficiencia en el número de comparaciones como en tiempos de ejecución, mientras que Quicksort, aunque competitivo en ciertos escenarios, puede ser sensible al desorden inicial de los datos, y Bubblesort sigue siendo el menos eficiente en especial para entradas con con mucho desorden preexistente.
Bibliografía
Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2022). Introduction to Algorithms (2nd ed.). MIT Press.
Estivill-Castro, V., & Wood, D. (1992). A survey of adaptive sorting algorithms. ACM Computing Surveys, 24(4), 441-476. https://doi.org/10.1145/146370.146381
Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Pearson Education, Inc.