Está en la página 1de 49

Análisis de eficiencia

1. Introducción al análisis de la eficiencia.

Dados dos algoritmos que resuelven el mismo problema de


forma diferente:
¿Cuál es el mejor?
¿Cómo se decide que un algoritmo es mejor que otro?

Posibles respuestas:

• El que consuma menos memoria (número de variables,


número y tamaño de estructuras de datos usadas).

• El que vaya más rápido (midiendo el tiempo que tarda


en ejecutarse o el número de operaciones elementales
que realiza).

• El que funcione adecuadamente.

• El más fácil para el humano de leer, escribir o


entender.

¿Por qué deberíamos preocuparnos por la calidad de un


algoritmo, si lo único que tenemos que hacer para que un
programa vaya más rápido es utilizar un ordenador con
más potencia, o si éste consume mucha memoria, añadirle
algunos módulos de memoria más?
Problema:

El aumento de las prestaciones no implica una clara mejora


en el tiempo de ejecución.

Ejemplo:

Ejecución de un programa con n datos en 10-42n segundos:

• Si n=10, el programa terminará en 0'1 segundos.


• Si fuera 20, aproximadamente 10 minutos;
• 30, un día y
• 40, un año.

Si cambiamos el ordenador que estamos usando por uno


100 veces más rápido:

• Si n=45 datos, en un año.

Sólo se mejoraría de manera importante si se modificara el


diseño del programa, sustituyendo este algoritmo anterior
por uno más eficiente.

2
¿Cómo podemos realizar el estudio de la eficiencia de un
algoritmo?

• Empíricamente: programar los algoritmos y ejecutarlos


varias veces con distintos datos de entrada.

• Teóricamente: determinar matemáticamente la cantidad


de recursos (tiempo de ejecución y memoria) requeridos
por la implementación en función del tamaño de la
entrada.

Problemas del estudio empírico:

• Dependencia de los resultados obtenidos del tipo de


ordenador,
• del lenguaje de programación usado y
• del traductor con el que se obtenga el código
ejecutable, y de la pericia del programador.

(si cambiamos alguno de estos elementos


probablemente se obtengan resultados diferentes, con
lo cual no podemos establecer una eficiencia empírica
absoluta).

• Existen algoritmos que pueden ser comparados con


esta técnica sólo cuando el número de datos con los
que se ejecutan es relativamente pequeño.

3
Ejemplo: el problema del viajante de comercio.

(encontrar una ruta que una un número de ciudades pero


con coste mínimo, por ejemplo en kilómetros a recorrer.)

10 ó 20 poblaciones: solución en pocos segundos.

100 poblaciones: no existe ordenador para encontrar


la solución en un tiempo razonable.

Ventajas del estudio teórico:

Independencia con todos los factores anteriores.

Genérico para cualquier entrada

Depende sólo de las instrucciones que componen el


algoritmo y del tamaño del problema.

Ofrece como salida una expresión matemática que


indica cómo se produce el crecimiento del tiempo
de ejecución conforme aumenta el tamaño de
entrada.

• De manera híbrida: realizar un estudio teórico y


posteriormente estimar ciertos parámetros de forma
empírica.

4
La entrada de un problema y su tamaño.

• Algoritmo de ordenación de un vector de n enteros: n


es el tamaño de los datos de entrada.

• Algoritmo para el cálculo del factorial de un entero: el


valor de ese número entero es el que nos determinaría
el tamaño de nuestro problema.

• Algoritmo que resuelve el problema del viajante de


comercio: el número de ciudades y el de carreteras que
unen a estas.

De manera general, el tamaño de la entrada de un problema


vendrá dado en función de un número entero que nos mide
el número de componentes de dicho ejemplo.

No existe una regla exacta para determinar el tamaño de un


problema, sino que tendrá que obtenerse según las
características de cada uno.

5
2. El tiempo de ejecución de un algoritmo y su orden
de eficiencia.

T(n)
Unidades de tiempo (segundos, milisegundos,...) que un
algoritmo tardaría en ejecutarse con unos datos de entrada
de tamaño n.

Pero...
... tiempo de ejecución depende

• del ordenador utilizado


• traductor con el que se genere el código objeto

Entonces...
...T(n) representará

• número de instrucciones simples (asignaciones,


comparaciones, operaciones aritméticas,...) ejecutadas.

O de manera equivalente,

• el tiempo de ejecución del algoritmo en un ordenador


idealizado, donde cada una de las instrucciones simples
consume una unidad de tiempo.

T(n) no se expresa con unidades y n≥0 y T(n) es positivo

6
Principio de invarianza:

Dos implementaciones distintas de un mismo algoritmo,


que toman t1(n) y t2(n) unidades de tiempo,
respectivamente, para resolver un problema de tamaño n,
no diferirán en eficiencia en más de una constante
multiplicativa.

Existe un c>0 tal que t1(n) ≤ ct2(n).

Lo que implica que ....

Un programa vaya 10 ó 1000 veces más rápido cambiando


de máquina,

pero sólo un cambio de algoritmo

permitirá obtener una mejoría mayor cuanto más crezca el


tamaño de los ejemplos, lo que nos llevará a ignorar las
constantes multiplicativas a todos los efectos.

7
Tiempos de ejecución en diferentes situaciones:

Un algoritmo se puede ejecutar con datos de entrada


diferentes.

• Hay datos con los que tarde más, y


• Datos con los que acabe antes.

Hay que indicar a qué situación pertenece la expresión T(n)


calculada:

• Peor caso: límite superior en el tiempo (máximo


valor). Generalmente es pesimista.

• Mejor caso: límite inferior en el tiempo (siempre se


ejecutará por encima de dicho tiempo).

• Caso promedio: media ponderada de ambos casos.

Salvo que se diga lo contrario, siempre trabajaremos con el


el peor caso.

8
Ejemplo 1:

algoritmo de ordenación por selección.

Vector ordenado de forma creciente (mejor caso)


Vector ordenado de manera decreciente (peor caso)

Ambos tiempos coinciden más o menos

Ejemplo 2:

algoritmo de ordenación por inserción.

Vector ordenado de forma creciente proporcional a n.


Vector ordenado de forma decreciente proporcional a n2

Los tiempo de ambos son diferentes

9
¿Cómo se calcula el tiempo de ejecución de un algoritmo?

sobre la base de las instrucciones lo componen

• Tiempo de ejecución(Evaluación de una expresión) =

lo que se tarde en realizar las operaciones que contenga

• Tiempo de ejecución (asignación a una variable simple


de una expresión) =

tiempo de evaluación de la expresión


+
tiempo de asignación

• Tiempo de ejecución (operación de escritura de una


expresión) =

tiempo de evaluación de la expresión


+
tiempo del proceso de escritura.

• Tiempo de ejecución (lectura de una variable) =

constante

10
• Tiempo de ejecución (secuencia de instrucciones) =

la suma de los tiempos de cada una de las instrucciones.

• Tiempo de ejecución (una sentencia condicional)

Tiempo de evaluar la condición


+
máximo de los costes del bloque entonces y del bloque sino.

• Tiempo de ejecución (bucle)

(tiempo del cuerpo del bucle


+
tiempo de evaluar la condición del bucle)
*
número de iteraciones

Operaciones elementales: todas aquellas cuyo tiempo de


ejecución esté limitado superiormente por una constante
(sólo depende de la implementación)

Ejemplo: operaciones de suma, multiplicación, resta,


división, módulo y similares.

11
Ejemplo: fución para calcular el valor mínimo de un vector.

/* 1 */ public int buscarMinimo(int vector [],


int n)
/* 2 */ {
/* 3 */ int j, min;
/* 4 */ min= 0;
/* 5 */ for (j=1; j<n;j++)
/* 6 */ if (vector[j] < vector[min])
/* 7 */ min= j;
/* 8 */ return min;
/* 9 */ }

T(n)= ca + (ci + ca+ ce)(n-1) + cr.

T(n)= 1+3*(n-1) + 1= 3n-1

12
Eficiencia

la forma en que el tiempo de ejecución (y en general


cualquier recurso) necesario para procesar una entrada de
tamaño n crece cuando se incrementa el valor de n.

Se especifica mediante una función de crecimiento

Orden de una función

Se dice que un algoritmo necesita un tiempo de ejecución


del orden de una función cualquiera f(n), cuando existe una
constante positiva c y una implementación del algoritmo
que resuelve cada instancia del problema en un tiempo
acotado superiormente por cf(n), siendo la función f(n) la
que marca cómo crecerá dicho tiempo de ejecución cuando
aumente n.

Un algoritmo será, por tanto, más eficiente que otro si el


tiempo de ejecución del peor caso tiene un orden de
crecimiento menor que el segundo.

13
Ejemplo:

Dos algoritmos ,A y B, resuelven un problema mediante las


funciones TA(n)=100n y TB(n)=2n2, respectivamente.

¿Cuál deberíamos usar?

Si n<50, B es mejor que A.

Si n≥50, A es mejor que B,


• si n= 100, A es el doble de rápido que B
• si n=1.000, A es 20 veces más rápido que B

¡OJO!
más importante la forma de las funciones (n y n2),
que las constantes (2 y 100).

En vez de decir que el tiempo de ejecución del algoritmo es


T(n)=4n+3, por ejemplo, diremos que T(n) es del orden de
n, lo que implica que el tiempo de ejecución es alguna
constante multiplicada por n, para tamaños de problemas
de dimensión n.

14
Las funciones que nos marcan más comúnmente el orden
de crecimiento de los algoritmos y los nombres por las que
se les conocen son las siguientes:

f(n)= 1 => Constante.

f(n)= log n => Logarítmica.

f(n)= n => Lineal.

f(n)= nlog n => Quasilineal.

f(n)= n2 => Cuadrática.


f(n)= n3 => Cúbica, y en general,
f(n)= nk => polinómica.

f(n)= kn => Exponencial.

15
• Algoritmo eficiente: como mucho un coste
quasilineal.

• Mejor orden es el logarítmico: doblar el tamaño del


problema, no afecta al tiempo y doblar el tiempo
permite tratar problemas enormes.

• Órdenes quasilienal y lineal: duplicar el tamaño del


problema se duplica aproximadamente el tiempo
empleado, y análogamente, al duplicar el tiempo se
pueden tratar problemas aproximadamente del
doble de tamaño.

• Órdenes polinómicos: se necesita mucho tiempo


para resolver un problema que ha crecido
relativamente poco en tamaño.

• Problemas de orden polinomial se consideran


tratables.

16
Crecimiento de algunas funciones conforme crece el tamaño
n del problema:

log n n nlog n n2 n3 2n
3 10 33 100 1.000 1.024
7 100 664 10.000 1.000.000 1'267650600228e+30
0 1.000 9.966 1.000.000 1.000.000.00 1'07150860718e+301
0
3 10.000 132.877 100.000.000 1'0e+12 Enorme
7 100.000 1.660.964 10.000.000.000 1'0e+15 Más enorme
10 1.000.000 19.931.569 1.000.000.000.000 1'0e+18 Sin comentarios

Representación gráfica de estas funciones.

17
Los órdenes de eficiencia son clases de equivalencia
(basadas en el principio de invarianza):

T1(n)= 2n3 + 4n2 + 5 y T2(n)= n3- 4n,

Pertenecen a la misma clase cuya


representante es la función f(n)= n3.

Otros criterios para elegir o diseñar un algoritmo:

• Si un programa se va a ejecutar pocas veces, el costo


de escritura es el principal, no influyendo en dicho
costo el tiempo de ejecución, debiéndose elegir un
algoritmo cuya aplicación sea la más fácil.

• Tamaño de las entradas: la velocidad de crecimiento


del tiempo de ejecución puede ser menos
importante que las constantes multiplicativas

Ejemplo:

Dos algoritmos con T(n)= 100n2 y 5n3,


respectivamente.

Para n < 20, el segundo programa será más rápido


que el primero si el programa se va a ejecutar
normalmente con entradas menores que 20,
conviene ejecutar el segundo.

18
3. Las notaciones asintóticas.

Las notaciones 0 mayúscula y o minúscula.


O(f(n)), representa el conjunto de funciones g que crecen
como mucho tan rápido como f, o lo que es lo mismo, las
funciones g tales que f llega a ser en algún momento una
cota superior para g.

g(n) ∈ O(f(n)) (g es del orden de f).

Sea f:N→R+∪{0} una función cualquiera, el conjunto de las


funciones del orden de f(n), notado como O(f(n)), se define:

O(f(n)) = { g ∃c0 ∈ R+ y ∃n0 ∈ N, ∀ n ≥ n0 g(n) ≤ c0f(n) }

• T(n)= 3n + 2 ∈ O(n), ∃ n0= 2, y c0=4 / 3n+2 ≤ 4n.

• T(n)= 1.000n2 + 100n - 6 ∈ O(n2), ∃n0= 100, y c0=1.001


/ 1.000n2 + 100n - 6 ≤ 1.001n2.

• T(n) = 6·2n + n2 ∈ O(2n), ∃ n0= 4, y c0=7 / 6·2n + n2 ≤


7·2n.

• T(n) = 3n ∉ O(2n). Supongamos ∃ n0 y c0 / ∀ n≥n0, 3n


≤ c02n. Entonces c0 ≥ (3/2)n ∀ n≥n0, pero no ∃
constante suficientemente grande que (3/2)n para
todo n.

19
La notación asintótica del orden generalizada a dos
parámetros es la siguiente: sea f:N x N→R+∪{0} una función
cualquiera, el conjunto de las funciones del orden de f(n,
m), notado como O(f(n, m)), se define como sigue:

O(f(n,m)) = { g ∃c0 ∈ R+ y ∃n0,m0∈ N, ∀ n ≥ n0 y m ≥ m0,


g(n,m) ≤ c0f(n,m) }

Propiedades de O(f), ∀ funciones f, f', g, h:

[Invarianza multiplicativa].
• ∀c ∈ R+, g ∈ O(f) si y sólo si c·g ∈ O(f)

[Invarizanza aditiva]
• ∀c ∈ R+, g ∈ O(f) si y sólo si c+g ∈ O(f)

[Reflexividad]
• f ∈ O(f)

[Transitividad]
• Si h ∈ O(g) y g ∈ O(f) entonces h ∈ O(f)

[Criterio de Caracterización].
• g ∈ O(f) si y sólo si O(g) ⊆ O(f)

20
[Regla de la suma]
• Si g ∈ O(f) y h ∈ O(f'), entonces g + h ∈ O(max(f, f'))

Demostración:

Si g(n) ∈ O(f(n)) entonces ∃c1, n1 / g(n) ≤ c1f(n),∀n ≥ n1.


Si h(n) ∈ O(f'(n)) entonces ∃c2, n2 / h(n) ≤ c2f'(n), ∀n ≥ n2.
∀n ≥ max(n1, n2), f(n)+f'(n) ≤ (c1 + c2) max(f(n), f'(n)).

Dos trozos de código independientes, con eficiencias


O(f(n)) y O(f'(n)), la eficiencia del trozo completo será
O(max(f(n), f'(n)).

Ejemplo:

O(n2),
O(n3)
O(nlgn)

Orden del tiempo de ejecución de los tres:

O(max(n2, n3, nlgn))= O(n3).

21
[Regla del producto].

• Si g ∈ O(f) y h ∈ O(f'), entonces g·h ∈ O(f·f')

Demostración:

Si g(n) ∈ O(f(n)) ∃c1, m1 / g(n) ≤ c1f(n), ∀ n ≥ n1.


Si h(n) ∈ O(f'(n)) ∃c2, m2 / h(n) ≤ c2f'(n), ∀ n ≥ n2.
∀ n≥ n1·n2 se tiene que f(n)·f'(n) ≤ (c1·c2) f(n)· f'(n).

Dos trozos de código anidados (no independientes),


con eficiencias O(f(n)) y O(f'(n)), la eficiencia del trozo
completo es O(f(n)*g(n)).

Ejemplo:

O(n)
O(lg n)

Orden del código completo pertenecerá a

O(nlgn).

22
La notación o minúscula es también una cota superior:

o(f(n)) = { g ∀c0 ∈ R+ y ∃n0 ∈ N, ∀ n ≥ n0 g(n) ≤ c0f(n)


}

Se cumple la siguiente cadena de inclusiones:

O(1) ⊂ O(log n) ⊂ O(n) ⊂ O(n2) ⊂ ... ⊂ O(na) ⊂...


...⊂ O(2n) ⊂ O(n!)

23
Las notaciones Ω

Ωk y Ω∞, dan cotas inferiores en el orden de eficiencia:

Ωk(f(n)) = { g ∃c0 ∈ R+ y ∃n0 ∈ N, ∀ n ≥ n0 g(n) ≥ c0f(n) }


Ω∞(f(n)) = { g ∃c0 ∈ R+ y ∃n0 ∈ N, ∃ n ≥ n0 g(n) ≥ c0f(n) }

Si g(n) ∈ Ωk(f(n)) entonces f es una cota inferior de g desde


un punto en adelante, o lo que es lo mismo, ofrece un
mínimo del cual nunca baja (g crece más deprisa que f). Por
otro lado, si g(n) ∈ Ω∞(f(n)), entonces en una cantidad
infinita de ocasiones g crece lo suficiente como para
alcanzar a f.

Las notaciones Θ.

Esta notación define las funciones con la misma tasa de


crecimiento (crecen al mismo ritmo) que f, es decir, Θ(f) =
O(f) ∩ Ωk(f). Formalmente:
Θ (f(n)) = { g ∃c0, c1∈ R+ y ∃n0 ∈ N, ∀ n ≥ n0,
0 ≤ c0f(n) ≤ g(n) ≤ c1f(n) }

24
Relaciones entre las diferentes notaciones.

25
4. Cálculo del tiempo de ejecución de un algoritmo
iterativo y de su orden de eficiencia.

Objetivo: calcular el orden de eficiencia de un algoritmo.

Dos formas diferentes:


• Obtener el tiempo de ejecución del programa, y
posteriormente calcular su orden de eficiencia.

• Ir calculando el orden de eficiencia de las diferentes


sentencias y bloques existentes en el programa.

Sentencias simples.

Cualquier sentencia simple (lectura, escritura, asignación,


...) tardará un tiempo O(1), ya que el tiempo constante que
tarda realmente la ejecución de esa sentencia se puede
acotar por una constante que multiplica a la función f(n)=1.

Esto podremos hacerlo siempre y cuando no intervengan en


dicha sentencia ni variables estructuradas,ni operandos
aritméticos que dependan del tamaño del problema.

26
/*1*/ public void main()
/*2*/ {
/*3*/ int a, b, c;
/*4*/ System.out.println("Introduzca los dos números a
sumar: ");
/*5*/ a= Console.in.readInt();
b= Console.in.readInt();
/*6*/ c= a+b;
/*7*/ System.out.println("La suma de”+a+”y”+b+”es “+c);
/*8*/ }

1) O(1+1+1+1) = O(max(1,1,1,1))= O(1) (regla de la suma)

2) T(n)= te+tl+toa+te= c,

te = el tiempo que se tarda en ejecutar una escritura;


tl,= el de la lectura y
toa = el que se tarda en hacer una operación aritmética
más una asignación.

T(n)=c ∈ O(1).

27
Sentencias de repetición.

* En bucles controlados por contador:

/*1*/ for (i=0; i<n;i++)


/*2*/ System.out.println(" "+ i);

• La sentencia /*2/ tiene un tiempo de ejecución te.


• El ciclo se repite n veces T(n) =(tc +te) * n ∈ O(n)
(tc el tiempo de evaluación de la condición más el
incremento)

/*1*/ for (i=0; i<n;i++)


/*2*/ for (j=0; j<n;j++)
/*3*/ matriz[i][j]= 0;

• La línea 3 tiene un orden de ejecución constante;


• El bucle de la línea 2 se repite n veces O(n),
• El ciclo de la primera línea O(n).
• Aplicando en este caso la regla del producto: O(n2).

Obteniendo el tiempo de ejecución del bloque completo :

• T2-3(n) = n(ta + tc), para las sentencias 2 y 3, y


• T1-3(n)= n(nta+ntc) + tc = tan2 +tcn2 +tc ∈ O(n2)

Si la dimensión fuera n x m, en este caso, el orden de


eficiencia sería O(n·m).

28
* Bucles controlados por centinela.

/*1*/ i=0;
/*2*/ while (i<n && vector[i] != valor)
/*3*/ i++;

(Se supone el peor caso: recorrer todo el vector)

Así, el bucle while repetirá n veces una sentencia de O(1),


por lo que su orden de eficiencia será n.

• Despreciamos el tiempo constante de la evaluación


de la expresión del bucle.
• Para bucles do...while la forma de proceder sería la
misma.

Sentencias condicionales.

El tiempo de ejecución será el tiempo de evaluación de la


condición, más el máximo del bloque a ejecutar cuando la
condición se evalúe como verdadera y del bloque a ejecutar
cuando se evalúe como falsa.

Cuando estamos tratando con órdenes, el de la evaluación


de la expresión booleana, que es constante, se desprecia.

• Bloque then tiene una eficiencia O(f(n))


• Bloque else posee una eficiencia O(g(n))
• el orden de eficiencia del if será O(max(f(n), g(n)).

29
/*1*/ if (n> m)
/*2*/ n= n *m;
/*3*/ else
/*4*/ for (i=0; i<n;i++)
/*5*/ m= m * n;

• Orden del else = el del bucle for: O(n),


• Orden del then: O(1),
• Orden del if = O(max(n,1))= O(n)

• T(n) = te + max(tea, ntea)= te + ntea ∈ O(n),


• te corresponde con el tiempo de evaluar la condición
• tea, el correspondiente a la asignación más la suma.

30
Llamadas a funciones no recursivas.

• Calcular el tiempo de ejecución de aquellas que no


llaman a otras.
• Calcular el tiempo de ejecución de las que llaman a
otras a partir de sus tiempos correspondientes.

Dependiendo de dónde estén situadas las llamadas a


funciones, se deberá incluir su tiempo de ejecución de una u
otra manera:

• En una asignación: T(n) asignación = T(n) de la


función. Si hay más de una función invocada en la
sentencia:
T(n) asignación = T(n) función1+ ...+T(n) funciónN.
(o el máximo de los órdenes)

• En una condición de un bucle (cualquier tipo):


T(n) bucle = (T(n) función + T(n) cuerpo) * numIter

• En la condición de una sentencia condicional:


T(n) if = T(n) función + T(n) if

• En una secuencia de sentencias: simplemente aplicar


la regla de la suma, si tratamos con órdenes, o
sumar tiempos si son tiempos de ejecución.

31
Ejemplos

public void Burbuja (int vector [], int n)


{
int i, j;
/*1*/ int aux;
/*2*/ for (i = 0; i < n - 1; i++)
/*3*/ for (j = n-1; j< i; j--)
/*4*/ if (vector[j-1] > vector[j])
/*5*/ {
/*6*/ aux= vector[j-1];
/*7*/ vector[j-1]= vector[j];
/*8*/ vector[j]= aux;
/*9*/ }
}

Bucle for de la línea 3:


If de la línea 4:
Then de la línea 5:
Tres sentencias O(1)
O(1) , por la regla de la suma.
O(1)
O(n-i), ya que se repite n-i veces.

El bucle de la línea 2: se ejecutará n-1 veces, por lo que el


tiempo de ejecución del bucle será una constante
multiplicada por:



− = = − ∈ O(n2).
=

32
public long SumaSubsecuenciaMaxima(int vector[], int n)
{
int i, j, k;
/*1*/ int sumMax=0, posInicioSec=0,
posFinSec=0, sumaActual;
/*2*/ for (i=0;i<n; i++)
/*3*/ for (j=i;j<n; j++)
/*4*/ {
/*5*/ sumaActual=0;
/*6*/ for (k=i;k<j; i++)
/*7*/ sumaActual+= vector[k];
/*8*/ if (sumaAcutal > sumaMax)
/*9*/ {
/*10*/ sumaMax= sumaActual;
/*11*/ posInicioSec= i;
/*12*/ posFinSec= j;
/*13*/ }
/*14*/ }
/*15*/ return sumMax;
}

Entre las líneas 4 y 14:


• Asignación línea 5: O(1);
• Condicional línea 8: O(1).
• Bucle línea 6: repetirá j-i+1 veces una asignación, lo
que implica que pertenecerá a O(j-i+1). (Peor de los
caso, se repite n veces O(n)).

O(n), aplicando la regla de la suma.

El bucle de la línea 3, en el peor de los casos ejecutará n


operaciones de O(n) O(n2). Y el de la línea 2, n veces
O(n2) O(n3) .

33
El tiempo de ejecución de la función vendrá dada por:
= ,
= = =

− + − +
Segunda sumatoria: − + = ,
=

Tercera sumatoria (primer bucle):

− + − +
= − + + + + =
= = = =
+ + + + + + +
= − + + = ∈

Teniendo en cuenta el orden de complejidad de la función,


y observando la implementación, nos damos cuenta que se
podría evitar ese orden si se elimina un bucle for,
consiguiendo un orden de eficiencia cuadrático.

De hecho, se puede reducir más su orden de complejidad


mediante un algoritmo recursivo a O(nlgn), tarea que le
proponemos al lector.

34
5. Cálculo del tiempo de ejecución de algoritmos
recursivos y de su orden de eficiencia.

Cuando se analiza la eficiencia de un programa recursivo,


suele ocurrir habitualmente que las funciones del tiempo de
ejecución que se obtengan sean también recursivas
relaciones recurrentes o recurrencias.

Objetivo: buscar una expresión de T(n) no recursiva.

public long factorial (long n)


{
/*1*/ if (n <= 0) return 1;
/*2*/ else return n * factorial(n-1);
}

Si n ≤ 1, T(n)= c y si n > 1, se cumplirá que T(n) = d +T(n-1).

¿Cómo resolvemos esta recurrencia?

Mediante la expansión de la misma: sustituyendo T(n-1) por


su valor correspondiente, y así sucesivamente hasta que se
elimine la recursividad

T(n) = d + T(n-1)= d + d+ T(n-2)= d + d + d + T(n-3).

i pasos: T(n)=i · d+T(n-i), n>i.

Para i=n-1, T(n) = (n-1)d + T(n-(n-1)) =


= (n-1)d + T(1)= (n-1) d+c ∈ O(n).

35
public void OrdenarVector (int vector[], int n)
{
int i, maxPos;
if (n>1)
{
maxPos=0;
for (i=1; i<n;i++)
if (vector[i] > vector[maxPos])
maxPos= i;
if (maxPos != 0)
{
i= vector[0];
vector[0]= vector[maxPos];
vector[maxPos]= i;
}
OrdenarVector(vector, n-1);
}
}

Ecuación de recurrencia:
Si n ≤ 1, T(1)=1, si no, T(n) = T(n-1) + n.

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

y en general:

= − + − ≥ + .
=


Para i=n-1, = + −
=


+
= + + + = − = ∈
=

36
6. Resolución de recurrencias homogéneas.

Las recurrencias lineales homogéneas tienen la forma:


a0tn + a1tn-1 + ... + aktn-k = 0 [1]

donde los ti son los valores buscados (aplicamos el adjetivo


de lineal por que no hay términos de la forma titi+j ó ti2, por
ejemplo), los coeficientes ai son constantes y la recurrencia
es homogénea por que la combinación lineal de los ti es
igual a 0.

Soluciones buscadas de la forma tn=xn, donde x es una


constante. Si sustituimos esta solución en [1] obtenemos:

a0xn + a1xn-1 + ... + ak xn-k = 0 [2]

Esta ecuación tiene dos soluciones:

x=0, que no nos sirve, y

a0xk + a1xk-1 + ... + ak= 0,


ecuación característica asociada a la ecuación recurrente inicial.

Supongamos que las k raíces de esta ecuación característica


son r1, r2, ..., rk, entonces cualquier combinación lineal
= de términos rin es solución para la ecuación
=

recurrente lineal homogénea.

37
=
= =
− + − ≥

Cambio de notación [1]: tn= 3tn-1 - 4tn-2 => tn - 3tn-1 - 4tn-2 = 0

Hacer la sustitución tn=xn: xn - 3xn-1 - 4xn-2 =0

Dividimos cada término por xn-k= xn-2 , con objeto de


eliminar la solución trivial y conseguimos la ecuación
característica:
x2-3x-4=0

Resolver la ecuación: raíces -1 y 4.

La solución general a la expresión recursiva tendrá la forma


de la expresión [2], donde los ri son las soluciones de la
recurrencia.
tn= c1(-1)n + c24n

Sustituyendo n=0 y n=1 en la ecuación anterior, llegaremos


al siguiente sistema de dos ecuaciones con dos incógnitas
(en este caso c1 y c2):

c1 + c2 = 0 y -c1 + 4c2 =1

Solución: c1= -1/5 y c2= 1/5,

Sustituyendo: tn= (1/5)(4n - (-1)n) ∈ O(4n).

38
Dada la recurrencia:

tn= tn-1 + tn-2, n≥2, y t0= 0, t1= 1

Cambiando de notación, sustituyendo tn=xn y dividiendo


por xn-k= xn-2 se obtiene la ecuación característica:

tn-tn-1-tn-2=0 => x2-x-1=0

Soluciones a la ecuación:

r1= (1+51/2)/2 y r2=(1-51/2)/2.

Sustituyendo las condiciones iniciales n=0 y n=1, en la


expresión que nos da la solución, tn=c1r1n +c2r2n,, obtenemos
el sistema de dos ecuaciones con dos incógnitas:

c1 + c2 = 0 y c1r1 + c2r2 = 1.

Al solucionarlo,

c1=1/(51/2) y c2=-1/(51/2).

Sustituir c1 y c2 en la expresión de la solución de la


recurrencia.

T(n) ∈ O((1/(51/2))n)

39
Dada la recurrencia:

tn= 5tn-1 - 8tn-2 + 4tn-3, n≥3, t0=0, t1=1, t2=2

La ecuación característica es:

x3 - 5x2+ 8x - 4= 0

Soluciones a la ecuación anterior:

• 1, con multiplicidad 1, y
• 2, con multiplicidad 2,
• es decir, (x-1)(x-2)2 =0.

La solución general será, por tanto:

tn= c11n + c22n + c3n2n

(Se tantos sumandos como multiplicidades tengan las raíces


múltiples: si m es la multiplicidad de una raíz r, entonces se
añadirán los sumandos rn, nrn, n2rn,..., nm-1rn, multiplicados
por sus correspondientes constantes)

Dadas las condiciones iniciales, se plantean las ecuaciones


de un sistema lineal:
c1 + c2= 0, c1 + 2c2 + 2c3= 0 y c1 + 4c2 + 8c3= 0,

Soluciones: c1= -2, c2= 2 y c3=-1/2

tn= 2n+1 - n2n-1 - 2 ∈O(2n)


40
7. Resolución de recurrencias no homogéneas.

Las recurrencias:
a0tn + a1tn-1 +...+ aktn-k = bnp(n) [3]

Donde p(n) es un polinomio de grado d, y b es una


constante (el resto es igual que [1]).

Resolver la ecuación característica:

(a0xk + a1xk-1 +...+ ak)(x-b)d+1=0 [4]

Donde (a0xk + a1xk-1 +...+ ak) es la aportación de la parte


homogénea y (x-b)d+1 de la parte no homogénea.

Una vez en este punto, se concluye el proceso dando los


mismos pasos que en la solución de recurrencias
homogéneas.

41
Dada la recurrencia:

tn= 1 + tn-1 + tn-2 => tn - tn-1 - tn-2 = 1

Como 1= 1np(n), siendo p(n)=1 con grado d=0 y b=1.

Entonces, la ecuación característica quedaría:

(x2 -x-1)(x-1)=0

Dada esta otra recurrencia:

tn - 2tn-1 = 3n

En este caso, b=3, p(n) = 1 y, por tanto, d=0, obteniendo la


ecuación característica:

(x-2)(x-3)=0

Dada esta tercera recurrencia:

tn - 2tn-1= (n+5)3n

En este caso, b=3, p(n)= n+5, con d=1. La ecuación


característica sería:

(x-2)(x-3)2=0

42
La recurrencia T(n) = 2T(n-1) + n, n ≥1 y T(0) = 0.

Cambiando de notación, tendremos la siguiente ecuación


lineal no homogénea:

tn - 2tn-1 = n

Identificamos b=1 y p(n)=n, siendo d=1, con lo que se


obtiene la ecuación característica siguiente:

(x-2)(x-1)2 = 0

Por tanto, la solución es:

tn= c12n + c21n + c3n1n = c12n + c2 +c3n

Con t0, se obtienen t1 y t2, y se sustituyen en la ecuación


anterior, obteniendo el siguiente sistema de tres ecuaciones
con tres incógnitas:

c1 + c2 = 0, 2c1 + c2 + c3 = 1, 4c1 + c2 + 2c3 = 4

Las soluciones son c1 = 2, c2 = -2 y c3 = -1, con lo que


finalmente:

tn =2·2n - n -2 = 2n+1 - n - 2 ∈ O(2n+1)

43
De esta misma manera, se pueden resolver recurrencias de
la forma:

a0tn + a1tn-1 +...+ aktn-k = b1np1(n) + b2np2(n) + b3np3(n) +... [5]

Siendo d1 el grado de p1(n), d2 el de p2(n) y así


sucesivamente.

Las ecuaciones características tendrán el siguiente patrón:

(a0xk + a1xk-1 + ...+ ak)(x - b1)d1+1(x - b2)d1+2...= 0 [6]

Dada la recurrencia:

T(n)= 2T(n-1) + n + 2n, n≥1 y T(0)= 0.

En primer lugar cambiemos la notación y reorganicemos la


expresión:
tn = 2tn-1 + n + 2n => tn - 2tn-1 = n + 2n

La parte derecha de la igualdad debemos expresarla de la


forma:
n + 2n = b1np1(n) + b2np2(n)

Cosa que conseguiremos si identificamos b1= 1, p1(n)= n


(grado d=1) y b2= 1 , p2(n)= 2n (d=1). De esta manera,
podemos obtener la ecuación característica:

(x-2)(x-1)2(x-1)2 = 0

44
Su solución tiene la forma:

tn=c11n + c2n1n + c3n21n + c4n31n +c52n =


c1 + c2n + c3 n2 + c4n3 + c52n

Sustituyendo las condiciones iniciales y resolviendo el


sistema de cuatro ecuaciones con cuatro incógnitas,
concluiremos que T(n) ∈ O(2n).

45
8. Resolución de recurrencias mediante cambio de
variable.

+ ≥
=
=

Debido a que el tamaño del problema se divide en dos


suponemos que n es potencia de dos, por lo que podemos
hacer n=2m, quedando:

T(2m)= T(2m-1) +1 , m ≥ 1, siendo el caso base T(20)= 1.

Realizamos varias expansiones:

T(2m)= T(2m-1) +1 =T(2m-2) + 1 + 1 = T(2m-3) + 1 + 1 + 1,

y en general:

T(2m)= T(2m-i)+i, m ≥ i

Particularizando para m=i para así poder eliminar la


recurrencia, tendríamos:

T(2m)= T(20) + m = m + 1

Como m= lg n, deshaciendo el cambio, finalmente


concluimos que T(n)= lgn + 1 ∈ O(lgn).

46
Un segundo ejemplo donde n es una potencia de 2,
corresponde a la resolución de la recurrencia:

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

Al hacer el mismo cambio de variable que en el ejercicio


anterior, se llega a

T(2m)= 4T(2m-1) + 4m,

O lo que es lo mismo:

tm= 4tm-1 + 4m

La ecuación característica es:

(x-4)2=0

Y la solución buscada tendrá la forma:

tm= c14m + c2m4m.

Deshaciendo los cambios, el tiempo de ejecución es

T(n)= c1n2 + c2n2lg n ∈ O(n2 lg n).

47