¿Qué es una explicación intuitiva de la complejidad del tiempo de ejecución del algoritmo?

No sé qué explicación encontrarás más intuitiva, pero haré lo mejor que pueda.

La complejidad asintótica del tiempo de ejecución es una forma de aumentar el tiempo de ejecución de un algoritmo.

¿Por qué necesitamos una forma de correr el tiempo?

¿No podríamos medir el tiempo de ejecución exacto? Bueno, podríamos, pero requeriría conocer la arquitectura exacta de la computadora en la que se ejecutará el algoritmo.

No puede comparar los tiempos exactos de ejecución de dos algoritmos en una entrada dada sin conocer la arquitectura exacta en la que se ejecutarán los algoritmos. Por ejemplo, puede ocurrir que el Algoritmo A se ejecute más rápido que el Algoritmo B en la arquitectura Intel x86 para una entrada determinada, pero el Algoritmo B se ejecuta más rápido que el Algoritmo A en la arquitectura MIPS para esa misma entrada .

No hay forma de saber qué algoritmo será más rápido para una entrada específica, sin conocer la arquitectura informática exacta que se utilizará. Incluso si se limita a considerar solo una familia de arquitectura específica, digamos Intel x86, los modelos de procesador cambian lo suficiente de una generación a la siguiente que no puede saber si su algoritmo seguirá funcionando más rápido en una entrada específica.

Por ejemplo, en los viejos tiempos, las instrucciones de multiplicación solían ser considerablemente más lentas que las instrucciones de adición en x86, pero esto ya no es cierto en los procesadores x86 de la generación actual.

Además, realmente no hay una forma práctica de medir el tiempo de ejecución exacto sin hacer puntos de referencia reales del código que se ejecuta en metal desnudo. Hay muchas cosas que pueden influir en el tiempo de ejecución: las instrucciones de CPU utilizadas, el orden de las instrucciones (canalización), aciertos y errores de caché, sin mencionar las optimizaciones del compilador.

En última instancia, no existe una forma analítica práctica de comparar el tiempo de ejecución de los algoritmos en una entrada específica, pero podemos analizar los algoritmos de una manera más general al observar el tiempo de ejecución de un algoritmo en función de su entrada y comparar las funciones para diferentes algoritmos

¿Cómo comparamos funciones?

La forma más natural de comparar funciones es determinar cuál crece más rápido. Si has estudiado Cálculo, entonces esto debería ser muy familiar para ti. De hecho, realmente necesita cálculo para comprender completamente el tiempo de ejecución asintótico.

Si tienes dos funciones f y g, decimos que f crece más rápido que g (escrito f >> g) si en algún momento, f toma gy permanece dominante.

Puede obtener una intuición para esto graficando tanto f como g en una calculadora gráfica, aunque tenga cuidado porque esto puede llevarlo a conclusiones erróneas. Por ejemplo, mira este gráfico:

Parece que la línea azul supera a la línea púrpura. Sin embargo, si nos alejamos, la historia cambia:

Aquí vemos que en x = 10, la línea púrpura sobrepasa a la línea azul nuevamente. Resulta que la línea púrpura seguirá siendo dominante, y la línea azul nunca volverá a superar a la línea púrpura. La forma en que sabemos esto es mediante el uso de la herramienta de límites de Cálculo.

Si todo esto le suena extraño, entonces realmente necesita estudiar Cálculo.

Vincular esto a la informática

La forma estándar de hacer un análisis asintótico de algoritmos es hacer lo siguiente:

  1. Tome un parámetro de la entrada del algoritmo que queremos ver para considerar cómo cambiará el tiempo de ejecución del algoritmo con respecto a ese parámetro.
  2. A partir del algoritmo, calcule una función que se aproxima al tiempo de ejecución del algoritmo con respecto a ese parámetro. La aproximación cruda suele ser solo para contar el número de instrucciones que realiza el algoritmo. Esta función estará desactivada por un factor constante del tiempo de ejecución real en una arquitectura informática determinada.
  3. Para dos algoritmos diferentes, estamos comparando A y B, digamos que hemos calculado el tiempo de ejecución aproximado aproximado de A como f (x) y el tiempo de ejecución aproximado aproximado de B como g (x), ambos en términos del mismo parámetro x de la entrada. Ahora, si sabemos que no importa qué constante multipliquemos g (x), vemos que f (x) aún crece más rápido, entonces eso implica lo siguiente:

    Independientemente de la arquitectura informática que elijamos, el Algoritmo A es más rápido que el Algoritmo B en un número infinito de entradas. Por el contrario, para cualquier arquitectura de computación dada, solo hay un número finito de entradas donde el Algoritmo B podría ser más rápido que el Algoritmo A.

    Entonces eso parece implicar que el Algoritmo A es mejor que el Algoritmo B, ¿no es así? Al menos en teoría.

    Tenga en cuenta que estamos asumiendo que solo un factor constante diferencia una arquitectura informática de otra. Esto suele ser cierto para las arquitecturas de Von Neumann, pero puede no ser cierto para todas las arquitecturas.

La complejidad del tiempo de ejecución es una forma muy burda de pensar sobre un algoritmo. Simplemente le dice cómo se espera que el tiempo de ejecución (o los requisitos de espacio) escalen con el tamaño de los datos procesados. Esto se expresa en notación ‘Big-O’. O simplemente significa ‘Orden’, que en este caso significa ‘una estimación muy aproximada del tiempo / espacio dada una cantidad de elementos de datos para procesar, generalmente denotada por n

Entonces, un algoritmo lineal como encontrar el valor más grande en una matriz es O ( n ), lo que significa que el algoritmo tiene que mirar cada elemento de datos una vez para asegurarse de que ha encontrado el más grande. Si el número de artículos se duplica, el tiempo necesario para encontrar el más grande también se duplica.

Si tenemos un algoritmo que funciona al mismo tiempo, independientemente de la cantidad de elementos, lo llamamos O (1) o tiempo constante. Este es un ideal, porque significa que el algoritmo no está limitado por el tamaño de los datos. A veces podemos organizar previamente los datos para lograr esto, por ejemplo, usando una tabla hash perfecta para localizar un elemento de datos.

Muchos algoritmos terminan necesitando tiempo cuadrático, o [matemática] O (n ^ 2) [/ matemática]. Esto ocurre cuando para cada elemento de una matriz, por ejemplo, necesitamos mirar todos los demás elementos de la matriz. Un ejemplo es el algoritmo de clasificación de burbujas. Naturalmente, esto se considera pobre, porque el tiempo aumenta con el cuadrado de los elementos a procesar. Eso podría significar que el algoritmo se volverá inutilizable con bastante rapidez si los datos crecen. A menos que podamos estar seguros de que nuestros datos siempre serán pequeños, o no hay alternativa, a los programadores les gusta evitar algoritmos como este (o [matemática] n ^ 3 [/ matemática] o [matemática] n ^ c [/ matemática])

Uno de los peores con los que podríamos terminar es O (n!) (N factorial). Esto es similar a [matemáticas] O (n ^ n) [/ matemáticas] El problema del vendedor ambulante es de este orden si solo usamos una búsqueda de fuerza bruta. El problema de TS es encontrar la ruta más corta para visitar cada ciudad en un recorrido sin volver a visitar una ciudad y terminar en la ciudad inicial. El enfoque de la fuerza bruta es determinar cada combinación del orden de las ciudades, medir la distancia total de cada una y luego elegir la más pequeña. n es el número de ciudades. Para algunas ciudades, el enfoque de la fuerza bruta es probablemente viable, pero muy rápidamente se vuelve cada vez más inviable a medida que se agregan ciudades.

Estos son solo algunos básicos que son bastante fáciles de intuir si piensa en algunos de los algoritmos que exhiben estos comportamientos. Los más complejos como O (log n), que se exhibe mediante una búsqueda binaria en una lista ordenada, también son razonablemente fáciles de intuir si comprende el algoritmo y aprecia que, en todos los casos, está implícito el registro de base 2. Este es un buen rendimiento, porque el registro de un número crece mucho más lentamente que el número mismo.

Si necesitamos realizar algo equivalente a una búsqueda binaria, pero a la vez en cada elemento de una lista, terminamos con O (n log n), que se exhibe en varios algoritmos de ordenación.

Oh, este es probablemente más fácil de lo que esperas.

La complejidad de tiempo de un algoritmo es la cantidad de tiempo que necesita para resolver un problema de un tamaño determinado.

¡Eso es todo al respecto! 🙂 Realmente!

Sin embargo, si intenta analizar la complejidad del tiempo de un algoritmo, se encuentra con problemas, porque no todas las computadoras son igual de rápidas. Por lo tanto, no puede decir “este algoritmo resolverá problemas de tamaño 1000 en exactamente un segundo”, solo puede decir, “este algoritmo resolverá este problema exacto de tamaño 1000 en exactamente un segundo en tal y tal máquina”. Lo cual no es muy útil para saber.

Entonces, todo el negocio complicado con notación big-O, etc., es hacer declaraciones generalizables sobre uh, cuánto tiempo necesita un algoritmo para resolver un problema de un tamaño determinado.

Piense en el entrenamiento de circuito, un tipo de régimen de ejercicio en el que tiene muchos ejercicios diferentes que hacer, y hace uno por un minuto más o menos, luego pase al siguiente.

Nuestro programa es toda la rutina. Digamos que hay 5 ejercicios diferentes que debes hacer, y estamos haciendo una serie por ejercicio, y cada serie es de un minuto. Por lo tanto, tiene un entrenamiento de cinco minutos con 5 ejercicios diferentes, realizados de forma lineal (cada ejercicio se realiza una vez, y hay un orden).

Ahora consideremos que queremos hacer más series. Si todavía estamos haciendo 5 ejercicios, pero haciendo dos series de cada uno, y dejamos n = 5, ahora estamos haciendo 2n = 10 ejercicios. Esto es como un bucle que atravesamos dos veces.

Manteniendo n = 5, ahora queremos hacer cada ejercicio n veces, o la cantidad de veces que hay ejercicios. Esto significa que estamos haciendo n ^ 2 conjuntos, o 25 conjuntos en total.

Hay más formas en que podemos aumentar la cantidad de series / ejercicios que estamos haciendo. Considere que comenzamos con un conjunto y un ejercicio. Cada vez que completamos las series de un ejercicio, agregamos un nuevo ejercicio y duplicamos el número de series, hasta que alcancemos un número predeterminado de ejercicios que estamos haciendo.

La idea de la complejidad del tiempo de ejecución es esencialmente comprender cuántas operaciones está realizando un algoritmo / programa (por simplicidad, suponga que todas las operaciones toman aproximadamente la misma cantidad de tiempo), lo que a menudo se reduce a analizar cosas como bucles.