Análisis de algoritmos - Analysis of algorithms

Para buscar una entrada dada en una lista ordenada dado, tanto el binario y la búsqueda lineal algoritmo (que ignora el pedido) se puede utilizar. El análisis del primero y del último algoritmo muestra que se necesitan como máximo log 2 ( n ) yn pasos de verificación, respectivamente, para obtener una lista de longitud n . En la lista de ejemplo representada de longitud 33, la búsqueda de "Morin, Arthur" toma 5 y 28 pasos con búsqueda binaria (mostrada en cian ) y lineal ( magenta ), respectivamente.
Gráficos de funciones comúnmente utilizadas en el análisis de algoritmos, que muestran el número de operaciones N versus el tamaño de entrada n para cada función.

En informática , el análisis de algoritmos es el proceso de encontrar la complejidad computacional de los algoritmos: la cantidad de tiempo, almacenamiento u otros recursos necesarios para ejecutarlos . Por lo general, esto implica determinar una función que relaciona la longitud de la entrada de un algoritmo con la cantidad de pasos que toma (su complejidad temporal ) o la cantidad de ubicaciones de almacenamiento que utiliza (su complejidad espacial ). Se dice que un algoritmo es eficiente cuando los valores de esta función son pequeños o crecen lentamente en comparación con un crecimiento en el tamaño de la entrada. Diferentes entradas de la misma longitud pueden hacer que el algoritmo tenga un comportamiento diferente, por lo que las descripciones de casos mejores, peores y promedio pueden ser de interés práctico. Cuando no se especifica lo contrario, la función que describe el rendimiento de un algoritmo suele ser un límite superior , determinado a partir de las entradas del peor caso al algoritmo.

El término "análisis de algoritmos" fue acuñado por Donald Knuth . El análisis de algoritmos es una parte importante de una teoría de la complejidad computacional más amplia , que proporciona estimaciones teóricas de los recursos que necesita cualquier algoritmo que resuelva un problema computacional dado . Estas estimaciones proporcionan una idea de las direcciones razonables de búsqueda de algoritmos eficientes .

En el análisis teórico de algoritmos, es común estimar su complejidad en el sentido asintótico, es decir, estimar la función de complejidad para una entrada arbitrariamente grande. La notación Big O , la notación Big-omega y la notación Big-theta se utilizan para este fin. Por ejemplo, se dice que la búsqueda binaria se ejecuta en un número de pasos proporcionales al logaritmo de la longitud de la lista ordenada que se busca, o en O (log (n)), coloquialmente "en tiempo logarítmico ". Por lo general, se utilizan estimaciones asintóticas porque diferentes implementaciones del mismo algoritmo pueden diferir en eficiencia. Sin embargo, las eficiencias de dos implementaciones "razonables" de un algoritmo dado están relacionadas por un factor multiplicativo constante llamado constante oculta .

A veces se pueden calcular medidas exactas (no asintóticas) de eficiencia, pero generalmente requieren ciertos supuestos relacionados con la implementación particular del algoritmo, llamado modelo de cálculo . Un modelo de cálculo puede definirse en términos de una computadora abstracta , por ejemplo, una máquina de Turing , y / o postulando que ciertas operaciones se ejecutan en una unidad de tiempo. Por ejemplo, si la lista ordenada a la que aplicamos la búsqueda binaria tiene n elementos, y podemos garantizar que cada búsqueda de un elemento en la lista se puede realizar en una unidad de tiempo, entonces, como máximo , se necesitan log 2 n + 1 unidades de tiempo para devolver una respuesta.

Modelos de costos

Las estimaciones de eficiencia del tiempo dependen de lo que definamos como un paso. Para que el análisis se corresponda de manera útil con el tiempo de ejecución real, se debe garantizar que el tiempo requerido para realizar un paso esté acotado por encima de una constante. Hay que tener cuidado aquí; por ejemplo, algunos análisis cuentan la suma de dos números como un paso. Esta suposición puede no estar justificada en ciertos contextos. Por ejemplo, si los números involucrados en un cálculo pueden ser arbitrariamente grandes, ya no se puede suponer que el tiempo requerido por una sola suma sea constante.

Generalmente se utilizan dos modelos de costos:

  • el modelo de costo uniforme , también llamado medición de costo uniforme (y variaciones similares), asigna un costo constante a cada operación de la máquina, independientemente del tamaño de los números involucrados
  • El modelo de costo logarítmico , también llamado medición de costo logarítmico (y variaciones similares), asigna un costo a cada operación de la máquina proporcional al número de bits involucrados.

Este último es más engorroso de usar, por lo que solo se emplea cuando es necesario, por ejemplo, en el análisis de algoritmos aritméticos de precisión arbitraria , como los que se usan en criptografía .

Un punto clave que a menudo se pasa por alto es que los límites inferiores publicados para los problemas a menudo se dan para un modelo de cálculo que es más restringido que el conjunto de operaciones que podría usar en la práctica y, por lo tanto, hay algoritmos que son más rápidos de lo que sería ingenuamente. pensó posible.

Análisis en tiempo de ejecución

El análisis de tiempo de ejecución es una clasificación teórica que estima y anticipa el aumento en el tiempo de ejecución (o tiempo de ejecución) de un algoritmo a medida que aumenta su tamaño de entrada (generalmente denotado como n ). La eficiencia en el tiempo de ejecución es un tema de gran interés en la informática : un programa puede tardar segundos, horas o incluso años en terminar de ejecutarse, según el algoritmo que implemente. Si bien las técnicas de creación de perfiles de software se pueden utilizar para medir el tiempo de ejecución de un algoritmo en la práctica, no pueden proporcionar datos de tiempo para todas las infinitas entradas posibles; esto último solo se puede lograr mediante los métodos teóricos de análisis en tiempo de ejecución.

Deficiencias de las métricas empíricas

Dado que los algoritmos son independientes de la plataforma (es decir, un algoritmo dado se puede implementar en un lenguaje de programación arbitrario en una computadora arbitraria que ejecuta un sistema operativo arbitrario ), existen inconvenientes significativos adicionales al usar un enfoque empírico para medir el rendimiento comparativo de un conjunto dado de algoritmos.

Tomemos como ejemplo un programa que busca una entrada específica en una lista ordenada de tamaño n . Supongamos que este programa se implementó en la Computadora A, una máquina de última generación, usando un algoritmo de búsqueda lineal , y en la Computadora B, una máquina mucho más lenta, usando un algoritmo de búsqueda binaria . Las pruebas comparativas en las dos computadoras que ejecutan sus respectivos programas pueden tener el siguiente aspecto:

n (tamaño de lista) Tiempo de ejecución de la computadora A
(en nanosegundos )
Tiempo de ejecución de la computadora B
(en nanosegundos )
dieciséis 8 100.000
63 32 150.000
250 125 200.000
1.000 500 250.000

Sobre la base de estas métricas, sería fácil llegar a la conclusión de que el equipo A está ejecutando un algoritmo que es muy superior en eficiencia a la del equipo B . Sin embargo, si el tamaño de la lista de entrada aumenta a un número suficiente, se demuestra dramáticamente que esa conclusión es errónea:

n (tamaño de lista) Tiempo de ejecución de la computadora A
(en nanosegundos )
Tiempo de ejecución de la computadora B
(en nanosegundos )
dieciséis 8 100.000
63 32 150.000
250 125 200.000
1.000 500 250.000
... ... ...
1.000.000 500.000 500.000
4.000.000 2,000,000 550 000
16.000.000 8.000.000 600.000
... ... ...
63.072 × 10 12 31,536 × 10 12 ns,
o 1 año
1.375.000 ns
o 1.375 milisegundos

La computadora A, que ejecuta el programa de búsqueda lineal, exhibe una tasa de crecimiento lineal . El tiempo de ejecución del programa es directamente proporcional a su tamaño de entrada. Duplicar el tamaño de entrada duplica el tiempo de ejecución, cuadriplicar el tamaño de entrada cuadriplica el tiempo de ejecución, y así sucesivamente. Por otro lado, la computadora B, que ejecuta el programa de búsqueda binaria, exhibe una tasa de crecimiento logarítmica . Cuadriplicar el tamaño de entrada solo aumenta el tiempo de ejecución en una cantidad constante (en este ejemplo, 50.000 ns). Aunque la computadora A es aparentemente una máquina más rápida, la computadora B inevitablemente superará a la computadora A en tiempo de ejecución porque está ejecutando un algoritmo con una tasa de crecimiento mucho más lenta.

Órdenes de crecimiento

De manera informal, se puede decir que un algoritmo exhibe una tasa de crecimiento del orden de una función matemática si más allá de un cierto tamaño de entrada n , la función multiplicada por una constante positiva proporciona un límite superior o límite para el tiempo de ejecución de ese algoritmo. En otras palabras, para un tamaño de entrada dado n mayor que algún n 0 y una constante c , el tiempo de ejecución de ese algoritmo nunca será mayor que . Este concepto se expresa con frecuencia utilizando la notación Big O. Por ejemplo, dado que el tiempo de ejecución de la ordenación por inserción crece cuadráticamente a medida que aumenta su tamaño de entrada, se puede decir que la ordenación por inserción es de orden O ( n 2 ).

La notación Big O es una forma conveniente de expresar el peor de los casos para un algoritmo dado, aunque también se puede usar para expresar el caso promedio; por ejemplo, el peor de los casos para la ordenación rápida es O ( n 2 ), pero el tiempo de ejecución promedio del caso es O ( n  log  n ) .

Órdenes empíricos de crecimiento

Suponiendo que el tiempo de ejecución sigue la regla de potencia, t ≈ kn a , el coeficiente a se puede encontrar tomando medidas empíricas del tiempo de ejecución en algunos puntos del tamaño del problema , y calculando así . En otras palabras, mide la pendiente de la línea empírica en la gráfica logarítmica del tiempo de ejecución frente al tamaño del problema, en algún punto del tamaño. Si el orden de crecimiento sigue de hecho la regla de la potencia (por lo que la línea en la gráfica logarítmica es una línea recta), el valor empírico de a permanecerá constante en diferentes rangos, y si no, cambiará (y la línea es una línea curva), pero aún podría servir para comparar dos algoritmos dados en cuanto a sus órdenes empíricos locales de comportamiento de crecimiento . Aplicado a la tabla anterior:

n (tamaño de lista) Tiempo de ejecución de la computadora A
(en nanosegundos )
Orden local de crecimiento
(n ^ _)
Tiempo de ejecución de la computadora B
(en nanosegundos )
Orden local de crecimiento
(n ^ _)
15 7 100.000
sesenta y cinco 32 1.04 150.000 0,28
250 125 1.01 200.000 0,21
1.000 500 1,00 250.000 0,16
... ... ...
1.000.000 500.000 1,00 500.000 0,10
4.000.000 2,000,000 1,00 550 000 0,07
16.000.000 8.000.000 1,00 600.000 0,06
... ... ...

Se ve claramente que el primer algoritmo exhibe un orden lineal de crecimiento siguiendo la regla de la potencia. Los valores empíricos para el segundo están disminuyendo rápidamente, lo que sugiere que sigue otra regla de crecimiento y, en cualquier caso, tiene órdenes de crecimiento locales mucho más bajos (y mejorando aún más), empíricamente, que el primero.

Evaluación de la complejidad del tiempo de ejecución

La complejidad del tiempo de ejecución para el peor de los casos de un algoritmo dado a veces se puede evaluar examinando la estructura del algoritmo y haciendo algunas suposiciones simplificadoras. Considere el siguiente pseudocódigo :

1    get a positive integer n from input
2    if n > 10
3        print "This might take a while..."
4    for i = 1 to n
5        for j = 1 to i
6            print i * j
7    print "Done!"

Una computadora determinada tomará una cantidad de tiempo discreta para ejecutar cada una de las instrucciones involucradas en la ejecución de este algoritmo. La cantidad específica de tiempo para llevar a cabo una instrucción dada variará dependiendo de qué instrucción se esté ejecutando y qué computadora la esté ejecutando, pero en una computadora convencional, esta cantidad será determinista . Digamos que se considera que las acciones llevadas a cabo en el paso 1 consumen tiempo T 1 , el paso 2 utiliza el tiempo T 2 , y así sucesivamente.

En el algoritmo anterior, los pasos 1, 2 y 7 solo se ejecutarán una vez. Para una evaluación del peor de los casos, se debe suponer que también se ejecutará el paso 3. Por lo tanto, la cantidad total de tiempo para ejecutar los pasos 1-3 y el paso 7 es:

Los bucles de los pasos 4, 5 y 6 son más difíciles de evaluar. La prueba de bucle externo en el paso 4 se ejecutará ( n + 1) veces (tenga en cuenta que se requiere un paso adicional para terminar el bucle for, por lo tanto, n + 1 y no n ejecuciones), lo que consumirá T 4 ( n + 1) tiempo . El bucle interno, por otro lado, se rige por el valor de j, que itera de 1 a i . En la primera pasada a través del bucle externo, j itera de 1 a 1: el bucle interno hace una pasada, por lo que ejecutar el cuerpo del bucle interno (paso 6) consume T 6 tiempo, y la prueba del bucle interno (paso 5) consume 2 T 5 veces. Durante la siguiente pasada por el bucle exterior, j itera de 1 a 2: el bucle interior hace dos pasadas, por lo que ejecutar el cuerpo del bucle interior (paso 6) consume 2 T 6 de tiempo, y la prueba del bucle interior (paso 5) consume 3 T 5 tiempo.

En total, el tiempo total necesario para ejecutar el cuerpo del bucle interno se puede expresar como una progresión aritmética :

que se puede factorizar como

El tiempo total requerido para ejecutar la prueba del circuito externo se puede evaluar de manera similar:

que se puede factorizar como

Por lo tanto, el tiempo de ejecución total de este algoritmo es:

que se reduce a

Como regla general , se puede suponer que el término de orden más alto en cualquier función dada domina su tasa de crecimiento y, por lo tanto, define su orden de tiempo de ejecución. En este ejemplo, n 2 es el término de mayor orden, por lo que se puede concluir que f (n) = O (n 2 ). Formalmente, esto se puede probar de la siguiente manera:

Pruebalo





Sea k una constante mayor o igual que [ T 1 .. T 7 ] Por lo tanto



Un enfoque más elegante para analizar este algoritmo sería declarar que [ T 1 .. T 7 ] son ​​todos iguales a una unidad de tiempo, en un sistema de unidades elegido de modo que una unidad sea mayor o igual que los tiempos reales para estos pasos. Esto significaría que el tiempo de ejecución del algoritmo se desglosa de la siguiente manera:

Análisis de la tasa de crecimiento de otros recursos

La metodología de análisis en tiempo de ejecución también se puede utilizar para predecir otras tasas de crecimiento, como el consumo de espacio de memoria . Como ejemplo, considere el siguiente pseudocódigo que administra y reasigna el uso de memoria por parte de un programa en función del tamaño de un archivo que ese programa administra:

while file is still open:
    let n = size of file
    for every 100,000 kilobytes of increase in file size
        double the amount of memory reserved

En este caso, a medida que aumenta el tamaño del archivo n, la memoria se consumirá a una tasa de crecimiento exponencial , que es de orden O (2 n ). Esta es una tasa de crecimiento extremadamente rápida y probablemente inmanejable para el consumo de recursos de memoria .

Relevancia

El análisis de algoritmos es importante en la práctica porque el uso accidental o no intencional de un algoritmo ineficiente puede afectar significativamente el rendimiento del sistema. En aplicaciones urgentes, un algoritmo que tarda demasiado en ejecutarse puede hacer que sus resultados sean obsoletos o inútiles. Un algoritmo ineficiente también puede terminar requiriendo una cantidad antieconómica de potencia de cálculo o almacenamiento para funcionar, volviéndolo prácticamente inútil.

Factores constantes

El análisis de algoritmos generalmente se enfoca en el desempeño asintótico, particularmente en el nivel elemental, pero en aplicaciones prácticas los factores constantes son importantes y los datos del mundo real son en la práctica siempre limitados en tamaño. El límite suele ser el tamaño de la memoria direccionable, por lo que en máquinas de 32 bits 2 32 = 4 GiB (mayor si se usa memoria segmentada ) y en máquinas de 64 bits 2 64 = 16 EiB. Así, dado un tamaño limitado, un orden de crecimiento (tiempo o espacio) puede ser reemplazado por un factor constante, y en este sentido todos los algoritmos prácticos son O (1) para una constante suficientemente grande o para datos suficientemente pequeños.

Esta interpretación es principalmente útil para funciones que crecen extremadamente lentamente: el logaritmo iterado (binario) (log * ) es menor que 5 para todos los datos prácticos (2 65536 bits); (binario) log-log (log log n ) es menor que 6 para prácticamente todos los datos prácticos (2 64 bits); y el registro binario (log n ) es menor que 64 para prácticamente todos los datos prácticos (2 64 bits). No obstante, un algoritmo con complejidad no constante puede ser más eficiente que un algoritmo con complejidad constante en datos prácticos si la sobrecarga del algoritmo de tiempo constante da como resultado un factor constante mayor, por ejemplo, uno puede tener tanto tiempo como y .

Para datos grandes, los factores lineales o cuadráticos no pueden ignorarse, pero para datos pequeños, un algoritmo asintóticamente ineficiente puede ser más eficiente. Esto se usa particularmente en algoritmos híbridos , como Timsort , que usan un algoritmo asintóticamente eficiente (aquí , ordenación por fusión , con complejidad de tiempo ), pero cambian a un algoritmo asintóticamente ineficiente (aquí ordenación por inserción , con complejidad de tiempo ) para datos pequeños, como el más simple El algoritmo es más rápido en datos pequeños.

Ver también

Notas

Referencias

enlaces externos