ANÁLISIS DE COMPLEJIDAD DE
ALGORITMOS
Karim Guevara Puente de la Vega
Agenda
Evaluando el rendimiento
Conteo de sentencias
Notación O-Grande
Caso peor, mejor, medio
Tipos de algoritmos
Reglas de notación asintótica
Complejidad de algoritmos iterativos
Complejidad de algoritmos recursivos
Introducción
Cuando hay necesidad de elegir entre varios algoritmos,
¿cómo evaluar y/o comparar las alternativas?...
Con frecuencia interesa el buen rendimiento
• Tiempo esperado y el uso de la memoria
• Considerar que sea fácil de implementar, verificar, mantener el código.
• Asegurar que la complejidad valga la pena.
Evaluando el rendimiento
A posteriori (empírico)
Se prueba con datos reales
Implementación del algoritmo en un computador, se corrige el
código
Sujeto a variables (hw, so, etc.)
A priori (teórico)
Análisis sin implementar el código
Conjunto abstracto, independiente de cualquier entorno
Los resultados empíricos pueden no coincidir exactamente
(especialmente para las entradas de tamaño pequeño) con un
entorno específico
¿Complejidad algorítmica?
La eficiencia de un algoritmo puede ser cuantificada con las
siguientes medidas de complejidad:
Complejidad Temporal o Tiempo de ejecución: Tiempo de
cómputo necesario para ejecutar algún programa.
Complejidad Espacial: Memoria que utiliza un programa para su
ejecución
El análisis se basa en las Complejidades Temporales, para
cada problema determinaremos una medida n (tamaño de la
entrada).
Conteo de sentencias
public double CtoF (double cTemp) {
return cTemp * 9.0 / 5.0 + 32;
}
Cada sentencia tiene un costo de 1c
Multiplicación, división, suma, retorno
Total = 4c
Valor de la entrada es importante?
Más de conteo de sentencias
public double Average (int v[]) {
int sum = 0;
for (int i = 0; i < v.length; i++)
sum += v[i];
return sum/v.length;
}
Contando las sentencias
Fuera bucle: 4 sentencias (inic. sum, inic. i, división, retorno)
Cuerpo bucle: 3 sentencias (evaluación, agregación, incr. i) por elemento
Total = 3N + 4
¿La entrada importa?
Duplica el tamaño del vector – como cambia el tiempo requerido?
Más de conteo de sentencias
public double GetExtremes (int v[], int r[]) {
r[0] = r[1] = v[0];
for (int i = 1; i < v.length; i++) {
if (v[i] > r[0])
r[0] = v[i];
}
for (int i = 1; i < v.length; i++) {
if (v[i] < r[1])
r[1] = v[i];
}
}
Bucle: evaluar i, compara, actualiza, incr. i: 4 por iteración * N
iteraciones
2 bucles
Fuera bucle: inic. i, inic. r
4 + 8N
Comparando algoritmos
Conteo de sentencia
CtoF (4) Average(3N +4) GetExtremes(8N + 4)
GetExtremes tendrá siempre más tiempo?
Considerar el crecimiento del patrón
Si Average toma 2 ms. para 1000 elementos,
• Cuánto estimamos para 2000 o 10000?
Qué de GetExtremes?
Notación O-Grande
Resumir el conteo de sentencias
Solo usa los términos más grandes, ignora otros, elimina todos los
coeficientes.
Tiempo = 3n + 5 O(n)
Tiempo = 10n - 2 O(n)
Tiempo = 1/2n2 - n O(n2)
Tiempo = 2n + n3 O(2n)
Describe la curva de crecimiento del algoritmo en el limite superior.
Intuición: evitar detalles cuando no importan, debido a que el tamaño de
la entrada (N) es lo suficientemente grande
Formalmente:
O(f(n)) es un límite superior sobre el tiempo requerido
• Tiempo de ejecución – T(n) <= Cf(n) para alguna constante C y el valor de
n suficientemente grande.
Utilizando O-Grande se predice el tiempo
Para un algoritmo O(n):
5,000 elementos toma 3.2 segundos
10,000 elementos toma 6.4 segundos
20,000 elementos tomará ….?
Para un algoritmo O(n2):
5,000 elementos toma 2.4 segundos
10,000 elementos toma 9.6 segundos
20,000 elementos tomará …?.
Caso peor – mejor - medio
public boolean Search (String names[], String key) {
for (int i=0; i<names.length; i++)
if (names[i].equals(key) )
return true;
return false;
}
Si la clave está al inicio? al medio? al final? Si no está?
Mejor caso
Muy rápido en algunas situaciones, a menudo no es tan relevante
(T): orden inferior de T, u omega de T.
Peor caso
Límite superior de lo malo que puede suceder
O(T): orden de complejidad de T
Caso medio
Media de todas las posibles entradas, puede ser más difícil de calcular con
precisión
(T): orden exacto de T
Tiempos de ejecución: 106 instr / seg
Patrones de crecimiento
Funciones de complejidad más frecuentes
No depende del tamaño del problema
O(1) Constante Algunos algoritmos de búsqueda en Tabla
Hashing
O(log n) Logarítmica Búsqueda binaria Eficiente
Búsqueda lineal o secuencial, búsqueda en
O(n) Lineal
texto
O(n•log n) Casi lineal QuickSort
O(n 2) Cuadrática Algoritmo de la burbuja, QuickSort (peor caso)
O(n 3) Cúbica Producto de matrices Tratable
O(n k) k>3 Polinómica
Algunos algoritmos de grafos, muchos
O(k n) k>1 Exponencial problemas de optimización, por lo general en
fuerza bruta Intratable
Algunos algoritmos de grafos , todas las
O(n!) Factorial
permutaciones
Tipos de algoritmos
Algoritmos polinomiales
Aquellos que son proporcionales a nk
Factibles o aplicables: son solucionables
Algoritmos exponenciales
Aquellos que son proporcionales a kn
No son factibles salvo un tamaño de entrada n exageradamente
pequeño.
Reglas de notación asintótica
Sean T1(n) y T2(n) dos funciones que expresan los tiempos
de ejecución de dos fragmentos de un programa, y se acotan
de forma que se tiene:
T1(n) = O(f1(n)) y T2(n) = O(f2(n))
Regla de la suma
T1(n) + T2(n) = O(max(f1(n),f2(n)))
Regla del producto
T1(n) T2(n) = O(f1(n) f2(n))
COMPLEJIDAD DE ALGORITMOS
ITERATIVOS
Instrucciones secuenciales
Asignaciones y expresiones simples
Tiempo de ejecución constante: O(1)
Secuencia de instrucciones
T. ejecución = t. ejecución individuales
P.e.:
Sean S1 y S2, una secuencia de dos instrucciones:
T(S1 ; S2) = T(S1) + T(S2)
Aplicando la regla de la suma:
O(T(S1 ; S2)) = max(O( T(S1), T(S2) ))
Instrucciones condicionales
SI-ENTONCES: es el tiempo necesario para evaluar la
condición, más el requerido para el conjunto de instrucciones.
T(SI-ENTONCES) = T(condición) + T(rama ENTONCES)
Aplicando la regla de la suma:
O(T(SI-ENTONCES)) =
max(O( T(condición),T(rama ENTONCES ))
Instrucciones condicionales
SI-ENTONCES-SINO: tiempo para evaluar la condición, más
el máximo valor del conjunto de instrucciones de las ramas
ENTONCES y SINO.
T(SI-ENTONCES-SINO) = T(condición) + max(T(rama
ENTONCES), T(rama SINO))
Aplicando la regla de la suma:
O(T(SI-ENTONCES-SINO)) = O( T(condición)) +
max(O(T(rama ENTONCES )), O(T(rama SINO)))
Instrucciones de iteración
PARA, MIENTRAS-HACER, HACER-MIENTRAS:
Producto del número de iteraciones por la complejidad de las
instrucciones del cuerpo del mismo bucle.
Considerar la evaluación del número de iteraciones para el peor
caso posible.
Si existen ciclos anidados, realizar el análisis de adentro hacia
fuera.
Ejercicio
Procedimiento MatrizProd (n Por Valor, A , B, C Por Referencia)
Define i,j,k Como Entero
Para i1 Hasta n
Para j1 Hasta n
I5 C[i,j] 0;
I4 Para k1 Hasta n
I3 C[i,j] C[i,j] + A[i,k] * B[k,j];
I2 FinPara
I1 FinPara
FinPara
FinProcedimiento
Llamadas a procedimientos
Tiempo requerido para ejecutar el cuerpo del procedimiento
llamado.
P.e. :
Procedimiento Principal (A , B, C Por Referencia)
Define n,j,i,x Como Entero
Leer n;
i1;
Mientras i<=n Hacer
Para ji Hasta n
A[i,j] j * 2;
FinPara
i i + 1;
FinMientras
MatrizProd(n,A,B,C); O(n3)
FinProcedimiento
COMPLEJIDAD DE ALGORITMOS
RECURSIVOS
Analizando algoritmos recursivos
int factorial (int n) {
if (n < 1 )
return 1;
else
return (n * factorial(n-1));
}
Una inspección al algoritmo puede resultar en una función de
recurrencia (relación de recurrencia):
Imita el flujo de control dentro del algoritmo.
Una vez obtenida esta función se puede aplicar alguna técnica:
Recurrencias homogéneas
Recurrencias no homogéneas
Cambio de variables, etc.
Función de recurrencia
int factorial (int n) {
if (n < 1 )
return 1;
else
return (n * factorial(n-1));
}
T(n) es el tiempo utilizado para una entrada n
1 si n=0
T(n) =
T(n-1) +1 en otro caso
Resolviendo la recurrencia
1 si n=0
T(n) =
T(n-1) +1 en otro caso
T(n) = (T(n-2) +1) +1 = T(n-2) +2
= (T(n-3) +1) +2 = T(n-3) +3
= (T(n-4) +1) +3 = T(n-4) +4
...
generalizando :
= T(n-k) +k
Si k=n : = T(n-n) +n
= 1+n = max(0(1),O(n))
= O(n)
Otro ejemplo
public void MoveTower (int n, char src, char dst, char tmp) {
if (n > 0) {
MoveTower (n-1, src, tmp, dst);
MoveDisk (n, src, dst);
MoveTower (n-1, tmp, dst, src);
}
}
Configurar la recurrencia T(n):
1 si n=0
T(n) =
2T(n-1) +1 en otro caso
Resolviendo la recurrencia
1 si n=0
T(n) =
1 + 2T(n-1) en otro caso
Repetir la substitución
T(n) = 1 + 2T(n-1)
= 1 + [ 2 + 2T(n-2) ]
= 1 + [ 2 + ( 4 + 8T(n-3))]
...
Generalizar el patrón :
= 2i-1 + 2i T(n-i)
Resolver para n-i = 0 (i=n)
= 2n-1 + 2n T(0)
= 2n+1 -1
= 2n+1 ==> O(2n)