Sunteți pe pagina 1din 14

Análisis de algoritmos

• Eficiencia de un algoritmo

• Técnicas de diseño de algoritmos

Bibliografía

Robert Sedgewick:
“Algorithms in C”
Addison-Wesley, 1990
ISBN 0201514257

Alfred V. Aho, John E. Hopcroft, Jeffrey D. Ullman:


“Data Structures and Algorithms”
Addison-Wesley, 1983
ISBN: 0201000237

Mark Allen Weiss


“Data Structures and Algorithm Analysis in C”
Addison-Wesley, Second Edition, 1996
ISBN 0201498405

Thomas H. Cormen, Charles E. Leiserson & Ronald L. Rivest


“Introduction to Algorithms”
MIT Press, 1990
ISBN 0262031418

Giles Brassard & Paul Bratley


“Fundamentals of Algorithmics”
Prentice Hall, 1996
ISBN 0133350681

“Fundamentos de Algoritmia”
Prentice Hall, 1997
ISBN 848966000X
Eficiencia de un algoritmo
El análisis de algoritmos estudia, desde el punto de vista teórico, los
recursos computacionales que necesita la ejecución de un programa de
ordenador: su eficiencia.
p.ej. Tiempo de CPU
Uso de memoria
Ancho de banda

NOTA: En el desarrollo real de software, existen otros factores que, a


menudo, son más importantes que la eficiencia: funcionalidad,
corrección, robustez, usabilidad, modularidad, mantenibilidad,
fiabilidad, simplicidad… y el tiempo del programador.

¿Por qué estudiar la eficiencia de los algoritmos?

Porque nos sirve para establecer la frontera entre lo factible y lo imposible.

Ejemplo: Algoritmos de ordenación

Observación
El tiempo de ejecución depende del tamaño del conjunto de datos.

Objetivo
Parametrizar el tiempo de ejecución en función del tamaño del conjunto
de datos, intentando buscar una cota superior que nos sirva de garantía

Tipos de análisis

Peor caso (usualmente)


T(n) = Tiempo máximo necesario para un problema de tamaño n

Caso medio (a veces)


T(n) = Tiempo esperado para un problema cualquiera de tamaño n
• Requiere establecer una distribución estadística

Mejor caso (engañoso)

Análisis de Algoritmos 1 © Fernando Berzal


Análisis del peor caso

¿Cuál es el tiempo que necesitaría un algoritmo concreto?


û Varía en función del ordenador que utilicemos.
û Varía en función del compilador que seleccionemos.
û Puede variar en función de nuestra habilidad como programadores.

IDEA: Ignorar las constantes dependientes del contexto

SOLUCIÓN: Fijarse en el crecimiento de T(n) cuando n → ∞

Notación O
O(g(n)) = { f(n) | ∃c,n0 constantes positivas tales que f (n) = c g(n) ∀ n = n0 }

En la práctica, se ignoran las constantes y los términos de menor peso:

3n3 + 90n2 – 5n + 6046 = O(n3)

Eficiencia asintótica
Cuando n es lo suficientemente grande…
un algoritmo O(1)
es más eficiente que
un algoritmo O(log n)
es más eficiente que
un algoritmo O(n)
es más eficiente que
un algoritmo O(n log n)
es más eficiente que
un algoritmo O(n2)
es más eficiente que
un algoritmo O(n3)
es más eficiente que
un algoritmo O(2n)

NOTA: En ocasiones, un algoritmo más ineficiente puede resultar más


adecuado para resolver un problema real ya que, en la práctica, hay
que tener en cuenta otros aspectos además de la eficiencia.

Análisis de Algoritmos 2 © Fernando Berzal


Análisis de la complejidad de un algoritmo

Propiedades de la notación O

c O(f(n)) = O(f(n))

O(f(n)+g(n)) = max{ O(f(n)), O(g(n)) }

O(f(n)+g(n)) = O(f(n)+g(n))

O(f(n))O(g(n)) = O(f(n)g(n))

O(O(f(n))) = O(f(n))

Asignaciones y expresiones simples

x = x+1; T(n) = O(1)

Secuencias de instrucciones

I1;
I2; T(n) = T1(n) + T2(n) = max { O(T1(n)), O(T2(n)) }

Estructuras de control condicionales (if-then-else)

T(n) = O (Tcondición(n)) + max { O(Tthen(n)), O(Telse(n)) }

Estructuras de control iterativas


Producto del número de iteraciones por la complejidad de cada iteración.
Si la complejidad varía en función de la iteración, obtenemos una sumatoria.

Si no conocemos con exactitud el número de iteraciones (bucles


while y do-while), se estima este número para el peor caso.

Algoritmos recursivos
Se obtienen recurrencias que hay que resolver…

Análisis de Algoritmos 3 © Fernando Berzal


Ejemplo: Algoritmo de ordenación por inserción

void OrdenarInsercion (double v[], int N)


{
int i, j;
double tmp;

for (i=1; i<N; i++) {

tmp = v[i];

for (j=i; (j>0) && (tmp<v[j-1]); j--)


v[j] = v[j-1];

v[j] = tmp;
}
}

Peor caso: Vector inicialmente ordenado al revés

n −1
T ( n ) =∑ O (i ) = O ( n 2 )
i =1

Caso promedio (con todas las permutaciones igualmente probables):

n −1
T ( n ) =∑ Θ(i / 2 ) = Θ( n 2 )
i =1

¿Es eficiente el algoritmo de ordenación por inserción?

ü Moderadamente sí cuando n es relativamente pequeño.

û Absolutamente no cuando n es grande.

Análisis de Algoritmos 4 © Fernando Berzal


Ejemplo: Algoritmo de ordenación por mezcla (merge-sort)

IDEA
- Dividir el vector en dos y ordenar cada mitad por separado.
- Una vez que tengamos las mitades ordenadas, las podemos ir mezclando
para obtener fácilmente el vector ordenado

IMPLEMENTACIÓN EN C

// Mezcla dos subvectores ordenados

double aux[maxN];

void merge (double v[], int l, int m, int r)


{
int i, j, k;

for (i=m+1; i>l; i--) // Vector auxiliar


aux[i-1] = v[i-1]; // O(n)
for (j=m; j<r; j++)
aux[r+m-j] = v[j+1];

for (k=l; k<=r; k++) // Mezcla


if (aux[i]<aux[j]) // O(n)
a[k] = aux[i++];
else
a[k] = aux[j--];
}

// Algoritmo de ordenación

void mergesort (double v[], int l, int r) // T(n)


{
int m = (r+l)/2; // O(1)

if (r > l) { // O(1)
mergesort (v, l, m); // T(n/2)
mergesort (v, m+1, r); // T(n/2)
merge (a, l, m, r); // O(n)
}
}

Análisis de Algoritmos 5 © Fernando Berzal


ANÁLISIS

1. Mezcla de dos subvectores ordenados (merge): Tmerge ( n ) = O ( n )


2

2. Algoritmo de ordenación

 O (1) si n = 1
T (n) = 
2T ( n / 2) + O ( n ) si n > 1

Solución de la ecuación T ( n ) = 2T ( n / 2) + n

O(n log n) crece más despacio que O(n2)

Por tanto,
la ordenación por mezcla es más eficiente que la ordenación por inserción.

Ejercicio
Comprobar experimentalmente a partir de qué valor de n el algoritmo de
ordenación por mezcla es más rápido que el de ordenación por inserción.

Análisis de Algoritmos 6 © Fernando Berzal


Resolución de recurrencias
Se pueden aplicar distintas técnicas y trucos:

Método de sustitución
1. Adivinar la forma de la solución.
2. Demostrar por inducción.
3. Resolver las constantes.

 O (1) si n = 1
T (n ) = 
4T ( n / 2) + n si n > 1

¿T(n) es O(n3)?
Suposición T(k) = ck3 para k<n
Demostramos por inducción T(n) = cn3
T(n) = 4T(n/2) + n
= 4c(n/2)3 + n = (c/2)n3 + n = cn3 – ((c/2)n3 – n)
= cn3 siempre que ((c/2)n3 – n)>0 (p.ej. c=2 y n=1)

PROBLEMA: ¿Podríamos encontrar una cota superior más ajustada?


Sugerencia: Probar con T(n) = cn2 y T(n) = c1n2-c2n

Árbol de recursión
Se basa en construir una representación gráfica intuitiva…

T ( n ) = T ( n / 4 ) + T ( n / 2) + n 2

Análisis de Algoritmos 7 © Fernando Berzal


Expansión de recurrencias
Equivalente algebraica al árbol de recurrencias

Ejemplo: Cálculo del factorial

int factorial (int n)


{
return (n==0)? 1: n*factorial(n-1);
}
 O (1) si n = 0
T (n ) = 
T ( n − 1) + 1 si n > 1
T(n) = T(n-1) + 1
= (T(n-2)+1) + 1 = T(n-2) + 2
= (T(n-3)+1) + 2 = T(n-3) + 3

= T(n-k) + k
Cuando k=n:
T(n) = T(0) + n = 1 + n = O(n) Algoritmo de orden lineal

Ejemplo: Sucesión de Fibonacci

int fibonacci (int n)


{
if ((n == 0) || (n == 1))
return 1;
else
return fibonacci(n-1) + fibonacci(n-2);
}
 O (1) si n ≤ 1
T (n ) = 
T ( n − 1) + T ( n − 2 ) + 1 si n > 1

T(n) = T(n-1) + T(n-2) + 1


= (T(n-2)+T(n-3)+1) + (T(n-3)+T(n-4)+1) + 1
= T(n-2) + 2T(n-3) + T(n-4) + (2+1)
= T(n-3) + 3T(n-4) + 3T(n-5) + T(n-6) + (4+2+1)

1  1 + 5   1 + 5  
n n

T (n) =   −  
5  2   2  
 

Análisis de Algoritmos 8 © Fernando Berzal


Método de la ecuación característica

Recurrencias homogéneas lineales con coeficientes constantes

T(n) = c1T(n-1) + c2T(n-2) + … + ckT(n-k)

a0tn + a1tn-1 + … + aktn-k = 0

Sustituimos tn=xn:
a0xn + a1xn-1 + … + akxn-k = 0

Ecuación característica (eliminando la solución trivial x=0):

a0xk + a1xk-1 + … + ak = 0

Obtenemos la solución a partir de las raíces del polinomio característico:

Caso 1: Raíces distintas ri


k
tn =∑ ci ri n
i =1

Caso 2: Raíces múltiples ri de multiplicidad mi


l mi −1
tn =∑ ∑ cij n j ri n
i =1 j =1

Finalmente, las constantes se determinan a partir de las k condiciones iniciales.

DEMOSTRACIÓN:

Aplicando el Teorema Fundamental del Álgebra,


factorizamos el polinomio de grado k como un producto de k monomios.

Si p(ri)=0, ri es una raíz del polinomio característico


x=ri es una solución de la ecuación característica
rin es una solución de la recurrencia.

Cuando tenemos una raíz múltiple r de multiplicidad m, podemos llegar a la conclusión de


que rn, nrn, n2rn … nm-1rn son otras soluciones de la recurrencia.

Análisis de Algoritmos 9 © Fernando Berzal


Recurrencias no homogéneas lineales

A partir de
a0tn + a1tn-1 + … + aktn-k = bnp(n)
donde b es una constante y p(n) un polinomio de grado d

podemos derivar un polinomio característico

( a0xk + a1xk-1 + … + ak ) ( x – b ) d+1

que resolveremos igual que el caso homogéneo.

Ejemplo: Las Torres de Hanoi

void hanoi (int n, int inic, int tmp, int final)


{
if (n > 0) {
hanoi (n-1, inic, final, tmp);
printf (“Del poste %d al %d.\n”, inic, final);
hanoi (n-1, tmp, inic, final);
}
}
 0 si n = 0
T (n ) = 
2T ( n − 1) + 1 si n > 0

Ecuación recurrente no homogénea: T(n) - 2T(n-1) = 1


Ecuación homogénea x-2 = 0
Constante b b=1
Polinomio p(n) p(n) = 1
Polinomio característico (x-2)(x-1)
Solución: T(n) = c11n + c22n

Condiciones iniciales: T(0) = 0 c1+c2 = 0 c1 = -1


T(1) = 2T(0)+1 = 1 c1+2c2 = 1 c2 = 1

T(n) = 2n – 1 è Algoritmo de orden exponencial O(2n)

Análisis de Algoritmos 10 © Fernando Berzal


Técnicas de diseño de algoritmos
Divide y vencerás
1. Dividir el problema en subproblemas independientes.
2. Resolver los subproblemas de forma independiente.
3. Combinar las soluciones de los subproblemas.

Ejemplos: Búsqueda binaria


Ordenación por mezcla (merge-sort)
Ordenación rápida (quicksort)
Búsqueda de la mediana (≈quicksort)
Multiplicación de matrices (algoritmo de Strassen)

Problema: Calcular an

Algoritmo ingenuo: Eficiencia O(n)

an = a · an-1

Estrategia divide y vencerás: T(n) = T(n/2) + O(1) = O(log2 n)

 a n / 2 a n / 2 si n es par
a =  ( n−1) / 2 ( n−1) / 2
n

a a a si n es impar

Problema: Sucesión de Fibonacci

Algoritmo recursivo básico: Tiempo exponencial O(φn)

Estrategia divide y vencerás: Eficiencia O(log2 n)

n
 Fn+1 Fn   Fn Fn−1  1 1 1 1
F = =
Fn −1   Fn−1 Fn −2  1 0 1 0
·
 n

Análisis de Algoritmos 11 © Fernando Berzal


Algoritmos voraces (greedy)
En cada momento se toma una decisión local irrevocable
- Técnica especialmente adecuada cuando podemos garantizar que la mejor
solución local nos lleva a la solución global del problema.
- Incluso, puede resultar adecuada cuando no tenemos esa garantía para obtener
rápidamente una aproximación a la solución del problema: heurísticas.

Aplicaciones: Problema del viajante de comercio (problema NP: O(n!)).

Estrategia greedy
Solución rápida pero no siempre óptima

Ejemplo: Algoritmo de Dijkstra O(n2)

Dado un grafo, obtener el camino más corto para llegar desde un vértice hasta otro:

V Conjunto de vértices del grafo (p.ej. ciudades)


S Conjunto de vértices cuya distancia más corta desde el origen ya se conoce
D[i] Distancia más corta entre el origen y el vértice i.
C[i,j] Coste de llegar desde el nodo i hasta el nodo j (p.ej. kilómetros)
P[i] Vértice anterior a i en el camino más corto desde el origen hasta i
(para poder reconstruir el camino más corto)

S = { origen }

Para todo i
D[i] = C[origen,i]
P[i] = origen

Mientras S ≠ V
Elegir w ∈ V – S tal que D[w] sea mínimo
S = S ∪ {w}
Para cada vértice v ∈ V – S
Si D[v] > D[w] + C[w,v] entonces
D[v] = D[w] + C[w,v]
P[v] = w

Análisis de Algoritmos 12 © Fernando Berzal


Programación dinámica
Cuando la solución óptima de un problema se puede construir a partir de las
soluciones óptimas de subproblemas del mismo tipo.

p.ej. Cuando la implementación recursiva de un algoritmo


repite muchas veces las mismas operaciones.

Estrategia de programación dinámica


- Trabaja etapa por etapa.
- Compara resultados con los ya obtenidos (uso masivo de memoria).

Uso de memoria O(f(n)) è Tiempo de ejecución O(nf(n))

Ejemplos

Sucesión de Fibonacci: Solución iterativa

fib( n )= fib( n − 1) + fib( n − 2)

Números combinatorios

 n   n − 1  n − 1
  =   +  
  
k k − 1   k 

Problema del camino mínimo (algoritmo de Floyd)

Dk (i, j ) = min{Dk −1 (i , j ), Dk −1 (i, k ) + Dk −1 ( k , j )}

Variante: “Memoization”
IDEA: No calcular dos veces lo mismo

Cada vez que se obtiene la solución de un subproblema,


ésta se almacena en una tabla para no tener que volver a calcularla.

Aplicaciones
Reconocimiento de voz (DTW: Dynamic Time Warping).

Análisis de Algoritmos 13 © Fernando Berzal

S-ar putea să vă placă și