Está en la página 1de 33

Universidad Nacional Micaela Bastida de Apurímac

Departamento de Ingeniería

Escuela Académico Profesional de


Ingeniería Informática y Sistemas

MANUAL PARA EL ANÁLISIS DE LA


COMPLEJIDAD DE LOS ALGORITMOS

Curso: Algorítmica III

Docente: Ing. Erech, Ordoñez Ramos

Abancay - 2013.
Contenido

Introducción ....................................................................................................................................... 3

Objetivo del manual ........................................................................................................................... 4

Eficiencia y complejidad .................................................................................................................... 4

Criterios para medir la eficiencia y complejidad de un algoritmo ................................................. 4

Eficiencia temporal ........................................................................................................................ 4

Operaciones Elementales (OE) ...................................................................................................... 6

Reglas generales para el cálculo del número de OE ...................................................................... 7

Análisis de mejor, peor y caso medio del algoritmo ...................................................................... 9

Asíntotas........................................................................................................................................... 16

Propiedades de los Conjuntos O(f(n)) .......................................................................................... 17

Órdenes de complejidad ................................................................................................................... 20

Complejidad de algoritmos recursivos ............................................................................................. 21

Clasificación de funciones recursivas .......................................................................................... 22

Ecuaciones de Recurrencia............................................................................................................... 23

Bibliografía ...................................................................................................................................... 33

Pág. 2
Complejidad de los algoritmos recursivos

Introducción

Este Manual esta dirigido a todos los estudiantes de la Escuela Académico

Profesional de Ingeniería Informática y Sistemas, quienes como parte de su formación

profesional, hacen cálculos de la complejidad de los algoritmos y así saber su eficiencia,

es decir el tiempo que tardará en ejecutarse y mejorar el mismo si es factible.

Por lo tanto el presente Manual nos ha de permitir medir de alguna forma el costo

(en función del tiempo) que consume un algoritmo para encontrar la solución y nos

permitirá la posibilidad de comparar distintos algoritmos que resuelven un mismo

problema.

Docente: Ing. Erech Ordoñez Ramos Pág. 3


Complejidad de los algoritmos

Objetivo del manual

Proporcionar al estudiante una guía que permita afianzar sus conocimientos


relacionados al análisis de la complejidad de los algoritmos.

Eficiencia y complejidad

Una vez dispongamos de un algoritmo que funciona correctamente, es necesario


definir criterios para medir su rendimiento o comportamiento. Estos criterios se centran
principalmente en su simplicidad y en el uso eficiente de los recursos.
De ahí que muchas veces prime la simplicidad y legibilidad del código frente a
alternativas más crípticas y eficientes del algoritmo.

Criterios para medir la eficiencia y complejidad de un algoritmo

 Simplicidad
 Uso eficiente de los recursos:
o el espacio (memoria que utiliza)
o y el tiempo (lo que tarda en ejecutarse).

Eficiencia temporal

Depender de diversos factores como son:


 los datos de entrada que le suministremos
 La calidad del código generado por el compilador para crear el programa objeto
 La naturaleza y rapidez de las instrucciones máquina del procesador concreto que
ejecute el programa y
 La complejidad intrínseca del algoritmo.

Existen dos estudios posibles sobre el tiempo:


1ro Medida teórica (a priori): Consiste en obtener una función que acote (por arriba
o por abajo) el tiempo de ejecución del algoritmo para unos valores de entrada
dados.
(Se vera en las clases teóricas)
2da Medida real (a posteriori): Consistente en medir el tiempo de ejecución del
algoritmo para unos valores de entrada dados y en un ordenador concreto.

Docente: Ing. Erech Ordoñez Ramos Pág. 4


Complejidad de los algoritmos

(Se vera en la prácticas de laboratorio). Consiste en ejecutar casos de prueba, haciendo


medidas para:
 una máquina concreta,
 un lenguaje concreto,
 un compilador concreto y
 datos concretos.

La unidad de tiempo a la que debe hacer referencia estas medidas de eficiencia se


denotan por T(n) en donde n es el tamaño de entrada.
T(n): Indica el número de instrucciones ejecutadas por un ordenador idealizado.

Por lo tanto un algoritmo tarda un tiempo de T(n).

Principio de Invarianza
Dado un algoritmo y dos implementaciones suyas I1 e I2, que tardan T1(n) y T2(n)
segundos respectivamente, el Principio de Invarianza afirma que existe una constante real
c > 0 y un número natural n0 tales que para todo n ≥ n0 se verifica que T1(n) ≤ cT2(n).

Es decir, el tiempo de ejecución de dos implementaciones distintas de un algoritmo dado


no va a diferir más que en una constante multiplicativa.

Con esto podemos definir sin problemas que un algoritmo tarda un tiempo del orden
de T(n) si existen una constante real c > 0 y una implementación I del algoritmo que tarda
menos que cT(n), para todo n tamaño de la entrada.

Dos factores a tener muy en cuenta son la constante multiplicativa y el n0 para los que
se verifican las condiciones, pues si bien a priori un algoritmo de orden cuadrático es
mejor que uno de orden cúbico, en el caso de tener dos algoritmos cuyos tiempos de
ejecución son 106n2 y 5n3 el primero sólo será mejor que el segundo para tamaños de la
entrada superiores a 200.000.

El comportamiento de los algoritmos cambia notablemente para diferentes entradas,


por lo cual se estudia tres casos para un mismo algoritmo:

Docente: Ing. Erech Ordoñez Ramos Pág. 5


Complejidad de los algoritmos

 Caso peor: Corresponde a la traza (secuencia de sentencias) del algoritmo que


realiza más instrucciones.
 Caso mejor: Corresponde a la traza (secuencia de sentencias) del algoritmo que
realiza menos instrucciones.
 Caso medio: Corresponde a la traza (secuencia de sentencias) del algoritmo que
realiza un promedio de instrucciones.

De los tres casos el que consideramos de mayor importancia es el caso peor, por las
siguientes razones:
 Es el más informativo (el caso mejor suele ser trivial).
 Nos permitirá acotar superiormente el consumo de recursos: sabremos que nuestro
programa
 no consumirá más de lo estimado.
 Es más fácil de calcular que el caso medio.

Ejemplo
Sea A una lista de n elementos A1, A2, A3, ... , An. Ordenar significa permutar estos
elementos de tal forma que los mismos queden de acuerdo con un orden preestablecido.
Ascendente A1 <=A2 <=A3 ..........<=An
Descendente A1 >=A2 >=........>=An
Caso peor: Que el vector esté ordenado en sentido inverso.
Caso mejor: Que el vector esté ordenado.
Caso medio: Cuando el vector esté desordenado aleatoriamente

Operaciones Elementales (OE)

Son las operaciones básicas, asignaciones a variables, los saltos, llamadas a funciones y
procedimientos, retorno de ellos, etc.
Las comparaciones lógicas y el acceso a estructuras indexadas, como los vectores y
matrices. Cada una de ellos cuenta como 1 OE.

Por lo tanto al medir el tiempo, siempre lo haremos en función del número de OEs,
para un tamaño de entrada dado.

Docente: Ing. Erech Ordoñez Ramos Pág. 6


Complejidad de los algoritmos

Reglas generales para el cálculo del número de OE

La siguiente lista presenta un conjunto de reglas generales para el cálculo del número de
OE, siempre considerando el peor caso.

 El tiempo de ejecución de una secuencia consecutiva de instrucciones se calcula


sumando los tiempos de ejecución de cada una de las instrucciones.
 El tiempo de ejecución de la sentencia “CASE C OF v1:S1|v2:S2|...|vn:Sn END;”
es T = T(C) + max{T(S1),T(S2),...,T(Sn)}. Obsérvese que T(C) incluye el tiempo
de comparación con v1, v2 ,..., vn.
 El tiempo de ejecución de la sentencia “IF C THEN S1 ELSE S2 END;” es T =
T(C) + max{T(S1),T(S2)}.
 El tiempo de ejecución de un bucle de sentencias “WHILE C DO S END;” es T =
T(C) + (nº iteraciones)*(T(S) + T(C)). Obsérvese que tanto T(C) como T(S)
pueden variar en cada iteración, y por tanto habrá que tenerlo en cuenta para su
cálculo.
 Para calcular el tiempo de ejecución del resto de sentencias iterativas (FOR,
REPEAT, LOOP) basta expresarlas como un bucle WHILE. A modo de ejemplo,
el tiempo de ejecución del bucle:
FOR i:=1 TO n DO
S
END;
Puede ser calculado a partir del bucle equivalente:
i:=1;
WHILE i<=n DO
S; INC(i)
END;
 El tiempo de ejecución de una llamada a un procedimiento o función F(P1, P2,...,
Pn) es 1 (por la llamada), más el tiempo de evaluación de los parámetros P1,
P2,..., Pn, más el tiempo que tarde en ejecutarse F, esto es, T = 1 + T(P1) + T(P2)
+ ... + T(Pn) + T(F). No contabilizamos la copia de los argumentos a la pila de
ejecución, salvo que se trate de estructuras complejas (registros o vectores) que se
pasan por valor. En este caso contabilizaremos tantas OE como valores simples
contenga la estructura. El paso de parámetros por referencia, por tratarse
simplemente de punteros, no contabiliza tampoco.

Docente: Ing. Erech Ordoñez Ramos Pág. 7


Complejidad de los algoritmos

 El tiempo de ejecución de las llamadas a procedimientos recursivos va a dar lugar


a ecuaciones en recurrencia, que veremos posteriormente.
Ejemplo: Supongamos que tenemos la implementación de un algoritmo de búsqueda
implementado en java y deseamos saber la complejidad bajo la medida teórica (a priori)
del método buscar.

public class Buscar


{
public static void main(String[] args)
{
Buscar B=new Buscar();
int posicion;
int array[]={0,2,4,6,10,81,104};
posicion=B.buscar(array,81); //Busca el elemento 81
System.out.println("La posición es: " + posicion);
}
public int buscar(int a[],int c)
{
int j = 1; (* 1 *)
int n = a.length; (* 2 *)
while (a[j]<c && j<n) (* 3 *)
{
j=j+1; (* 4 *)
}
if (a[j]==c) (* 5 *)
return j; (* 6 *)
else
return 0; (* 7 *)
}
}
Analizamos el número de operaciones elementales (OE) que se realizan en cada línea:
 En la línea (1) se ejecuta: 1 OE (una asignación)
 En la línea (2) se ejecuta: 2 OE (una asignación y una llamada a
función)
 En la línea (3) se ejecuta: 4 OE (dos comparaciones, un acceso al
vector y un AND)
 En la línea (4) se ejecuta: 2 OE (un incremento y una asignación)
 En la línea (5) se ejecuta: 2 OE (una condición y un acceso al vector)
 En la línea (6) se ejecuta: 1 OE (un retorno)
 En la línea (7) se ejecuta: 1 OE (un retorno)

Docente: Ing. Erech Ordoñez Ramos Pág. 8


Complejidad de los algoritmos

Análisis de mejor, peor y caso medio del algoritmo

1ro Mejor caso: Se ejecutan el menor número de líneas posibles


 En la línea (1) se ejecuta: 1 OE (una asignación)
 En la línea (2) se ejecuta: 2 OE (una asignación y una llamada a
función)
 En la línea (3) se ejecuta: 2 OE (porque se ejecuta la mitad de la
condición)
 En la línea (5) se ejecuta: 2 OE (una condición y un acceso al vector)
 En la línea (6) se ejecuta: 1 OE (un retorno)

Por lo tanto: T(n) = 1+2+2+2+1 = 8 OE

2do Peor caso: Se ejecutan el máximo posible de líneas de código.


 En la línea (1) se ejecuta: 1 OE (una asignación)
 En la línea (2) se ejecuta: 2 OE (una asignación y una llamada a función)
 En la línea (3) se ejecuta: 4(n-1)+4 OE (Se ejecutan 4 operaciones en cada
vuelta del bucle)
 En la línea (4) se ejecuta: 2(n-1) OE (Se ejecutan 2 operaciones en cada
pasada del bucle mas 2 OE en la salida del bucle)
 En la línea (5) se ejecuta: 2 (una condición y un acceso al vector)
 En la línea (6) se ejecuta: 1 OE (un retorno)

Por lo tanto: T(n) = 1+2+((4(n-1) +2(n-1)) +4)+2+1 = 6n+4 OE

O podemos expresar el mismo análisis en términos de sumatorias


  n 1  
T(n)  1  2     ( 4  2 )   4   2  1  6n  4 OE
  i 1  

3ro Caso medio: En el caso medio, el bucle se ejecutará un número de veces entre 0 y
n–1, y vamos a suponer que cada una de ellas tiene la misma probabilidad de suceder.
Como existen n posibilidades (puede que el número buscado no esté) suponemos a priori
que son equiprobables y por tanto cada una tendrá una probabilidad asociada de 1/n. Con
esto, el número medio de veces que se efectuará el bucle es de:

Docente: Ing. Erech Ordoñez Ramos Pág. 9


Complejidad de los algoritmos

n 1
1 n 1
i n 
i 0 2
Tenemos pues que   n 1 2  
  
T(n)  1  2     ( 4  2 )   4   2  1  3n  7 OE
  i 1  
  

En la práctica no se trabaja con T(n) sino con funciones que clasifiquen estas
funciones acotando su valor asintóticamente. Por ejemplo: si el T(n) de un algoritmo es:
10n 2
T(n)   2n
3
Diremos que T(n) pertenece O(n2) como lo veremos posteriormente en el tema de
asíntotas.

Ejercicios:

1. Sea el algoritmo: calcular su complejidad en el mejor, peor y caso medio.

1. Read(n)
2. s=0
3. i=1
4. While(i<=1) do
5. s=s+1
6. i=i+1
7. End(While)
8. Write(n,s)

a. Mejor Caso: En el mejor de los casos no entra en el bucle While por lo cual se
ejecutan las siguientes operaciones elementales.

1. Read(n) 1 OE (acceso a una función)


2. s=0 1 OE (asignación)
3. i=1 1 OE
4. While(i<=1) do 1 OE (evaluación de la condición)
5. s=s+1
6. i=i+1
7. End(While)
8. Write(n,s) 1 OE (acceso a una función)

t(n) = 5 OE

Docente: Ing. Erech Ordoñez Ramos Pág. 10


Complejidad de los algoritmos

b. Peor Caso: En el peor caso se ejecuta todo el código posible.

1. read(n) 1 OE
2. s=0 1 OE
3. i=1 1 OE
4. while(i<=1) do 1n +1 OE (n vueltas + 1 evaluación de salida)
5. s=s+1 2n OE
6. i=i+1 2n OE
7. end(while)
8. write(n,s) 1 OE

t(n) = 5n+5 OE

c. Caso medio: En el caso medio como se explico en el ejercicio 1, los bucles se dividen
entre la mitad de vueltas que den los bucles, por lo cual se tiene:

1. Read(n) 1 OE
2. s=0 1 OE
3. i=1 1 OE
4. While(i<=1) do 1n/2+1 OE
5. s=s+1 2n/2 OE
6. i=i+1 2n/2 OE
7. End(While)
8. Write(n,s) 1 OE

t(n) = 5n/2+5 OE

2. Sea el algoritmo: calcular su complejidad en el mejor, peor y caso medio.

1. Read(n)
2. s=0
3. i=1
4. While(i<=1) do
5. s=s+1
6. i=i+1
7. End(While)
8. Write(n,s)

Docente: Ing. Erech Ordoñez Ramos Pág. 11


Complejidad de los algoritmos

a. Mejor Caso: En el mejor de los casos no entra en el bucle While por lo cual se
ejecutan las siguientes operaciones elementales.

1. Read(n) 1 OE (acceso a una función)


2. s=0 1 OE (asignación)
3. i=1 1 OE
4. While(i<=1) do 1 OE (evaluación de la condición)
5. s=s+1
6. i=i+1
7. End(While)
8. Write(n,s) 1 OE (acceso a una función)

t(n) = 5 OE

b. Peor Caso: En el peor caso se ejecuta todo el código posible.

1. read(n) 1 OE
2. s=0 1 OE
3. i=1 1 OE
4. while(i<=1) do 1n +1 OE (n vueltas + 1 evaluación de salida)
5. s=s+1 2n OE
6. i=i+1 2n OE
7. end(while)
8. write(n,s) 1 OE

t(n) = 5n+5 OE

c. Caso medio: En el caso medio como se explico en el ejercicio 1, los bucles se dividen
entre la mitad de vueltas que den los bucles, por lo cual se tiene:

1. Read(n) 1 OE
2. s=0 1 OE
3. i=1 1 OE
4. While(i<=1) do 1n/2+1 OE
5. s=s+1 2n/2 OE
6. i=i+1 2n/2 OE
7. end(while)
8. Write(n,s) 1 OE

t(n) = 5n/2+5 OE

Docente: Ing. Erech Ordoñez Ramos Pág. 12


Complejidad de los algoritmos

En adelante consideraremos solo el caso peor por ser de mayor representatividad para
nuestros fines.

3. Sea el algoritmo: calcular su complejidad en el peor caso.

1. Read(n) La segunda vez que se ejecuta el ciclo


2. S=0
n=16/2=8
3. While n>1 do
4. S=S+1
5. n=n/2 La tercera vez que se ejecuta el ciclo
6. End(while)
n=8/2=4
7. Write (n,S)

En el algoritmo anterior, la variable La cuarta vez n=4/2=2


controladora del bucle es n, y dentro del
La quinta vez n=2/2=1
ciclo, n se divide por dos (2). Si hacemos
un seguimiento detallado y damos un
Y termina el ciclo, es decir, las
valor arbitrario a n=32, tendremos:
instrucciones del bucle se ejecutaron 5
veces.
La primera vez que se ejecuta el ciclo, la
variable n sale valiendo n=32/2=16.

Si en un inicio n=32 y el bucle termina dando 5 entradas, entonces la relación que existe
entre 32 y 5 es: Log 2 32=5, por consiguiente cada instrucción del ciclo se ejecuta un
número de veces igual al logaritmo en base dos de n. Por lo tanto:

Docente: Ing. Erech Ordoñez Ramos Pág. 13


Complejidad de los algoritmos

1. Read(n) 1 OE
2. S=0 1 OE
3. While n>1 do (Log2n)+1 OE
4. S=S+1 2*(Log2n) OE
5. n=n/2 2*(Log2n) OE
6. End(While)
7. Write (n,S) 1 OE

t(n) = 5 Log2n+3

4. Sea el algoritmo anterior modificado: calcular su complejidad en el peor caso.

1. Read(n)
2. S=0
3. While n>1 do
4. S=S+1
5. n=n/3
6. End(While)
7. Write (n,S)

Aquí la variable controladora se divide por 3 dentro del ciclo. Damos un valor
arbitrario a n para ver su comportamiento n=81, haciendo el seguimiento tenemos. En la
primera pasada n=27, en la segunda n=9, el la tercera n=3 y en la cuarta sale con n=1 y
finaliza el bucle. Ahora la relación que existe entre n=81 y 4 vueltas del bucle es:
Log381=4.

Por lo tanto dependiendo por cuanto es dividido la variable controladora de un bucle,


obtendremos Log x n vueltas.

Ahora si analicemos nuestro algoritmo línea por línea, incluyendo las vueltas del
bucle, obtenemos:

1. Read(n) 1 OE
2. S=0 1 OE
3. While n>1 do (Log3n)+1
4. S=S+1 2*(Log3n)
5. n=n/3 2*(Log3n)
6. End(While)
7. Write (n,S) 1 OE

t(n) = 5 Log3n + 3

Docente: Ing. Erech Ordoñez Ramos Pág. 14


Complejidad de los algoritmos

5. Sea el algoritmo de ordenamiento escrito en Delphi: Calcular su complejidad en el


peor caso.

1. Function ordenar(lista: array of Integer):integer;


2. var
3. n,i,j,aux:integer;
4. begin
5. n:=Length(lista);
6. for i:=0 to n-1 do
7. begin
8. for j:=i to n-1 do
9. begin
10. if lista[i]>lista[j] then
11. begin
12. aux:=lista[i];
13. lista[i]:=lista[j];
14. lista[j]:=aux;
15. end;
16. end;
17. end;
18. end;

Analicemos la complejidad línea por línea:

Nota: Para calcular el número de Operaciones Elementales del bucle FOR hacemos la
conversión a su equivalente bucle WHILE:

1. i=1 1OE
2. While i<=n do 1OE
1. for i:=1 to n do 3. Begin
2. begin 4. i:=i+1; 2OE
3. end; 5. End;

Como vemos una aproximada conversión en cuanto a su complejidad del bucle FOR al
WHILE es 4 OE por lo cual en adelante contabilizaremos a todo bucle FOR como 4 OE.

Docente: Ing. Erech Ordoñez Ramos Pág. 15


Complejidad de los algoritmos

1. n:=Length(lista); 2 OE
2. for i:=0 to n-1 do (4+1)n +1OE multiplicado por n vueltas
3. begin
4. for j:=0 to n-1 do ((4+1)n+1) n OE se multiplican ambos bucles
5. begin
6. if lista[i]>lista[j] then 3n2 OE
7. begin
8. aux:=lista[i]; 2 n2 OE
9. lista[i]:=lista[j]; 3 n2 OE
10. lista[j]:=aux; 2 n2 OE
11. end;
12. end;
13. end;

t(n) = 15n2+6n+3

Asíntotas

Como vimos anteriormente podemos calcular el tiempo teórico t(n) de cualquier


algoritmo el cual nos ha de permitir comparar la potencia de dicho algoritmo
independientemente de la potencia de la máquina que los ejecute e incluso de la habilidad
del programador que los codifique. Por otra parte, de este análisis nos interesa
especialmente cuando el algoritmo se aplica a problemas grandes. No debe olvidarse que
cualquier técnica de ingeniería, que funciona, acaba aplicándose al problema más grande
que sea posible: las tecnologías de éxito, antes o después, acaban llevándose al límite de
sus posibilidades.

Las consideraciones anteriores nos llevan a estudiar el comportamiento de un


algoritmo cuando se fuerza el tamaño del problema al que se aplica. Matemáticamente
hablando, cuando n tiende al infinito. Es decir, su comportamiento asintótico.

Sean “g(n)” diferentes funciones que determinan el uso de recursos, pudiendo


existir infinidad de funciones “g”.

Lo que vamos a intentar es identificar “familias” de funciones, usando como


criterio de agrupación su comportamiento asintótico.

Docente: Ing. Erech Ordoñez Ramos Pág. 16


Complejidad de los algoritmos

Una familia de funciones que comparten un mismo comportamiento asintótico


será llamada un Orden de Complejidad. Estas familias se designan con O( ).

Para cada uno de estos conjuntos se suele identificar un miembro f(n) que se
utiliza como representante de la familia, hablándose del conjunto de funciones “g” que
son del orden de "f(n)", denotándose como:

g IN O(f(n)) (g esta incluido en f(n))

Con frecuencia nos encontraremos con que no es necesario conocer el


comportamiento exacto, sino que basta conocer una cota superior, es decir, alguna
función que se comporte “aún peor”.

La definición matemática de estos conjuntos debe ser muy cuidadosa para


involucrar ambos aspectos: identificación de una familia y posible utilización como cota
superior de otras funciones menos malas:

Dícese que el conjunto O(f(n)) es el de las funciones de orden de f(n), que se


define como:

O(f(n)) = { g : INTEGER → REAL+ / ∃ k y n0 / ∀ n > n0, g(n) ≤ k*f(n) }

O(f(n)) esta formado por aquellas funciones g(n) que crecen a un ritmo menor o igual que
el de f(n).

De las funciones “g” que forman este conjunto O(f(n)) se dice que “están
dominadas asintóticamente” por “f”, en el sentido de que para n suficientemente grande,
y salvo una constante multiplicativa “k”, f(n) es una cota superior de g(n).

Propiedades de los Conjuntos O(f(n))

Las primeras reglas sólo expresan matemáticamente el concepto de jerarquía de


órdenes de complejidad:

Docente: Ing. Erech Ordoñez Ramos


Pág. 17
Complejidad de los algoritmos

a) La relación de orden definida por

f(n) < g(n) f(n) IN O(g(n))


Es reflexiva: f(n) IN O(f(n))

y transitiva: f(n) IN O(g(n)) y g(n) IN O(h(n)) f(n) IN O(h(n))

b) f(n) IN O(g(n)) y g IN O(f(n)) O(f(n)) = O(g(n))

Las siguientes propiedades se pueden utilizar como reglas para el cálculo de órdenes de
complejidad. Toda la maquinaria matemática para el cálculo de límites se puede aplicar
directamente:

c) lim 𝒇(𝒏)/𝒈(𝒏) = 𝟎 f(n) IN O(g(n))


𝑛→∞

 g(n) NOT_IN O(f(n))


 O(f(n)) es subconjunto de O(g(n))

d) lim 𝒇(𝒏)/𝒈(𝒏) = 𝐤 f(n) IN O(g(n))


𝑛→∞

 g(n) IN O(f(n))
 O(f(n)) = O(g(n))

e) lim 𝒇(𝒏)/𝒈(𝒏) = ∞ f(n) NOT_IN O(g(n))


𝑛→∞

 g(n) IN O(f(n))
 O(f(n)) es superconjunto de O(g(n))

Ejemplo:

1. Demostrar que la función f (n) 0,01n no pertenece a O(n )


3 2

Para demostrar que f(n) no es de orden cuadrático basta tener en cuenta que:

0,01𝑛3
lim = lim 0.01𝑛 = ∞
𝑛→∞ 𝑛2 𝑛→∞

Docente: Ing. Erech Ordoñez Ramos Pág. 18


Complejidad de los algoritmos

2. Sea 𝑓 𝑛 = 𝑛2 + 𝑛 − 5 y 𝑔 𝑛 = 𝑛3 + 2𝑛 − 2


𝑓 𝑛 𝑛 2 +𝑛−5 𝑛 2 +𝑛−5 ∞ 2𝑛+1 ′ 2 2
Entonces lim𝑛→∞ = 𝑔(𝑛) = 𝑛 3 +2𝑛−2 = =∞= = 6𝑛 = ∞ = 0
𝑛 3 +2𝑛−2 ′ 3𝑛 2 +2 ′

Por lo tanto 𝑓 𝑛  𝑂(𝑔 𝑛 ) y deducimos que:

 𝑔 𝑛  𝑂(𝑓 𝑛 )
 𝑂 𝑓 𝑛 𝑒𝑠 𝑆𝑢𝑛𝑐𝑜𝑛𝑗𝑢𝑛𝑡𝑜 𝑑𝑒 𝑂 𝑔 𝑛

3. Sea 𝑓 𝑛 = 𝑛2 + 𝑛 − 5 y 𝑔 𝑛 = 𝑛2


𝑓 𝑛 𝑛 2 +𝑛−5 ∞ 𝑛 2 +𝑛−5 2𝑛+1 ′ 2
Entonces lim𝑛→∞ 𝑔(𝑛) = =∞= = =2=1
𝑛2 𝑛2 ′ 2𝑛 ′

Por lo tanto 𝑓 𝑛  𝑂(𝑔 𝑛 ) y deducimos que:

 𝑔 𝑛  𝑂(𝑔 𝑛 )
 𝑂 𝑓 𝑛 = 𝑂 𝑔 𝑛

4. Demostrar que la función f (n) 200n 300n pertenece a O(n )


2 2

Para demostrar que f(n) es de orden cuadrático basta tener en cuenta que:

𝟐𝟎𝟎𝒏𝟐 +𝟑𝟎𝟎𝒏 𝟑𝟎𝟎


lim 𝒏𝟐
= lim 𝒏
+ 𝟐𝟎𝟎 = 𝟐𝟎𝟎
𝑛→∞ 𝑛→∞

Otra posibilidad es encontrar un número real c positivo y un natural no que


cumplan las condiciones de la definición Basta tomar c=500 y no = 1 para que se
cumpla:
200n2+300n500n2 para cualquier n2, en efecto 200n2+300n500n2 implica que
ha de ser 300n300 n2 lo que a su vez implica que ha de ser 300300n, lo cual
ocurre para cualquier n mayor o igual que 1.

Docente: Ing. Erech Ordoñez Ramos


Pág. 19
Complejidad de los algoritmos

Órdenes de complejidad

Se dice que O(f(n)) define un "orden de complejidad". Escogeremos como


representante de este orden a la función f(n) más sencilla del mismo. Así tendremos:

O(1) orden constante

O(log n) orden logarítmico

O(n) orden lineal

O(n log n)

O(n2) orden cuadrático

O(na) orden polinomial (a > 2)

O(an) orden exponencial (a > 2)

O(n!) orden factorial

Docente: Ing. Erech Ordoñez Ramos Pág. 20


Complejidad de los algoritmos

Complejidad de algoritmos recursivos

Recursividad: técnica con la que un problema se resuelve sustituyéndolo por otro


problema de la misma forma pero más simple.

En general, un algoritmo recursivo tiene dos partes:

 El caso base, que maneja una entrada simple que puede ser resuelta sin una
llamada recursiva
 La parte recursiva, que contiene una o más llamadas recursivas al algoritmo,
donde los parámetros están en un sentido más cercano al caso base, que la
llamada original.

Ejemplo: Definición de factorial para n ≥ 0.

0! = 1
n! = n * (n-1)! si n>0

Es una herramienta muy potente, útil y sencilla.

Ciertos problemas se adaptan de manera natural a soluciones recursivas; pero en


general, las soluciones recursivas son más ineficientes en tiempo y espacio que las
versiones iterativas, debido a las llamadas a subprogramas, la creación de variables
dinámicamente en la pila, la duplicación de variables, etc.

En general, cualquier función recursiva se puede transformar en una función iterativa.

 Ventaja de la función iterativa: más eficiente en tiempo y espacio.


 Desventaja de la función iterativa: en algunos casos, muy complicada; además,
suelen necesitarse estructuras de datos auxiliares

Docente: Ing. Erech Ordoñez Ramos


Pág. 21
Complejidad de los algoritmos

Si la eficiencia es un parámetro crítico, y la función se va a ejecutar frecuentemente,


conviene escribir una solución iterativa.

La recursividad se puede simular con el uso de pilas para transformar un programa


recursivo en iterativo. Las pilas se usan para almacenar los valores de los parámetros del
subprograma, los valores de las variables locales, y los resultados de la función.

Clasificación de funciones recursivas

a) Según desde dónde se haga la llamada recursiva:


 Recursividad directa: la función se llama a sí misma.
 Recursividad indirecta: la función A llama a la función B, y ésta llama a A.

b) Según el número de llamadas recursivas generadas en tiempo de ejecución:


 Función recursiva lineal o simple: se genera una única llamada interna.
 Función recursiva no lineal o múltiple: se generan dos o más llamadas
internas.

c) Según el punto donde se realice la llamada recursiva, las funciones recursivas


pueden ser:

 Final: (Tail recursion): La llamada recursiva es la última instrucción que se


produce dentro de la función.
 No final: (Nontail recursive function): Se hace alguna operación al volver de
la llamada recursiva.

Las funciones recursivas finales suelen ser más eficientes (en la constante
multiplicativa en cuanto al tiempo, y sobre todo en cuanto al espacio de memoria) que
las no finales. (Algunos compiladores pueden optimizar automáticamente estas

Docente: Ing. Erech Ordoñez Ramos Pág. 22


Complejidad de los algoritmos

funciones pasándolas a iterativas)

Ejemplo de función recursiva final: algoritmo de Euclides para calcular el máximo


común divisor de dos números enteros positivos.

mcd (a, b) = mcd (b, a) = mcd (a-b, b) si a > b


mcd (a, b-a) si a < b
a si a = b

Ecuaciones de Recurrencia

En un algoritmo recursivo, la función 𝑡 𝑛 que establece su tiempo de ejecución


viene dada por una ecuación 𝑒 𝑛 de recurrencia, donde en la expresión aparece la propia
función 𝑡 .
𝑡 𝑛 = 𝑒 𝑛 , y en 𝑒 𝑛 aparece la propia función 𝑡 .

Para resolver ese tipo de ecuaciones hay que encontrar una expresión no recursiva
de 𝑡 𝑛 . (En algunos casos no es tarea fácil.)

A menudo, al plantear las ecuaciones que describen el costo de un algoritmo,


obtendremos ecuaciones de recurrencia, que describen el costo del algoritmo en función
de instancias más pequeñas del mismo. Como primer ejemplo veremos la solución de las
torres de Hanoi.

El problema de las torres de Hanoi, tiene una solución recursiva que se basa en
mover n − 1 discos de A a B, luego el más grande de A a C y finalmente los n − 1 en B
hacía C. Si definimos T (n) cantidad de movimientos de disco necesarios para resolver el
problema de las torres de Hanoi con n discos, obtenemos:

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

El +1 corresponde a mover el disco más grande de A a C, cada t(n-1) corresponde a

Docente: Ing. Erech Ordoñez Ramos


Pág. 23
Complejidad de los algoritmos

mover n − 1 discos como si fuese el mismo problema de las torres de Hanoi (notar que
sólo cambia a torre intermedia, la solución es efectivamente la misma).

A continuación veremos 5 métodos de solución para ecuaciones de recurrencia, las


torres de Hanoi servirán para el primero.

1. Primer método: Suponer una solución f(n), y usar la recurrencia para demostrar que
t(n) = f(n). La prueba se hace generalmente por inducción sobre n.

Veamos algunos valores para t(n) = 2 t(n − 1) + 1. Primero debemos darnos un


caso base, cuántos movimientos se requieren para resolver el problema de las
torres de Hanoi con un solo disco: t(1) = 1.
n t(n)
1 1
2 3
3 7
4 15
Cuadro 1: Algunos valores de t(n)

De esta tabla no es tan directo ver la solución, pero si escribimos la misma


tabla para t(n) + 1 obtenemos:

n t(n)+1
1 2
2 4
3 8
4 16
Cuadro 2: Algunos valores de t(n)+1

Aquí ya es posible notar que t(n)+1 = 2n o bien que t(n) = 2n − 1. Esto en


principio es sólo una especulación y debemos ser capaces de probar que lo que
decimos es cierto. Esto se puede hacer por inducción sobre n.

Teorema (Torres de Hanoi) La solución de la ecuación

Docente: Ing. Erech Ordoñez Ramos Pág. 24


Complejidad de los algoritmos

t(n) = 2 t(n−1) + 1 es

t(n) = 2n − 1.

Demostración Por inducción sobre n:

Caso base: Para n = 1 tenemos que t(n) = 21 − 1 = 1, lo cual es correcto.

Paso inductivo: Asumamos que la hipótesis t(n) = 2n − 1 es correcta para n y


probemos que se cumple para n + 1. Desarrollemos la ecuación:

t(n + 1) = 2 t(n) + 1 (2)

t(n + 1) = 2(2n − 1) + 1 (3)

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

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

Desde (2) a (3) se utiliza la hipótesis y desde ahí se llega directamente al resultado
esperado.

Es importante tener en cuenta que cualquier solución que encontremos con los
métodos expuestos a continuación pueden ser verificados utilizando esta misma
forma.

2. Segundo método: Sustituir las recurrencias por su igualdad hasta llegar a cierta
𝑡(𝑛 0 ) que sea conocida.
Ejemplo:

int Fact (int n)


{
if (n <= 1)
return(1);
else
return( n * Fact(n-1));
}

Sea 𝑡(𝑛) el tiempo de ejecución en el caso peor. Se escribe una ecuación de

Docente: Ing. Erech Ordoñez Ramos


Pág. 25
Complejidad de los algoritmos

recurrencia que acota a 𝑡(𝑛) superiormente como:

𝐶1 , 𝑆𝑖 𝑛 ≤ 1
𝑡(𝑛) = 𝑡
(𝑛−1) + 𝐶2 , 𝑆𝑖 𝑛 > 1

𝐶1 : Tiempo de ejecución del caso trivial 𝑛 ≤ 1 estructura if(n≤1) y la


función return(1);

𝑆𝑖 𝑛 > 1: tiempo requerido por Fact puede dividirse en dos partes:

 𝑡(𝑛−1) La llamada recursiva a FAct con 𝑛 − 1.


 𝐶2 El tiempo de evaluar la condición (𝑛 > 1), la multiplicación
de n ∗ Fact(n − 1) y el acceso a return().

Entonces resolvemos la ecuación de recurrencia:

Si 𝑡(𝑛) = 𝑡(𝑛−1) + 𝐶2 , entonces calculamos la parte recurrente:

𝑡(𝑛) = 𝑡(𝑛−1) + 𝐶2 (I)

𝑡(𝑛−1) = 𝑡(𝑛−2) + 𝐶2

𝑡(𝑛−2) = 𝑡(𝑛−3) + 𝐶2

𝑡(𝑛−3) = 𝑡(𝑛−4) + 𝐶2

Luego de ello sustituimos sus equivalentes en (I), hasta obtener el k-esimo termino
de las sustituciones.

𝑡(𝑛) = 𝑡(𝑛−1) + 𝐶2
𝑡(𝑛) = 𝑡(𝑛−2) + 𝐶2 + 𝐶2
𝑡(𝑛) = 𝑡(𝑛−2) + 2𝐶2
𝑡(𝑛) = 𝑡(𝑛−3) + 𝐶2 + 2𝐶2
𝑡(𝑛) = 𝑡(𝑛−3) + 3𝐶2

𝑡(𝑛) = 𝑡(𝑛−𝑘) + 𝑘𝐶2 (II)

Docente: Ing. Erech Ordoñez Ramos Pág. 26


Complejidad de los algoritmos

Si sabemos que 𝑡(𝑛) = 𝐶1 , 𝑆𝑖 𝑛 ≤ 1, entonces n = 1, 𝑡(1) = 𝐶1


𝑛−𝑘 =1 →𝑘 =𝑛−1

Reemplazando los valores conocidos en (II):


𝑡(𝑛) = 𝑡(1) + (𝑛 − 1)𝐶2
𝑡(𝑛) = (𝑛 − 1)𝐶2 + 𝐶1

De donde podemos concluir que el tiempo de ejecución del algoritmo es:


𝑡(𝑛) = (𝑛 − 1)𝐶2 + 𝐶1

3. Tercer método: Resolución de la Ecuación Característica

A) Primera forma

Este método se aplica para ecuaciones de la forma:

𝑡(𝑛) = 𝑓(𝑡 𝑛−1 , 𝑡 𝑛 −2 , … , 𝑡 𝑛 −𝑘 , 𝐶)

Un ejemplo muy conocido son los números de Fibonacci, cuya ecuación es:

𝑡(𝑛) = 𝑡 𝑛 −1 + 𝑡 𝑛 −2

Con los valores iniciales 𝑡(0) =0 y 𝑡(1) = 1.

Pare resolver ecuaciones de este tipo asumiremos 𝑡(𝑛) = 𝜆𝑛 y simplemente


reemplazamos:
𝑡(𝑛) = 𝑡 𝑛 −1 + 𝑡 𝑛 −2
𝜆𝑛 = 𝜆𝑛−1 + 𝜆𝑛−2 (6)
𝜆2 = 𝜆 + 1 (7)
𝜆2 − 𝜆 − 1 = 0 (8)

Si resolvemos esa ecuación obtenemos dos soluciones:

Docente: Ing. Erech Ordoñez Ramos


Pág. 27
Complejidad de los algoritmos

1+ 5 1− 5
λ1 = , λ2 =
2 2

Cada valor por su cuenta es una solución, como también los es la


combinación lineal de ambos:
𝑡(𝑛) = 𝐶1 𝜆1𝑛 + 𝐶2 𝜆𝑛2

Usando las condiciones inciales podemos obtener los valores de las


constantes 𝐶1 y 𝐶2 .
Usamos t(0) = 0 y t(1) = 1
t(0) = 0
= 𝐶1 + 𝐶2
0 = 𝐶1 + 𝐶2
𝐶1 = −𝐶2

Ahora, al reemplazar en la segunda condición inicial, consideraremos 𝐶1 = −𝐶2 .


t(1) = 1
= 𝐶1 𝜆1 + 𝐶1 𝜆2
1 = 𝐶1 𝜆1 − 𝜆2
1 = 5𝐶1
1
𝐶1 = 5

Obteniendo finalmente la solución:

𝑛 𝑛
1 1+ 5 1− 5
𝑡𝑛 = −
5 2 2

B) Segunda Forma

Para ecuaciones de la forma 𝑡 𝑛 = 𝑡 𝑛 −1 + 𝑓 𝑛 con 𝑡 𝑘 conocido para algún

Docente: Ing. Erech Ordoñez Ramos Pág. 28


Complejidad de los algoritmos

𝑘 ≥ 0.
Teorema: La solución para la ecuación 𝑡 𝑛 = 𝑡 𝑛−1 + 𝑓 𝑛 es:

𝑡𝑛 =𝑡𝑘 + 𝑓 𝑖 ∀𝑛 ≥ 𝑘
𝑖=𝑘+1

Para alguna condición inicial 𝑡 𝑘 con 𝑘 ≥ 0.

Demostración: La solución puede ser deducida de la siguiente manera:

𝑡 𝑛 = 𝑡 𝑘−1 + 𝑓 𝑛 𝑡 𝑛 − 𝑡 𝑘−1 = 𝑓 𝑛
En esta ecuación es una variable muda y puede escribirse como:
𝑡 𝑖 − 𝑡 𝑖−1 = 𝑓 𝑖

Si sumamos desde 𝑖 = 𝑘 + 1 hasta 𝑖 = 𝑛 obtenemos:

𝑛 𝑛

(𝑡(𝑖) − 𝑡(𝑖−1) ) = 𝑓𝑖
𝑖=𝑘+1 𝑖=𝑘+1

La primera parte es una telescópica y sobreviven los términos 𝑡 𝑛 y 𝑡 𝑘 ,


quedando:
𝑛

𝑡𝑛 −𝑡𝑘 = 𝑓𝑖
𝑖=𝑘+1

Obteniendo finalmente:
𝑛

𝑡𝑛 =𝑡𝑘 − 𝑓𝑖
𝑖=𝑘+1

Ejemplo (Ordenamiento por selección): En temas posteriores al curso veremos el


ordenamiento por selección, por el momento podemos adelantar que la ecuación de
recurrencia que describe el 𝑡 𝑛 del algoritmo es:

Docente: Ing. Erech Ordoñez Ramos


Pág. 29
Complejidad de los algoritmos

𝑡 𝑛 = 𝑡 𝑛−1 + 𝑛 − 1, 𝑡 0 = 0

Usando la fórmula tenemos:


𝑛
𝑡 𝑛 =0+ 𝑖−1
𝑖=1
𝑛(𝑛 − 1)
𝑡 𝑛 =
2

C) Tercera forma

Para ecuaciones de la forma 𝑡 𝑛 = 𝑎𝑡 𝑛−1 + 𝑓 𝑛 con alguna condición inicial


𝑡 𝑘 con 𝑘 ≥ 0.

Teorema La ecuación 𝑡 𝑛 = 𝑎𝑡 𝑛−1 + 𝑓 𝑛 , con 𝑎 constante y una condición inicial


𝑡 𝑘 conocida con 𝑘 ≥ 0, tiene como solución:

𝑛
𝑡 𝑛 = 𝑎𝑛−𝑘 𝑡 𝑘 + 𝑎𝑛−𝑘 𝑓 𝑖 ∀ 𝑛 ≥ 𝑘
𝑖=𝑘+1

Demostración: Para demostrar la solución obtenida basta tomar la ecuación y


dividirla por 𝑎𝑛 :

𝑡 𝑛 𝑎𝑡 𝑛−1 𝑓 𝑛
= + 𝑛
𝑎𝑛 𝑎𝑛 𝑎

𝑎𝑡 𝑛 −1 𝑡 𝑛 −1 𝑡𝑛
Notamos que = . Si definimos 𝑔 𝑛 = 𝑎𝑛
la ecuacion resultante es:
𝑎𝑛 𝑎 𝑛 −1
𝑓𝑛
𝑔 𝑛 =𝑔 𝑛−1 +
𝑎𝑛
Usando el teorema propuesto obtenemos:
𝑛
𝑓𝑖
𝑔 𝑛 =𝑔 𝑛−1 +
𝑎𝑖
𝑖=𝑘+1

Docente: Ing. Erech Ordoñez Ramos Pág. 30


Complejidad de los algoritmos

𝑛
𝑡𝑛 𝑡𝑘 𝑓𝑖
𝑛
= 𝑘 +
𝑎 𝑎 𝑎𝑖
𝑖=𝑘+1
𝑛

𝑡 𝑛 = 𝑎𝑛−𝑘 𝑡(𝑘) + 𝑎𝑛−𝑖 𝑓 𝑖


𝑖=𝑘+1

Ejemplo (Torres de Hanoi) El problema de las torres de Hanoi, resuelto utilizando el


método 1, se puede resolver usando este método también. La ecuación es:

𝑡 𝑛 = 2𝑡(𝑛−1) + 1, 𝑡 0 =0
La solución es:
𝑛
𝑛
𝑡 𝑛 = 2 .𝑡 0 + 2𝑛−𝑖
𝑖=1
𝑛
𝑡 𝑛 =2 −1

D) Cuarta Forma

Finalmente, para las ecuaciones de la forma 𝑡 𝑛 = 𝑎𝑡(𝑛 ) + 𝑓 𝑛 obtendremos


𝑏

𝑘
una solución exacta para 𝑛 = 𝑏 . Para simplificar el resultado, consideraremos
𝑡 1 como condición inicial conocida.

Teorema: La ecuación 𝑡 𝑛 = 𝑎𝑡(𝑛 ) + 𝑓 𝑛 , con 𝑎 ≥ 0 𝑦 𝑏 > 1 constantes tiene


𝑏

como solución:
𝑙𝑜𝑔 𝑏 (𝑛)
𝑙𝑜𝑔 𝑏 (𝑛)
𝑡 𝑛 =𝑎 𝑡(1) + 𝑎𝑙𝑜𝑔 𝑏 𝑛 −𝑖
𝑓 𝑏𝑖 ∀ 𝑛 = 𝑏𝑘 , 𝑘 ≥ 1
𝑖=1

Demostración: Como estamos resolviendo la ecuación de manera exacta para


potencias de 𝑏, es decir, 𝑛 = 𝑏𝑘 , podemos reescribir la ecuación en términos de 𝑏 y
𝑘:

𝑡 𝑛 𝑘 = 𝑎𝑡 𝑏𝑘 𝑏 + 𝑓(𝑏 𝑘 )

Si definimos 𝑔 𝑘 = 𝑡 𝑏 𝑘 la ecuación queda:

Docente: Ing. Erech Ordoñez Ramos


Pág. 31
Complejidad de los algoritmos

𝑔 𝑘 = 𝑎𝑔 𝑘−1 +𝑓 𝑏𝑘

Haciendo uso del Teorema propuesto para este caso obtenemos:


𝑘
𝑘
𝑔 𝑘 =𝑎 𝑔0 + 𝑎𝑘−𝑖 𝑓 𝑏𝑖
𝑖=1

Finalmente, 𝑔 𝑘 = 𝑡𝑛 , 𝑘 = 𝑙𝑜𝑔𝑏 𝑛 𝑦 𝑔 0 =𝑡 1 , por lo que reemplazando


esos valores se obtiene:
log b (𝑛)

𝑡 𝑛 = 𝑎log b (𝑛) 𝑡(1) + 𝑎log b 𝑛 −𝑖


𝑓(𝑏 𝑖 )
𝑖=1

Ejemplo (Merge-Sort): Otro algoritmo conocido de ordenamiento es MergeSort. La


ecuación que describe el costo de este algoritmo es:
𝑡 𝑛 = 2𝑡 𝑛 2 + 𝑛, 𝑡 1 =0

Por lo que la solución es:


𝑙𝑜𝑔 2 (𝑛)
𝑙𝑜𝑔 2 (𝑛)
𝑡 𝑛 =2 𝑡1 + 2𝑙𝑜𝑔 2 𝑛 −𝑖 𝑖
2
𝑖=1
𝑙𝑜𝑔 2 (𝑛)

𝑡 𝑛 = 2𝑙𝑜𝑔 2 𝑛

𝑖=1

𝑡 𝑛 = 2𝑙𝑜𝑔 2 𝑛
𝑙𝑜𝑔2 𝑛
𝑡 𝑛 = 𝑛𝑙𝑜𝑔2 (𝑛)

Ejemplo (Búsqueda binaria): La ecuación que representa el costo de la búsqueda


binaria es:
𝑡 𝑛 =𝑡 𝑛 2 + 1, 𝑡 1 =1

Si usamos el resultado del teorema anterior obtenemos:


𝑙𝑜𝑔 2 (𝑛)

𝑡 𝑛 = 1𝑙𝑜𝑔 2 (𝑛) 𝑡 1 + 1𝑙𝑜𝑔 2 𝑛 −𝑖


1
𝑖=1
𝑙𝑜𝑔 2 (𝑛)

𝑡 𝑛 =𝑡 1 + 1
𝑖=1

𝑡 𝑛 = 1 + 𝑙𝑜𝑔2 (𝑛)

Docente: Ing. Erech Ordoñez Ramos Pág. 32


Complejidad de los algoritmos

Bibliografía

 Flóres Rueda, Roberto. “Algoritmos, estructura de datos y programación orientada


a objetos”. Bogota: Ecoe Ediciones. Primera edición 2005.

 Gueregueta Garcia ,Rosa y Vallecillo Moreno, Antonio.“Técnica de diseño de


algoritmos” Universidad de Malaga.

 Víctor Valenzuela Ruz. “Manual de análisis y diseño de algoritmos”. 2003 de


Instituto Nacional de Capacitación

Docente: Ing. Erech Ordoñez Ramos


Pág. 33

También podría gustarte