Estructuras de Datos 2016-10 Solución propuesta Taller 1 2.1 Factorial 1. La gráfica presenta el tiempo (en segundos) que tarda en ejecutarse la función recursiva para calcular el factorial de un número (curva roja) y la función iterativa para calcular el factorial (curva verde), utilizando un número creciente entre 0 y 160. Al analizar la gráfica, lo que puede observarse es que tanto la curva roja como la curva verde presentan el mismo comportamiento, lo que indicaría que ambas funciones tardan el mismo tiempo en ejecutarse. El tiempo de ejecución es bastante pequeño, por lo que no alcanza a marcar valores diferentes de 0 en la medición. 2. La complejidad teórica de los dos algoritmos presentados es O(n). En el caso de la función recursiva, se hacen tantos llamados recursivos a la función como el número de entrada, por lo que la complejidad es de orden lineal (O(n)). En el caso de la versión iterativa, el ciclo se ejecuta cerca de n+1 veces, lo que también representa una complejidad de orden lineal (O(n)). En relación con la evidencia experimental recopilada en la gráfica, ya se observó que los comportamientos son similares, por lo que puede decirse que el método empírico corrobora en este caso el análisis teórico. Ambas versiones (recursiva e iterativa) pueden utilizarse indistintamente, ya que su complejidad es la misma y en tiempo de ejecución tardan el mismo tiempo, independientemente de su longitud en términos de líneas de código. 3. Al calcular 170! se obtiene un valor de 7.25742e+306, pero al calcular el siguiente factorial, es decir, 171!, se obtiene inf como resultado. Esto se debe al tipo de dato utilizado para almacenar el resultado. En este caso, se está usando un double, que tiene un límite de representación hasta el valor 1.7E+/308 (15 dígitos). El valor 170! alcanza a caer dentro de este rango de representación, pero el factorial del siguiente número ya no alcanza a representarse utilizando ese tipo de dato. 2.2 Sort 1. La primera gráfica presenta el tiempo (en segundos) que tarda en ejecutarse el algoritmo de ordenamiento en burbuja (bubblesort, curva roja), el algoritmo de ordenamiento rápido (quicksort, curva verde) y el algoritmo de ordenamiento por montículos (heapsort, curva azul) sobre un arreglo de datos aleatorios, de tamaño entre 500 y 40000. La segunda gráfica presenta el tiempo (en segundos) que tarda en ejecutarse el algoritmo de ordenamiento en burbuja (bubblesort, curva roja), el algoritmo de ordenamiento rápido (quicksort, curva verde) y el algoritmo de ordenamiento por montículos (heapsort, curva azul), ahora sobre un arreglo de datos ya ordenados, de tamaño entre 500 y 40000. La última gráfica presenta el tiempo (en segundos) que tarda en ejecutarse el algoritmo de ordenamiento en burbuja (bubblesort, curva roja), el algoritmo de ordenamiento rápido (quicksort, curva verde) y el algoritmo de ordenamiento por montículos (heapsort, curva azul), ahora sobre un arreglo de datos ordenados inversamente, de tamaño entre 500 y 40000. De las tres gráficas puede observarse que el algoritmo de ordenamiento en burbuja (bubblesort) es el que más tarda en ejecutarse en las tres configuraciones de las entradas. Para cuando los datos son aleatorios, los algoritmos de ordenamiento rápido (quicksort) y por montículos (heapsort) tardan el mismo tiempo de ejecución, sin embargo, cuando los datos están ordenados ascendente o descendentemente, el algoritmo de ordenamiento rápido (quicksort) tiende a tomar mucho más tiempo que el algoritmo de ordenamiento por montículos (heapsort). Con esto se puede decir que el algoritmo de peor comportamento en tiempo es el bubblesort, mientras que el de mejor comportamiento en tiempo es el heapsort. En arreglos de tamaño “pequeño”, hasta 10000 datos, las diferencias en tiempo para los algoritmos son relativamente pequeñas, por lo que en esos casos podrían no ser necesariamente críticas. Ya en arreglos de más tamaño, las diferencias empiezan a notarse entre los 3 algoritmos. 2. El algoritmo de ordenamiento en burbuja (bubblesort) presenta una complejidad teórica de O(n^2). Esto es debido a que en el peor caso, debe hacer intercambios de cada uno de los datos con todos los demás.1 La implementación permite evidenciar que se utiliza un primer ciclo para recorrer cada dato del arreglo, y para cada uno de ellos, se utiliza un segundo ciclo para recorrer el resto de datos y revisar si es necesario el intercambio. Como cada ciclo se hace sobre todos los datos, la complejidad de cada ciclo es de O(n), y al estar anidados, su complejidad se multiplica, resultando en O(n^2). El algoritmo de ordenamiento rápido (quicksort) presenta una complejidad teórica general de O(n log n), aunque en algunos casos muy particulares puede tomar hasta O(n^2). 2 El algoritmo particiona el arreglo en dos, y recursivamente ordena cada partición. El trabajar cada vez con la mitad del arreglo implica un orden de complejidad de O(log n), pero además, en cada partición los datos se ordenan haciendo intercambios, por lo que en cada intercambio se comparan todos los datos entre sí, llegando a un orden de complejidad de O(n). La combinación de ambos procesos implica una multiplicación de los órdenes de complejidad, resultando en O(n log n). El algoritmo de ordenamiento por montículos (heapsort) presenta una complejidad teórica de O(n log n).3 El procedimiento para el ordenamiento implica crear primero un montículo con los datos (árbol binario con propiedad especial), y luego generar el arreglo ordenado a partir de la extracción controlada de los elementos del montículo. El proceso de creación del montículo y su actualización a medida que se van extrayendo los elementos para que se realice de manera ordenada implica un proceso de complejidad O(n log n). 3. De acuerdo a la complejidad teórica de los algoritmos, y el tiempo de ejecución observado en la experimentación, se puede decir que la evidencia experimental soporta correctamente los análisis teóricos de complejidad. De acuerdo al análisis teórico, bubblesort es el de peor comportamento, con complejidad O(n^2), y heapsort es el de mejor comportamiento, con complejidad O(n log n). La complejidad de quicksort oscila entre esos dos valores, y la evidencia experimental permitió identificar que dependiendo de la configuración de los datos de entrada quicksort puede ser muy rápido o puede tardar tanto como bubblesort. En la experimentación, la curva de heapsort siempre se mantuvo muy cerca al eje horizontal, siendo en todos los casos el mejor algoritmo. Y la curva de bubblesort siempre fue la de mayor crecimiento, indicando que en las tres configuraciones es el que más tiempo tarda. 2.1 Lucas 1. La gráfica presenta el tiempo (en segundos) que tarda en ejecutarse la función recursiva para calcular el número de Lucas (curva roja) y la función iterativa para calcular el número de Lucas (curva verde), 1 2 3 http://www.cs.nott.ac.uk/~psznza/G52ADS/simplesorts1.pdf https://www.khanacademy.org/computing/computer-science/algorithms/quick-sort/a/analysis-of-quicksort https://fmse.info.uaic.ro/getteachingfile/53/heap-comp.pdf utilizando un número creciente entre 0 y 50. Al analizar la gráfica, lo que puede observarse es que tanto la curva roja como la curva verde presentan el mismo comportamiento hasta cerca del número de Lucas 38, lo que indicaría que ambas funciones tardan el mismo tiempo en ejecutarse. Hasta ese punto, el tiempo de ejecución es bastante pequeño, por lo que no alcanza a marcar valores diferentes de 0 en la medición. Sin embargo, a partir del número de Lucas 39, la curva roja empieza a crecer rápidamente, mientras que la curva verde se mantiene cerca al eje horizontal. De esta forma, el algoritmo recursivo tarda bastante más tiempo en generar el resultado que el algoritmo iterativo. 2. La complejidad de los algoritmos para calcular el número de Lucas corresponde también a la complejidad del cálculo de los números de Fibonacci, pues la estructura de los algoritmos es la misma, sólo cambian los valores de inicialización. En el caso de la función recursiva, su complejidad está acotada por O(2n), aunque para mayor exactitud se suele hablar de O(1.6n).4 Para calcular un número de Lucas, se requiere tener el valor de dos números anteriores. Como para cada llamado el cálculo se duplica, y estos llamados se hacen casi tantas veces como el valor del número de entrada, es por esto que se define la complejidad como O(2n). En el caso de la versión iterativa, el ciclo se ejecuta cerca de n-1 veces, lo que representa una complejidad de orden lineal (O(n)). En relación con la evidencia experimental recopilada en la gráfica, se puede afirmar que la evidencia experimental corrobora correctamente los análisis teóricos de complejidad. El crecimiento de la función 2n es bastante poco al principio, pero luego tiene un crecimiento bastante acelerado, a comparación de la tendencia de la función lineal. De esta forma, para algunos pocos números pequeños es posible usar tanto la versión iterativa como la recursiva, pero en general es mejor utilizar la versión iterativa, pues su complejidad en tiempo es menor. Esto es independiente de la cantidad de líneas de código en la implementación de cada algoritmo, pues siempre la versión recursiva es más compacta. 4 http://stackoverflow.com/questions/360748/computational-complexity-of-fibonacci-sequence
© Copyright 2024