Cuál Es El Objetivo de Un Algoritmo

13

Click here to load reader

description

Cuál Es El Objetivo de Un Algoritmo

Transcript of Cuál Es El Objetivo de Un Algoritmo

Page 1: Cuál Es El Objetivo de Un Algoritmo

¿Cuál es el objetivo de un algoritmo?

Secuencia de instrucciones cuyo objetivo es la resolución de un problema. El término clave aquí es el de problema. Existen multitud de problemas de computación que se pueden resolver mediante un algoritmo (aunque algunos pocos no tienen un algoritmo que los solucione)

Bueno... pues para resolver cada problema, podemos obtener más de un algoritmo que lo solucione... pero... ¿Cuál de ellos es el mejor? Sería conveniente poder aplicar algún tipo de "puntuación" a los algoritmos, y que cuanta más puntuación sacara un algoritmo, pues supondremos que es mejor. Eso es, en cierto modo, la complejidad.

¿Qué es la complejidad de un algoritmo?

Atacar el problema desde distintos puntos de vista, aplicando distintas estrategias, y por tanto, llegando a soluciones algorítmicas distintas.

Desde el punto de vista computacional, es necesario disponer de alguna forma de comparar una solución algorítmica con otra, para conocer cómo se comportarán cuando las implementemos, especialmente al atacar problemas grandes.

La complejidad algorítmica es una métrica teórica que se aplica a los algoritmos en este sentido.

Introducción

Entender la complejidad es importante porque a la hora de resolver muchos problemas, utilizamos algoritmos ya diseñados. Saber valorar su valor de complejidad puede ayudarnos mucho a conocer cómo se va a comportar el algoritmo e incluso a escoger uno u otro.

Nos vamos a dedicar a intentar exponer qué es la complejidad de un algoritmo desde un punto de vista sencillo, intentado distinguir qué impacto tiene el que un algoritmo tenga una u otra complejidad. Y, como de costumbre, adoptamos grandes simplificaciones, con el único ánimo de obtener una visión general de los conceptos.

Saber si un algoritmo es mejor que otro puede estudiarse desde dos puntos de vista: un algoritmo es mejor cuanto menos tarde en resolver un problema, o bien es tanto mejor cuanta menos memoria necesite.

A la idea del tiempo que consume un algoritmo para resolver un problema le llamamos complejidad temporal y a la idea de la memoria que necesita el algoritmo le llamamos complejidad espacial.

La complejidad espacial, en general, tiene mucho menos interés. El tiempo es un recurso mucho más valioso que el espacio.

Así que cuando hablamos de complejidad a secas, nos estamos refiriendo prácticamente siempre a complejidad temporal.

Page 2: Cuál Es El Objetivo de Un Algoritmo

Bueno... pues ya hemos presentado de manera intuitiva esto de la complejidad: la complejidad de un algoritmo es un "valor", por así decirlo, que nos da una idea de cuánto va a tardar un algoritmo en resolver el problema para el que fue diseñado.

El tamaño de un problema

La idea que subyace tras el concepto de complejidad temporal de un algoritmo es, básicamente, medir cuánto tarda en resolver el problema.

Para resolver cualquier problema, son necesarios unos datos de entrada sobre los que trabaja el algoritmo y que describen una ocurrencia concreta del problema que queremos resolver. El algoritmo, finalmente obtiene una o varias soluciones al problema (si es que el problema tiene soluciones).

Sin embargo, debemos tener en cuenta algunas consideraciones. Por ejemplo, piensa en un típico algoritmo para ordenar los elementos de un vector. Seguro que conoces alguno. El algoritmo consta de una serie de instrucciones que se repiten una y otra vez (bucles), y probablemente, de una serie de selecciones (comparaciones) que hacen que se ejecute uno u otro camino dentro del algoritmo.

Se hace necesaria una pregunta: ¿Tardará lo mismo un algoritmo de ordenación en ordenar un vector con 100 valores que uno con 100000 valores?.... Obviamente no. Pues aquí es donde tenemos que empezar a hablar del tamaño o talla del problema.

Un algoritmo de ordenación debería ser capaz de ordenar un vector con cualquier número de elementos. Sin embargo, el tamaño del vector incide directamente en el tiempo que tarda el algoritmo en resolverse.

Pues cualquier problema tiene un tamaño, que es un valor o un conjunto de valores que se pueden obtener de los datos de entrada y que si varían, normalmente tienen una repercusión en el tiempo que tardará el algoritmo en finalizar (aunque en algunos casos no).

Por ejemplo, del problema de ordenar un vector, la talla del problema nos la da el número de elementos del vector.

En un algoritmo que halle el término n-ésimo de la, la talla nos la da el propio término número n que queremos hallar.

Cada problema tiene uno o varios valores que determinan su talla.

La complejidad se calcula en función de una talla genérica, y no concreta. Por ejemplo, la complejidad de un algoritmo de ordenación se calcula pensando en un array de longitud n, y no 5, 120 o 100000.

Page 3: Cuál Es El Objetivo de Un Algoritmo

La complejidad no es un número: es una función

Otra consideración a tener en cuenta a la hora de tratar con la complejidad es que si estamos contando el tiempo que tarda un algoritmo en resolver un problema ¿En qué ordenador lo ejecutamos? Parece obvio que el mismo algoritmo ejecutado en un ordenador el doble de rápido que otro tardará la mitad en encontrar la solución. ¿Cuál debería ser entonces la unidad de medida de la complejidad? Ninguna unidad de tiempo nos vale: ni segundos ni milisegundos, porque el resultado variaría de un ordenador a otro.

Además... parece obvio también que el mismo algoritmo tardará más o menos en solucionar un problema de una talla u otra. Es decir, no puede tardarse lo mismo en ordenar un array de 100 valores que uno de 100000.

La medida del tiempo tiene que ser independiente: – de la máquina – del lenguaje de programación – del compilador – de cualquier otro elemento hardware o software que influya en el análisis.

Para conseguir esta independencia una posible medida abstracta puede consistir en determinar cuantos pasos se efectúan al ejecutarse el algoritmo.

Bueno... pues vamos a adoptar una simplificación que nos permita no tener en cuenta en qué ordenador se ejecutará el algoritmo: en lugar de medir tiempos, vamos a contar las instrucciones que debe realizar el algoritmo. Supondremos que cada instrucción se ejecuta en un tiempo constante.

Nos podemos permitir esa simplificación porque lo que realmente queremos saber es cómo crece el número de instrucciones necesarias para resolver el problema con respecto a la talla del problema. Eso es realmente la complejidad.

Por ejemplo, observa ésta función escrita en un pseudocódigo (da igual cuál sea su propósito).

Page 4: Cuál Es El Objetivo de Un Algoritmo

En este algoritmo hay un par de bucles para que se ejecuten uno después del otro.

Observemos el primer bucle. Se ejecuta n veces, y en su interior hay una instrucción (la de la línea 5). Eso quiere decir que la línea 5 se ejecuta n veces. Después se ejecuta el segundo bucle, que contiene en su interior dos instrucciones (las de las líneas 8 y 9). Como ese segundo bucle se ejecuta también n veces y tiene dos instrucciones, se realizan 2n instrucciones. Finalmente hay una instrucción en la línea 11 que se ejecuta una sola vez.

Bien.... el número de instrucciones que se ejecutan en total son n+2n+1... Es decir, 3n+1

Todavía no hemos llegado al fondo de la cuestión, pero vamos encaminados. Podemos decir que la complejidad de ese algoritmo es 3n+1, porque ese es el número de instrucciones que hay que realizar para solucionar el problema cuando la talla del problema es n.

La idea que subyace es que podemos saber cómo se comporta el algoritmo conforme la talla del problema va creciendo. En este caso, si representamos 3n+1 con respecto a n nos daremos cuenta de que esta función es una recta. Para terminar de ver la importancia de esto, vamos a ver un par de funciones más de ejemplo.

La función ejemplo2 realiza siempre 3 instrucciones (las líneas 4, 5 y 6), independientemente de lo que se le pase como parámetro. Así pues, este algoritmo siempre tarda lo mismo para

Page 5: Cuál Es El Objetivo de Un Algoritmo

cualquier valor de n. El número de instrucciones se mantiene constante, a diferencia del ejemplo anterior, que crecía linealmente con respecto a n.

La función ejemplo3 tiene dos bucles para anidados. Cada bucle se ejecuta n veces, y en el interior del segundo hay 2 instrucciones. Ej bucle interior hace que las dos instrucciones se repitan n veces, y el bucle exterior hace que todo eso se repita n veces más. Después hay una última instrucción (la de la línea 10). Así pues, se hacen 2×n×n+1 instrucciones... es decir, 2n2+1.

Si representamos 2n2+1 con respecto a n en un gráfico nos podemos dar cuenta de que es una parábola... es decir, el tiempo crece muchísimo más rápido conforme crece n que en los ejemplos anteriores. En este caso, el tiempo crece cuadráticamente. Igual que ocurría en los ejemplos anteriores, si en una determinada máquina para n=100 se tardan 7 unidades de tiempo, con unas pocas operaciones podemos intuir cuánto va a tardar para un n=1000 o para cualquier otro valor.

Lo importante de la idea expuesta hasta ahora es que los distintos algoritmos se comportan de forma distinta. Su tiempo de ejecución varía de manera muy distinta conforme aumenta la talla del problema.

Peor caso, mejor caso

En los tres ejemplos que hemos visto hasta ahora, la complejidad del algoritmo es totalmente dependiente de la talla, pero no todos los algoritmos se comportan de igual manera frente a un problema de la misma talla.

En la mayor parte de los algoritmos, también influye el propio contenido de los datos. Es posible que para un problema determinado de tamaño n, unas veces el algoritmo tarde más y otras tarde menos, dependiendo de los propios datos de entrada del problema de tamaño n.

Un ejemplo... Éste algoritmo comprueba si un determinado valor x está contenido en un array v, cuyos índices van de 1 a n

Page 6: Cuál Es El Objetivo de Un Algoritmo

Contiene en su interior un bucle mientras que no se ejecuta un número determinado de veces (como ocurría en los ejemplos anteriores, que utilizaban bucles for). Imagina que pasamos a esa función un vector de n=100 enteros, y un entero x=17. Es evidente que la talla del problema es n=100, ya que es lo que determina el número de instrucciones que se ejecutarán. Sin embargo, es posible que el valor 17 esté situado en la posición 30 del vector, con lo que el bucle se realizará 30 veces, o quizá en la posición 50, o quizá no esté en el vector, con lo que el bucle se ejecutará n=100 veces, recorriendo todo el vector.

En éste caso, nos conviene distinguir dos métricas: qué es lo peor que nos puede pasar para un problema de tamaño n, y qué es lo mejor que nos puede pasar para un problema de tamaño n.

Vamos a echar unas pocas cuentas para el ejemplo de arriba. Vamos a suponer que en el interior del bucle mientras hay 2 instrucciones (el si y el incremento de i), y en el exterior hay 3, dos antes del mientras y una después.

Pues bien... lo mejor que nos puede pasar es que encontremos el valor x a la primera. En ese caso, el bucle se ejecuta una sola vez.. El número de instrucciones que realizamos son 3+2=5.

Lo peor que puede pasar es que el valor x no se encuentre en el vector, así que el bucle se ejecutará n veces, recorriendo todo el vector. El número de instrucciones que realizamos es 3+2n

Para expresar esto, se utiliza una notación específica, diremos que para este algoritmo, su complejidad en el peor caso es O(2n+3) A esta notación se le denomina "O Grande" (del inglés "Big-O"), o simplemente "Complejidad en el peor caso".

Análogamente, diremos que para este algoritmo su complejidad en el mejor caso es Ω(5). A esta notación se le denomina "Omega" (por la letra griega omega mayúscula Ω) o simplemente, "complejidad en el mejor caso".

En general, cuando decimos "complejidad" a secas, casi siempre nos referimos a la complejidad en el peor caso. Es decir... cuánto va a tardar el algoritmo como mucho.

Asíntotas y órdenes de complejidad

Ya tenemos una ligera idea informal de qué es la complejidad. Pero entonces ¿Cómo se comparan unos algoritmos con otros? Bueno... la idea de la complejidad de un algoritmo, es conocer cómo se comporta el tiempo de ejecución conforme la talla del problema va creciendo.... especialmente para valores muy grandes... lo más grandes que podamos imaginar, y especialmente en el peor de los casos.

En ese contexto de tallas muy grandes podemos hacer otra "simplificación", por así decirlo. El hecho es que podemos encontrar ciertas similitudes entre las funciones que definen la complejidad de los algoritmos. Por ejemplo, si comparamos todos los algoritmos cuya complejidad es lineal (es decir, una recta... por ejemplo: 10n+3, 32n+12, 56n+1... o cualquier otro) y los comparamos con todos aquellos cuya complejidad es cuadrática (Por ejemplo, 2n2+3, 4n2+n, 6n2+4n+3...) y dibujamos sus funciones de complejidad en gráfica.

Page 7: Cuál Es El Objetivo de Un Algoritmo

Observaremos que conforme n se va haciendo grande, tienen un patrón de crecimiento bien diferenciado.

En ese contexto, podemos agrupar todas las complejidades que crecen igual en el mismo saco. A ese saco le vamos a llamar orden de complejidad. Hablando un pelín más formalmente, para todas las funciones que agrupemos en un mismo orden, encontraremos una asíntota que al multiplicarla por un valor nos acote a nuestra función superiormente cuando estemos tratando el peor caso.

Por ejemplo, todas las complejidades cuadráticas están acotadas asintóticamente por n2. Eso quiere decir que para cualquiera de las complejidades cuadráticas que hemos visto antes, por ejemplo 6n2+4n+3, existe un valor real c que hace que 6n2+4n+3 ≤ cn2 cuando n se hace muy grande, es decir, cuando n→∞

De esta manera, la complejidad suele clasificarse en una serie de órdenes comunes.

Como ves, finalmente el concepto de complejidad se nos simplifica mucho. En lugar de hallar los tiempos exactos que tarda un algoritmo en solucionar los problemas, con lo que nos quedamos en con una asíntota que representa a todos los algoritmos cuyo tiempo crece de igual forma cuando la talla del problema tiende a infinito. Eso hace que el cálculo de la complejidad se simplifique mucho. Si no queremos hallar la función de complejidad real (y casi nunca queremos), sino el orden al que pertenece la complejidad del algoritmo, a la hora de hallarla, prácticamente ni siquiera tenemos que contar las instrucciones... Cada secuencia de instrucciones se cuenta como una sola instrucción. Lo que influye en la complejidad son principalmente los bucles y las sentencias de selección que tienen efecto sobre los bucles. En los algoritmos recursivos, la cosa se complica un poco más... Pero como hemos dicho al principio, el cálculo de la complejidad de un algoritmo queda fuera del alcance de éste artículo.

Cuando decimos que la complejidad de un algoritmo es de un orden concreto, por ejemplo n2, podemos estar seguros de que para cualquier valor de n, y por muy mal que nos vayan las cosas (peor) caso, el valor que obtengamos nunca será mayor que cn2, siendo c un real mayor que 1.

Órdenes de complejidad más comunes

Como ya hemos mencionado, la complejidad, digamos "real" en el peor caso de los algoritmos puede enmarcarse en un "orden de complejidad". Dicho de otra manera, la complejidad de un algoritmo "crece como" el representante de su orden cuando la talla del problema se hace muy grande.

Bien... pues los órdenes de complejidad que se suelen manejar son éstos, de mejor a peor

Page 8: Cuál Es El Objetivo de Un Algoritmo

Resumen, conclusiones y recomendaciones

Bueno... al final no ha sido tan dificil. Resulta que un algoritmo que resuelve un problema con una determinada talla (por ejemplo, n) tarda un tiempo, en general, mayor en obtener la solución cuanto mayor es esa talla n (cosa que resulta obvia). La complejidad es una medida que nos da una idea de cómo es ese crecimiento, resultando que para la mayor parte de algoritmos, ese crecimiento se puede enmarcar en un determinado "orden", ya que todas las funciones que están en un orden crecen de manera similar cuando los valores de la talla se van haciendo grandes.

Para cada algoritmo, son interesantes un par de medidas de complejidad: en el peor caso (que señalamos con la notación O (Omicron, "Big-O"), y en el mejor, para el que utilizamos la notacion Ω (Omega). En especial, suele ser más imprescindible conocer el peor caso, ya que nos da una idea de qué es lo que puede pasar cuando las cosas van realmente mal.

Toda esta teoría de la complejidad, que tuvo un gran auge hace algunos años, ha sido criticada desde muchos puntos de vista. La verdad es que es muy útil en un nivel teórico, pero un nivel práctico tampoco conviene obsesionarse demasiado. Es necesario tener en cuenta que todo

Page 9: Cuál Es El Objetivo de Un Algoritmo

esto tiene que ver con límites cuando el tamaño de los problemas es muy grande y teniendo en cuenta el peor de los casos. A efectos prácticos, si estamos seguros de que el tamaño de nuestros problemas es pequeño, incluso un algoritmo con una complejidad "intratable" puede obtener soluciones en un tiempo razonable.

Además, muchos algoritmos con una elevada complejidad en el peor caso, resulta que no se comportan tan mal en la práctica, ya que muchas veces los problemas que se resuelven con él no son el peor caso. A veces, incluso se manipulan un poco los problemas para que nunca presenten el peor caso, o se aplican técnicas que ahorren trabajo al algoritmo (heurísticas, podas, etc). La complejidad del algoritmo es la misma, pero en promedio se comportan mucho mejor que lo que cabría esperar vista su complejidad en el peor caso, simplemente porque no se suele llegar al peor caso.

Por último, comentar que existen otras medidas de complejidad que las vistas hasta aquí, que son simplemente máximos y mínimos teóricos cuando la talla del problema tiende a un número muy grande. Y también, cuando un algoritmo se quiere probar "en la práctica" se aplican otran técnicas, principalmente teniendo en cuenta la probabilidad que tiene un problema en aparecer o no. Por ejemplo, los algoritmos de ordenación de arrays más sencillos tienen una complejidad máxima teórica de O(n2). Uno de los peores casos que se pueden presentar es que el array esté ordenado completamente al revés de cómo queremos... pero en una aplicación real es muy poco probable que esto ocurra... con lo que a veces la realidad no es tan mala, especialmente cuando los problemas no tienen una talla muy elevada.

Sobre la complejidad espacial que se mencionaba al principio podemos actuar de la misma manera, solo que en lugar de contar instrucciones, contamos las piezas de memoria dinámica que se utilizan. Esto es, las que se solicitan del espacio del heap, de memoria secundaria, o las que que quedan atrapadas en la. Sin embargo, su estudio suele tener menos interés, porque el tiempo es un recurso mucho más valioso que el espacio.

Este artículo está, con seguridad, repleto de incorrecciones formales. Lo sabemos... pero como decíamos al principio, no es nuestro objetivo describir formalmente la complejidad, sino simplemente intentar transmitir una idea intuitiva de lo que representa.