Tema 1: Análisis de algoritmos
Qué se analiza
Para un mismo problema suele haber varios programas, y se pueden comparar por muchos criterios (legibilidad, interfaz, memoria, consumo…). En este tema el criterio es la eficiencia: el mejor aprovechamiento de los recursos.
Se estudian los algoritmos (los métodos), no los programas (sus implementaciones en un lenguaje concreto).
Hay dos recursos:
- Complejidad espacial: memoria que consume (memory-bound).
- Complejidad temporal: tiempo que tarda (compute-bound).
Suelen ser objetivos contrapuestos: a menudo se gana tiempo gastando más memoria, y hay que buscar un compromiso. El tema se centra en la temporal, pero todo vale igual para la espacial.
Análisis teórico y experimental
| Análisis teórico (a priori) | Análisis experimental (a posteriori) | |
|---|---|---|
| Qué hace | Obtiene una expresión matemática del coste en función de los parámetros | Ejecuta casos de prueba y mide tiempos |
| Depende de | Solo del algoritmo | Máquina, lenguaje, compilador y datos concretos |
| Sirve para | Generalizar | Caracterizar el programa en sus condiciones de uso |
Extrapolar medidas es peligroso
Generalizar a partir de medidas suele llevar a errores graves. En las diapositivas, con tallas pequeñas parece que el tiempo es constante y que un algoritmo es el mejor; midiendo hasta 4096 la realidad es muy distinta.
El análisis teórico interesa porque permite predecir el coste sin implementar el algoritmo y se puede usar ya en la fase de diseño.
Talla, paso y coste temporal
Talla del problema: valor o valores de la entrada que miden su tamaño.
| Entrada | Talla |
|---|---|
| Vector | Número de elementos |
| Matriz | Dimensiones |
| Número | El propio número |
Paso: fragmento de código cuyo tiempo no depende de la talla y está acotado por una constante. Es lo que se suele llamar operación elemental: operaciones aritméticas y lógicas, comparaciones, accesos a variables o a elementos de vectores, asignaciones, lecturas, retornos…
Como cada paso tarda un tiempo acotado en cualquier máquina, se supone que todos tardan lo mismo, y el coste se mide en pasos, no en segundos.
Coste temporal de un algoritmo: función que da el número de pasos que necesita el algoritmo para cada talla posible.
Principio de invarianza: dos implementaciones del mismo algoritmo solo se diferencian en una constante multiplicativa. Si y son sus tiempos:
Un factor 10 o 100 importa poco frente a una diferencia en cómo crece el coste con la talla: para tallas grandes, eso es lo que decide. Por eso el algoritmo es más importante que el programa.
Ejemplo: tres formas de calcular
Funcion Solucion1 (n: entero) retorna (m: entero)
m = n*n; // 2 pasos
retorna m // 1 paso
ffuncion
Funcion Solucion2 (n: entero) retorna (m: entero)
m = 0; // 1 paso
para i = 1 hasta n hacer // 2n + 2 pasos
m = m + n; // 2 pasos, n veces
fpara
retorna m // 1 paso
ffuncion
Funcion Solucion3 (n: entero) retorna (m: entero)
m = 0; // 1 paso
para i = 1 hasta n hacer // 2n + 2 pasos
para j = 1 hasta n hacer // 2n + 2 pasos, n veces
m++; // 1 paso, n² veces
fpara
fpara
retorna m // 1 paso
ffuncion
Un bucle para i = 1 hasta n cuesta pasos: 1 asignación, comparaciones y incrementos.
| Solución | Pasos |
|---|---|
| 1 | (constante) |
| 2 | |
| 3 |
Aunque se dé un coste distinto a cada operación (en las diapositivas, producto 30 µs, suma 20 µs y el resto 1 µs), las gráficas tienen la misma forma: la solución 1 acaba siendo siempre la mejor. Un método de tiempo constante acaba ganando a uno lineal, y uno lineal a uno cuadrático. Se dice que la solución 1 es asintóticamente más eficiente que las otras dos, y la 2 que la 3.
Instancias: mejor, peor y caso promedio
El coste no siempre depende solo de la talla.
Instancia: factor con el que varía el coste para una talla fija. Es un caso particular del problema que hace que el algoritmo se comporte de una forma u otra.
Búsqueda secuencial
función Secuencial(A[1..n]: vector de enteros; x: entero) retorna (i: entero)
i = 1;
mientras (i ≤ n y A[i] ≠ x) hacer
i = i + 1;
fmientras
retorna i
ffunción
Con fijo, puede estar en la posición 1, en la 2…, en la , o no estar. Cada caso es una instancia distinta y cuesta distinto.
- Mejor caso: instancias que, para cada talla, se resuelven más rápido. Aquí, en la primera posición: coste constante.
- Peor caso: instancias que, para cada talla, necesitan más pasos. Aquí, no está: coste lineal.
- Caso promedio: necesita conocer la probabilidad de cada instancia.
¿Cuál da más información? Depende. El promedio parece el más interesante, pero en algunas aplicaciones importa más el peor caso (un algoritmo razonable de media puede ser prohibitivo en el peor).
En la asignatura se estudian el mejor y el peor caso porque:
- Son fáciles de analizar; el promedio exige conocer la distribución de probabilidades y matemáticas más avanzadas.
- El promedio siempre está entre los dos, así que ya dan información sobre él.
Error típico: confundir instancia con talla
Una instancia cambia el coste sin cambiar la talla. Que el algoritmo haga algo distinto cuando no es una instancia: es un valor de la talla.
Notación asintótica
Estudia el comportamiento para tallas suficientemente grandes, sin fijarse en lo que pasa con tallas pequeñas y olvidando las constantes. Sean , con el coste del algoritmo.
(o grande): cota superior
domina asintóticamente a : a partir de , nunca supera a un múltiplo de . es el conjunto de funciones acotadas superiormente por .
Ejemplos
- : con , para todo .
- : no hay ni que valgan, porque acaba superando a cualquier .
(omega): cota inferior
es dominada por : hay un múltiplo de que nunca baja.
Ejemplos
- : con , para todo .
- : con , para todo .
(theta): orden exacto
Es decir, : domina y es dominada por .
Jerarquía de órdenes
Con las inclusiones van al revés: .
Así, está en , pero también en y en . Siempre se da la cota más ajustada.
| Tipo | Orden |
|---|---|
| Constante | |
| Logarítmica (sublineal) | , |
| Lineal | |
| Superlineal | |
| Polinómica: cuadrática, cúbica | , |
| Exponencial | , |
Propiedades de las cotas
Todas valen igual cambiando por o por .
- Polinomios: un polinomio de grado es .
- Regla de la suma: . Ejemplo: .
- Regla del producto: . Ejemplo: .
- Cerrado para la suma: si , entonces . Ejemplo: .
- No cerrado para el producto: si , no tiene por qué estar en . Ejemplo: , no .
- Constantes: si , entonces para . Ejemplo: .
Comparar órdenes con límites
| Conclusión | |
|---|---|
| (constante) | y |
| crece más rápido que | |
| crece más rápido que |
La base del logaritmo no importa
Para : , porque
Por eso se escribe simplemente .
Sumas habituales
- Progresión aritmética ():
- Progresión geométrica ():
- Linealidad: y
| Suma | Orden (también vale con y ) |
|---|---|
| , | |
| , | |
| , | |
| , |
Cómo calcular la complejidad de un algoritmo
- Talla: decidir de qué depende el tamaño del problema.
- Instancias: ver si hay mejor y peor caso.
- Si el coste no depende de la instancia: se da con .
- Si depende: se da con (mejor caso) y (peor caso).
- Cuantificar:
- Algoritmos iterativos: contar los pasos significativos.
- Algoritmos recursivos: plantear y resolver una relación de recurrencia.
Mismo código, con y sin instancias
Funcion Ejemplo1 (v: vector enteros; n: entero) retorna (entero)
m = 0;
si (n = 0) retorna 1;
si no
para i = 1 hasta n hacer m = m + n; fpara
fsi
retorna m
ffuncion
- Con la condición
n = 0: no hay instancias, porque es un valor de la talla. Coste . - Cambiando la condición por
par(v[n]): sí hay instancias, porque con fijo el coste depende del contenido del vector. Mejor caso constante y peor caso lineal: y .
Algoritmos recursivos
Su coste se expresa de forma natural con una relación de recurrencia:
donde ; lo habitual es o , y puede ser .
Hay varios métodos para resolverlas (sustitución, inducción, función generadora…). En la asignatura se usa el de sustitución o expansión: ir sustituyendo cada por su definición hasta ver el término general, y llegar al caso base.
Factorial
Función Factorial(n: entero) retorna (entero)
si (n = 0) entonces retorna 1
sino retorna n * Factorial(n-1)
fsi
ffuncion
Talla , sin instancias. Recurrencia:
Expandiendo: . Se llega a la base cuando , es decir, : .
Recurrencias inmanejables: acotar
A veces la expansión se complica tanto que no se puede resolver. Entonces se busca la menor cota superior y la mayor cota inferior posibles, sustituyendo la recurrencia por otras dos más sencillas que la acoten.
Dos llamadas de tamaños distintos
Función Ejemplo(n: entero) retorna (entero)
si n = 1 entonces retorna 1
sino retorna n * Ejemplo(n-1) * Ejemplo(n div 2)
fsi
ffuncion
- Cota inferior: cambiar por , que es más pequeño: .
- Cota superior: cambiar por , que es más grande: .
Por tanto, y .
Modelos generales de recurrencia
Sirven para comprobar que los cálculos hechos a mano están bien.
En los exámenes no se puede usar esta tabla
Salvo que se diga lo contrario, la complejidad hay que obtenerla resolviendo la recurrencia; la tabla es solo para comprobar el resultado.
Sustracción (el problema se reduce restando ):
División (el problema se reduce dividiendo entre ):
Aquí es el número de llamadas recursivas, cuánto se reduce el problema y el coste del trabajo no recursivo. El caso en sustracción no tiene utilidad práctica; está por completitud.
Comprobaciones
- Factorial: sustracción con , , : . ✓
- : división con , , ; : . ✓
- : sustracción con , : . ✓
Límites del criterio asintótico
¿Es mejor un algoritmo de coste o uno de ? Asintóticamente, el segundo, porque . Pero eso es para suficientemente grande: solo cuando .
- Si casi todos los problemas reales tienen , conviene el de .
- Para ir bien con cualquier talla, se pueden combinar: un programa que llame a uno u otro según .
Otras cosas a tener en cuenta:
- Un algoritmo teóricamente mejor puede esconder una constante enorme que lo haga peor en la práctica.
- Si se va a usar pocas veces, puede compensar uno menos eficiente pero más rápido de desarrollar: el coste incluye desarrollo y mantenimiento, no solo ejecución.
- Ganar tiempo puede costar memoria.
Como cualquier ingeniero, el diseñador tiene que buscar un compromiso entre todos estos factores (talla, frecuencia de uso…).