Materia: #algoritmos_y_estructuras_de_datos
Tags:
Complejidad
A la hora de diseñar algoritmos y estructuras de datos, tenemos que procurar dos cosas clave: que el algoritmo en cuestión efectivamente resuelva el problema (lo que venimos viendo hasta ahora); y que sea eficiente. Ahora bien, ¿Qué significa la
Si tuviéramos dos algoritmos, y uno tarda 2 horas, y el otro tardase 1 hora, es fácil advertir cual de los dos es mejor: el segundo. Tarda menos, evidentemente lo que sea que haga lo hace
Supongamos que tenemos un programa, y nos interesa determinar su duración en base al tamaño del input. Es decir, queremos ver la relación entre el tamaño de la entrada (por ejemplo, que tan larga es una lista de entrada) y el tiempo que consume resolver el problema (por ejemplo, cuanto tarda en encontrar un elemento en esa lista). Este concepto de relación entre el tamaño de la entrada y el tiempo que consume, es al que llamamos eficiencia. A mayor eficiencia, menor tiempo consumirá con inputs grandes.
Esta medida de la eficiencia es la que nos va a permitir elegir entre dos o mas algoritmos, poder categorizarlos y evaluarlos, determinar cual es mejor o peor que otro según la circunstancia que queramos medir.
Ahora bien, medir el tiempo de un programa no es tarea fácil. Uno puede hacerlo de dos maneras: midiéndolo a mano, es decir, cronometrando el programa al correrlo; o medirlo de manera teórica en base a su comportamiento. La primera medida tiene varios posibles contratiempos. A saber, quizás cambie la medición según la computadora que corre el programa, siendo una mas potente que la otra, o quizás depende del lenguaje en el que este escrito pero no del algoritmo en si. La segunda medida, por otro lado, es independiente a la computadora o al lenguaje, y vale para cualquier instancia del programa.
Vamos a basarnos en un concepto importante para medir la complejidad: las operaciones elementales (
Un poco de notación: vamos a llamar
De lo anterior se deducen las siguientes reglas:
If C Then S1 Else S2 Endif;
Case C Of v1:S1 | v2:S2 | …| vn:Sn End;
While C Do S End;
MiFuncion(P1, P2, …, Pn)
En el ultimo caso tenemos una llamada a función, donde toma n parámetros, y hace la operación F.
Hay que tener una cosa mas en cuenta: entradas de mismo tamaño pueden tener medidas de tiempo diferentes. Por ejemplo, supongamos que queremos evaluar la complejidad de un algoritmo que busca un elemento en una lista, y lo que hace es chequear uno a uno entre los elementos y compararlos con el elemento en cuestión. Si la lista es mas larga, el algoritmo llevara mas tiempo en resolverse, salvo que el elemento este al principio de la lista. Es decir, si preguntamos por el primer elemento de la lista, por mas larga que esta sea, vamos a medir siempre el mismo tiempo (el mínimo, en este caso).
Es por esta situación que nos interesan tres casos particulares de la medición del tiempo: el caso peor, el caso mejor y el caso medio. Podemos construirlos de la siguiente manera:
Análisis asintótico de la complejidad
En general, no nos interesa saber el tiempo exacto de un algoritmo, sino que nos interesa ver su crecimiento. Es decir, nos interesa su orden de magnitud. De esa forma, vamos a poder evaluar mas fácilmente entre dos algoritmos que, a priori, tienen medidas distintas, pero quizás mismo orden de magnitud, o viceversa.
Entonces, decimos que el orden (logarítmico, lineal, cuadrático, exponencial, etc.) de la función
Como tal, lo que nos interesa es el análisis asintótico de la complejidad, que es el comportamiento de la misma para valores de entrada suficientemente grandes. Nosotros vamos a aproximar estos valores con cotas (de ahí lo asintótico), las cuales denotamos:
(O grande) es la corta superior (omega) es la corta inferior (theta) es el orden exacto de la función
Estas cotas van a determinar los limites de la complejidad del algoritmo que estemos analizando. La notación
Análogamente, definimos a omega como:
Finalmente, a theta lo expresamos así:
Una vez explicadas estas definiciones podemos empezar a deducir muchas de las propiedades que vamos a utilizar a la hora de resolver ejercicios de complejidad. Aca listamos unas cuantas:
- Si existe
, y segun los valores de : - Si
y , entonces , y - Si
, entonces , y
- Si
- Si
y - Si
y - Si
y - Si
y - Si
y - Si
y
Existen muchas propiedades mas, pero estas suelen ser las mas utiles.
Ahora, ¿como se relaciona todo esto con la eficiencia de programas? Bueno, ahi es donde entra lo que definimos antes de la función
Decimos que el peor de los casos (la peor instancia de un tamaño
Vamos a mencionar casos conocidos o que ya vimos y mostrar su correspondiente complejidad.
Para empezar, tenemos el famoso caso de buscar un elemento en una lista. Si la lista no tiene una forma de orden, nos va a ser muy dificil encontrar una forma eficiente de encontrar un elemento en ella. Lo unico que podemos hacer es ir chequeando uno a uno, y compararlo con el elemento en cuestion. En el peor de los casos, terminamos chequeando todos los elementos (o bien el elemento esta en la ultima posicion de la lista, o bien ni siquiera pertenece). Eso quiere decir que, si la lista mide
Ahora, supongamos que la lista esta ordenada. Por ejemplo, es una lista de numeros enteros y se encuentra ordenada de menor a mayor. En este caso podriamos llegar a provecharnos de esto y usarlo a nuestro favor. No hace falta chequear uno a uno cada elemento, podriamos dividir la lista a la mitad, y ver si el elemento que estamos buscando pertenece a una o a la otra. Para hacer esto, podriamos preguntar si el elemento es mayor que el valor del medio. Al estar la lista ordenada, podemos afirmar que esto nos separa la lista en dos, y que con ese dato ya sabemos a que mitad pertenece. Noten que esto se puede hacer repetidas veces hasta llegar al elemento en cuestion (si pertenece, en caso contrario, llegariamos a un numero al que no podriamos llegar, o lista vacia). Si hacen las cuentas que ahora no vamos a detallar, podemos llegar a que, en el peor de los casos, la complejidad de este algoritmo es de
Como veran, el simple dato de que la lista esta ordenada, ya nos ayuda a agilizar el procedimiento. El problema ahora seria procurar que la lista siempre se encuentre ordenada.