Facultad de Ciencias
Escuela de Computación
ND 2001– 05
Septiembre 2001
RESUMEN
1
Laboratorio MeFIS - Centro ISYS
e-mail: escalise@acm.org
2
Laboratorio de Computación Gráfica
e-mail: rcarmona@strix.ciens.ucv.ve
CONTENIDO
1 Introducción........................................................................................ 2
4 Modelos de Computación.................................................................. 17
4.1 RAM (Random Access Machine)...................................................................... 17
4.2 RASP (Random Access Stored Program Machine) ........................................ 17
4.3 Turing Machine (Máquina de Turing)............................................................. 17
Referencias .............................................................................................. 19
1 Introducción
Un algoritmo es "una secuencia finita de instrucciones, cada una de las cuales tiene un
significado preciso y puede ejecutarse con una cantidad finita de esfuerzo en un tiempo
finito" [AHU 83]. Un programa es un algoritmo expresado en un lenguaje de programación
específico.
Los criterios para evaluar programas son diversos: eficiencia, portabilidad, eficacia,
robustez, etc. El análisis de complejidad está relacionado con la eficiencia del programa. La
eficiencia mide el uso de los recursos del computador por un algoritmo. Por su parte, el
análisis de complejidad mide el tiempo de cálculo para ejecutar las operaciones
(complejidad en tiempo) y el espacio de memoria para contener y manipular el programa
más los datos (complejidad en espacio). Así, el objetivo del análisis de complejidad es
cuantificar las medidas físicas: "tiempo de ejecución y espacio de memoria" y comparar
distintos algoritmos que resuelven un mismo problema.
El tiempo de ejecución de un programa depende de factores como [AHU 83]:
− los datos de entrada del programa
− la calidad del código objeto generado por el compilador
− la naturaleza y rapidez de las instrucciones de máquina utilizadas
− la complejidad en tiempo del algoritmo base del programa
El tiempo de ejecución debe definirse como una función que depende de la entrada; en
particular, de su tamaño. El tiempo requerido por un algoritmo expresado como una
función del tamaño de la entrada del problema se denomina complejidad en tiempo del
algoritmo y se denota T(n). El “comportamiento límite” de la complejidad a medida que
crece el tamaño del problema se denomina complejidad en tiempo asintótica. De manera
análoga se pueden establecer definiciones para la complejidad en espacio y la complejidad
en espacio asintótica.
En muchos casos, la complejidad de tiempo de un algoritmo es igual para todas las
instancias de tamaño n del problema. En otros casos, la complejidad de un algoritmo de
tamaño n es distinta dependiendo de las instancias de tamaño n del problema que resuelve.
Esto nos lleva a estudiar la complejidad del peor caso, mejor caso, y caso promedio.
Para un tamaño dado (n), la complejidad del algoritmo en el peor caso resulta de tomar el
máximo tiempo (complejidad máxima) en que se ejecuta el algoritmo, entre todas las
instancias del problema (que resuelve el algoritmo) de tamaño n; la complejidad en el caso
promedio es la esperanza matemática del tiempo de ejecución del algoritmo para entradas
de tamaño n, y la complejidad mejor caso es el menor tiempo en que se ejecuta el
algoritmo para entradas de tamaño n. Por defecto se toma la complejidad del peor caso
como medida de complejidad T(n) del algoritmo.
Aún cuando son los programas que consumen recursos computacionales (memoria, tiempo,
etc.) sobre una máquina concreta, se realiza el análisis sobre el algoritmo de base,
considerando que se ejecuta en una máquina hipotética.
2
2 Herramientas para el análisis de complejidad
Esta sección presenta un conjunto de herramientas utilizadas para el cálculo de complejidad
de algoritmos en tiempo y espacio. Para mayor detalles se recomienda [AHU 74] y [AHU
83].
3
En la siguiente figura [Bro 89], se presenta un ejemplo de dos funciones f y g con la misma
tasa de crecimiento.
A medida que los computadores se hacen más rápidos, podemos manejar problemas de
tamaño superior y mediante la complejidad del algoritmo se puede determinar el
incremento posible en el tamaño del problema. Suponga que tenemos 5 algoritmos A1 -A5
con las complejidades en tiempo indicadas en la Tabla 1 [AHU 74]. La complejidad en
tiempo viene dada por el número de unidades de tiempo requeridas para procesar una
4
entrada de tamaño n. En la tabla, se asume que una unidad de tiempo equivale a un
milisegundo.
Nótese que para algoritmos con altos órdenes de complejidad, un incremento en 10 veces
en capacidad de cómputo no aumenta considerablemente los tamaños de problemas que se
pueden tratar.
Regla de la suma
Sean T1(n) y T2(n) las funciones de complejidad para ejecutar dos instrucciones P1 y P2
respectivamente (dos instrucciones de un programa), con T1 (n)=Ο(f(n)) y T2 (n)=Ο(g(n)).
La complejidad en tiempo de la secuencia P 1;P 2 es T1 (n)+T2 (n)=Ο(Max{f(n),g(n)}). Nótese
que T1 ,T2 :Ν→ℜ+-{0}, porque el tamaño de los problemas es entero positivo, y el tiempo
Ti(n) siempre es real positivo.
5
− T1 (n)=c.f(n)+d ⇒ T1 (n)=Ο(f(n))
− T1 (n)=Ο(nk) ∧ T2 (n)=Ο(nk+1) ⇒ T1 (n)+T2 (n)=Ο(nk+1)
− T(n)=c.nd ⇒ T(n)=Ο(nd )
− T(n) = P k(n) ⇒ T(n)=Ο(nk ). (Pk (n) es un polinomio de grado k≥0).
− T1(n)=Ln(n) ∧ T2(n)=nk ∧ k>1 ⇒ T1 (n)+T2 (n)=Ο(nk )
− T1 (n)=rn ∧ T2 (n)=Pk (n) ∧ r>1 ⇒ T1 (n)+T2 (n)=Ο(rn )
Ejemplo :
Las tres instrucciones {b ← 100; a ← a+1; a ← (b div 5) mod 7 - 3} son de complejidad
T1 (n)=c1 , T2 (n)=c2 , T3 (n)=c3 respectivamente con c1 ≤c2≤c3 . Así, la función de
complejidad de las tres instrucciones en conjunto es:
T(n)=T1(n)+T2(n)+T3 (n)=max{c1 ,c2 ,c3 }=c3 ⇒ T(n)=Ο(1).
Nótese que no es relevante cual instrucción se tarda más y cual menos, porque al final T(n)
es una constante y por ende de orden Ο(1). Por esto, la complejidad de cada instrucción
simple la tomaremos como la misma constante "c" para simplificar los razonamientos.
6
Regla 4: La complejidad en tiempo de una instrucción selectiva condicional simple de la
forma:
si <Condición> entonces <Instrucciones> fsi
está dado por complejidad T1(n) de <Condición> sumado a la complejidad T2 (n) de
<Instrucciones> (porque en el peor caso se ejecutan ambas). Por regla de la suma, si T1 (n)=
Ο(f(n)) y T2(n)=Ο(g(n)), entonces el tiempo requerido para ejecutar el condicional simple
es la suma T1(n)+T2 (n), lo cual es Ο(max{f(n),g(n)}).
Análogamente, para una instrucción selectiva de la forma:
si <Condición> entonces { <Condición> es de complejidad T1 (n)=Ο(f(n)) }
<Instrucciones1> { de complejidad T2 (n)=Ο(g(n)) }
sino
<Instrucciones2> { de complejidad T3 (n)=Ο(h(n)) }
fsi
su complejidad está dado por: T1(n)+max{T 2(n)+T3(n)}, lo cual, por regla de la suma es de
orden Ο(max{f(n),max{g(n),h(n)}}) o sencillamente Ο(max{f(n),g(n),h(n)})
Regla 5: La complejidad en tiempo de un ciclo iterativo es la suma sobre todas las
iteraciones de:
− la complejidad en tiempo para ejecutar el cuerpo de la iteración, y
− la complejidad en tiempo para evaluar la condición de terminación del ciclo.
La estructuras iterativa más general es el iterar:
iterar
<Instrucciones1>
parada <Condición>
<Instrucciones2>
fiterar
En ocasiones, la complejidad de <Instrucciones1> e <Instrucciones2> puede depender del
número de la iteración. De aquí que en general, la complejidad del ciclo iterativo viene
dado por
k k −1
T(n)= ∑ T1,i ( n) + ∑ T2,i (n ) + k .Tcond (n )
i =1 i =1
7
fmientras
La estructura del repetir se deriva del iterar cuando el cuerpo de <Instrucciones 2> es vacío.
k −1
Por lo tanto ∑T 2 ,i (n ) = 0 .
i =1
repetir
<Instrucciones 1> { se ejcuta k veces }
hasta <Condición>
Se usa el para comúnmente cuando el cuerpo de <Instrucciones 2> es tan sólo un
incremento de una variable entera (asociada al número de iteraciones) de complejidad "c".
para <Variable> ← <Inicio> hasta <Final> hacer
<Instrucciones 1> { se ejecuta Final-Inicio+1 veces }
fpara
8
Se denota Cm(T) al costo en espacio para un tipo de datos T definido. Entonces toda
variable X de tipo T tendrá el mismo costo. Por convención, este costo está dado en
unidades de palabra de memoria (generalmente 2 bytes).
Tomando como referencia el lenguaje PASCAL, se asumirán los costos para estos objetos.
Los valores que se representan dan un orden de magnitud válido para un gran número de
implementaciones:
1) Las instancias de los tipos elementales: Entero, Real, Lógico y Caracter requieren una
palabra de memoria. Luego, Cm(T)=1 si T es un tipo elemental.
2) Las instancias de los tipos definidos por enumeración requieren una palabra de memoria.
Si T es un tipo definido por enumeración, Cm(T)=1.
3) Las instancias de los tipos estructurados (sólo arreglos y registros) requieren una
cantidad de memoria determinada por la suma del espacio de sus componentes, más el
espacio ocupado por el descriptor de la estructura.
3.1) En el caso de un arreglo (estructura homogénea) de longitud n con tipo base T0, el
costo en memoria esta dado por n.Cm(T0)+3, donde 3 es el tamaño en palabras del
descriptor por convención (para algunas implementaciones de PASCAL).
Ejemplo :
a) Sea la siguiente definición:
tipo Arr = arreglo [Li .. Ls] de T0
Entonces, cualquier instancia del tipo Arr requiere como espacio:
Cm(Arr) = 3+(Ls-Li+1).Cm(T0 )
b) Sea la siguiente definición:
tipo Matriz = arreglo[1..N, 1..M] de T0
Entonces, cualquier instancia del tipo Matriz requiere como espacio:
Cm(Arr) = 6+N.M.Cm(T0 )
3.2) En el caso de los registros (estructura heterogénea porque las componentes pueden ser
de distinto tipo) se requiere una cantidad de memoria equivalente a la suma del espacio
requerido para cada componente. Sea la siguiente definición:
tipo Reg = registro de
C1 : T1 ;
C2 : T2 ;
...
Cn : Tn ;
fregistro
donde Ci es el campo i-ésimo del registro y de tipo Ti. Entonces, el requerimiento en
espacio para cuaquier objeto de tipo Reg viene dado por la ecuación:
n
Cm(Reg)= ∑ Cm(Ti )
i =1
9
Para un registro variante, se realiza el cálculo de manera similar, sólo que entre los campos
variantes se considera sólo el que ocupa mayor cantidad de espacio.
4) Para una instancia de tipo referencia (apuntador o dirección), se asume el costo de una
palabra.
5) El costo en memoria para una constante es el mismo que para variables del tipo al que
pertenece.
6) La definición de un tipo no tiene costo alguno. Esta definición sirve para declarar
instancias con ese tipo que si ocuparán espacio de memoria por ser direccionables.
2.4 Ejemplo
En esta sección se realizará un análisis de complejidad en tiempo y espacio para el
algoritmo de clasificación por burbuja, que ordena ascedentemente un arreglo de registros
con campo clave.
Sea la siguiente definición de tipos:
tipo Cadena = arreglo [1..40] de caracter;
tipo Reg = registro de
NumCarnet: entero; {Campo clave }
Nombre: Cadena
Sueldo: real;
fregistro
tipo Empleados = arreglo[1..n] de Reg; {n = Número de empleados}
Se define además una variable A de tipo Empleados. La acción que ordena el arreglo A es
el siguiente:
acción burbuja (var A: Empleados);
var
i,j: entero
aux: Reg;
para i:=1 hasta n-1 hacer
para j:=n hasta i+1 hacer
si A[j-1].NumCarnet > A[j].NumCarnet entonces
aux := A[j-1];
A[j-1] := A[j];
A[j] := aux;
fsi
fpara
fpara
facción
10
c1 . Las acciones internas del condicional (3 asignaciones) son de orden constante (que se
pueden asumir iguales, por ejemplo c2 ). Luego, por la regla 4 (regla del condicional) se
tiene Tcond(n) = O(max{c1 , c2 }) = c.
La complejidad del ciclo interno viene dada por:
n n
TCicloInterno(n) = ∑ Tj(n) + 2.c.( n − i) =
j =i +1
∑ c + 2.c.( n − i) = c.(n-i)+2.c.(n-i) = 3.c.(n-i)
j =i +1
El ciclo externo se ejecuta n-1 veces; luego, su complejidad viene dada por:
n −1 n −1
TCicloExterno(n) = ∑ TCicloInterno (n) + 2.c.(n − 1) = 3.c ∑ (n − i) + 2.c.( n − 1)
i =1 i =1
n−1 n −1
n.( n − 1)
= 3.c ∑ n − ∑ i + 2.c.( n − 1) = 3.c n.( n − 1) − + 2.c.( n − 1)
i=1 i =1 2
=
3
2
3 1 1
[
.c.n.( n − 1) + 2.c.(n − 1) = .c.n 2 − c.n − 2.c = 3.c.n 2 − c.n − 4.c
2 2 2
]
Luego, la complejidad en tiempo T(n) del algoritmo de la burbuja es: T(n) = TCicloExterno (n)
que es O(n2 ); finalmente, T(n)= O(n2 ). Entonces, para ejecutar el algoritmo de
ordenamiento mostrado arriba, es necesario un tiempo proporcional al cuadrado del número
de elementos a clasificar.
La complejidad de un algoritmo también se puede contabilizar en base al número de
operaciones fundamentales que realiza (en este ejemplo, las comparaciones), ya que las
otras operaciones generarán un factor multiplicativo o aditivo en la función T(n), el cual es
despreciable al calcular el orden de complejidad. Así que para calcular el orden del
algoritmo de burbuja basta con hallar el número de comparaciones C(n) y determinar el
orden a partir de éste.
Según esto, en la iteración i del ciclo interno se realizan: Ci(n) = n-i comparaciones. El
ciclo externo se ejecuta n-1 veces; por lo tanto, el número de comparaciones C(n) viene
dado por:
n −1
n(n − 1) n 2 − n
C(n) = ∑ (n − i) =
i =1 2
=
2
que es O(n2 ); luego, T(n)= O(n2 ).
Para calcular la complejidad en espacio del algoritmo, de manera ordenada, se determina la
complejidad en espacio para cada tipo de objeto en el algoritmo, y luego se calcula la suma
del número de palabras que requiere cada variable involucrada en el algoritmo.
− El tipo Cadena es un arreglo de 40 caracteres, por lo que Cm(Cadena)=40+3=43
palabras de memoria.
− El Tipo Reg require Cm(Reg)=1+43+1=45 palabras de memoria.
− El Tipo Empleados requiere Cm(Empleados)=n.Cm(Reg)+3=43.n+3 palabras de
memoria.
11
El algoritmo involucra:
− Tres enteros: i,j,n, cada uno ocupa Cm(Entero)=1 palabra de memoria.
− Una variable de tipo Reg: aux, que ocupa Cm(Reg)=45
− Un arreglo A de tipo Empleados que ocupa Cm(Empleados) palabras de memoria
Así, la complejidad en espacio requerido viene dado por:
3.Cm(Entero)+1.Cm(Reg)+1.Cm(Empleados)=3+45+43.n+3=43.n+51 palabras
lo cual es Ο(n).
Esta función ordena una lista de elementos realizando particiones sucesivas hasta obtener
listas de un elemento y aplicando el algoritmo tradicional de mezcla entre sub-listas
ordenadas. Por ejemplo, para la lista (5, 2, 4, 6) la función MergeSort divide la lista hasta
obtener sub-listas unitarias y en los retornos realiza las siguientes mezclas (cada nivel del
gráfico corresponde a un nivel de la recursion):
5 2 4 6
mezcla mezcla
2 5 4 6
mezcla
2 4 5 6
La función de complejidad T(n) para el algoritmo mergeSort viene dada por una definición
recursiva. En particular, se distingue el tratamiento de listas unitarias y el tratamiento de
listas de tamaño mayor o igual que dos.
12
Para el caso de listas unitarias (condición n=1 en la función mergeSort) se tiene una
complejidad constante que denominaremos c1 . Para listas de tamaño superior a uno (parte
else de la función mergeSort) el problema es dividido en dos sub-problemas de tamaño
n/2 con un costo adicional para:
− construir dos listas de tamaño n/2, con costo c3 .n
− mezclar dos listas de tamaño n/2, con costo c4 .n
Luego, tomando c2 = c3 + c4 , se tiene:
c1 si n =1
T(n) =
2.T ( n 2) + c 2 .n si n >1
donde se asume que n es potencia de 2 (n = 2k, k ≥ 0). Para determinar el orden de
complejidad de la función mergeSort es necesario resolver la ecuación de recurrencia
asociada a T(n). A manera de ejemplo, se presentarán tres enfoques para determinar el
orden de un algoritmo cuya función T(n) es una ecuación de recurrencia:
− Proponer una cota superior f(n) para T(n); o sea, demostrar que T(n) ≤ f(n) para un f
dado
− Hallar una forma cerrada 3 para T(n)
− Emplear soluciones conocidas para recurrencias
En lo que resta de esta sección se ilustrarán estos tres enfoques para resolver la función de
complejidad en tiempo T(n) asociada a la función mergeSort.
3
una forma cerrada es una función (no recursiva) en términos de n y algunas constantes
13
≤ a.n. log 2 ( n) − a.n + 2.b + c 2 .n
≤ a.n. log 2 ( n) + b (siempre y cuando a ≥ c2 + b [r2 ])
Luego, si se seleccionan a y b de manera que cumplan con las restricciones [r1 ] y [r2 ]
citadas anteriormente, por ejemplo: b = c1 y a = c1 + c2 , se cumple que para todo n:
T(n) ≤ (c1 + c2 ).n.log2 (n) + c1
Luego, T(n) = O(n.log2 (n)). Cabe destacar que esta no es la tasa exacta de T(n), pero se
comprueba que T(n) no es peor que c.n.log2 (n).
14
Por razones de espacio, no se cita la demostración del teorema; para mayor detalles de esta
demostración se recomienda [AHU 83]. Este tipo de soluciones generales pueden utilizarse
para hallar el orden de complejidad de funciones T(n). En particular, para la función de
complejidad de mergeSort :
c1 si n =1
T(n) =
2.T ( n 2) + c 2 .n si n >1
15
máximo como el mínimo de cada sub-conjunto, mediante aplicaciones recursivas. El
algoritmo basado en esta estrategia viene dado por:
función max_min(C: Conjunto_Enteros) → tupla_enteros;
var
max1, max2, min1, min2: entero;
si || C || = 2 entonces // sea C el conjunto {a,b}
return (max(a,b), min(a,b));
sino
divide C en dos sub-conjuntos C1 y C2, cada uno con la mitad de los elementos
(max1, min1) := max_min(C1);
(max2, min2) := max_min(C2);
return (max(max1,max2), min(min1,min2));
fsi
ffuncion
Sea T(n) el número total de comparaciones requeridas por max_min para encontrar el
máximo y el mínimo del conjunto C. Es obvio que T(2) = 1. Si n>2, T(n) es el número total
de comparaciones utilizadas en las dos llamadas de max_min sobre conjuntos de tamaño
n/2, más las dos comparaciones utilizadas para determinar el máximo y el mínimo de los
sub-conjuntos. Luego:
1 n=2
T ( n) =
2T ( n / 2) + 2, n > 2
La ecuación de recurrencia T(n) puede ser resuelta de manera general. La función
3
T(n) = n − 2
2
es la solución de la ecuación de recurrencia presentada anteriormente. Por lo tanto, se
demuestra que la segunda solución es más eficiente que la primera.
3.2 Balanceo
En general, las estrategias de particionar un problema utilizan sub-problemas de igual
tamaño, pero esto no es una condición necesaria. Por ejemplo, la clasificación u
ordenamiento por insersión puede considerarse como la división de un problema en dos
sub-problemas, uno de tamaño 1 y otro de tamaño n-1, con un costo máximo de n pasos
para combinarlos. Esto se puede expresar la mediante la ecuación de recurrencia:
T(n) = T(1) + T(n-1) + n
que es de orden O(n2 ). Si se utiliza la clasificación por intercalación (mergesort), que divide
el problema en dos sub-problemas de tamaño n/2 y combina las permutaciones obtenidas,
se tiene una función de complejidad representada por:
0 n =1
T ( n) =
2T ( n / 2) + n − 1, n > 1
que es de orden O(n.log n).
Como principio general, dividir un problema en sub-problemas iguales o casi iguales es un
factor crucial para la obtención de un buen rendimiento.
16
4 Modelos de Computación
En esta sección se definen diversos modelos teóricos de computación utilizados para
evaluar la complejidad de algoritmos. Estos modelos permiten hacer un estudio formal de la
complejidad de un programa (asociado a un algoritmo) en términos de modelos de
computación concretos, cercanos a los modelos de cómputo reales. Esto permite hacer
estudios comparativos independientes de la plataforma de hardware.
17
− Cambiar el estado del control finito.
− Escribir nuevos símbolos sobre los actuales en cualquiera de las celdas de las diversas
cintas.
− Mover cualquiera de las cabezas de las cintas a la izquierda o a la derecha.
Una máquina de turing puede ser vista formalmente como una 7-tupla: (Q, T, I, δ, b, q0 , qf),
donde: Q es el conjunto de estados, T es el conjunto de símbolos de las cintas, I es el
conjunto de símbolos de entrada, b es el símbolo blanco o nulo, q0 es el estado inicial, qf es
el estado final y δ es la función de transición de estados.
Una máquina de turing constituye un modelo formal para reconocedores de lenguajes. Para
esto es necesario colocar en la primera cinta la cadena a ser reconocida; luego, la cadena es
reconocida si y sólo si, partiendo del estado inicial, la máquina realiza una secuencia de
pasos tales que alcanza un estado de aceptación.
El modelo definido por una máquina de turing es utilizado para determinar cotas inferiores
para las funciones de complejidad tanto en espacio como en tiempo de la solución de un
problema determinado.
Este modelo general es conocido también como Máquina de Turing Determinísitica
(MTD). Adicionalmente, se puede definir una Máquina de Turing No Determinística
(MTND) como aquella en la cual la función de transición δ tiene más de un resultado para
pares estado-símbolo. Desde otro punto de vista, puede afirmarse que un programa en una
MTND difiere de un programa de una MTD en que el primero se ejecuta en dos etapas:
etapa de pronóstico (que consiste en obtener una secuencia de pronóstico o cadena inicial
para ejecutar el programa de la MT) y una etapa de chequeo (que opera bajo las mismas
reglas que una MTD).
18
NP puede ser transformado en tiempo polinomial en L0 . Cabe destacar que tanto L como L0
representan problemas, expresados como lenguajes.
Las relaciones entre las clases de problemas se muestran en la siguiente gráfica:
NP
NP-completo
Referencias
[AHU 74] AHO A., HOPCROFT J., ULLMAN J. The Design and Analysis of Computer
Algorithms. Addison-Wesley Publishing Company. 1974.
[AHU 83] AHO A., HOPCROFT J., ULLMAN J. Data Structures and Algorithms.
Addison-Wesley Publishing Company. 1983.
[Bro 89] BROOKSHEAR J. G., Theory of Computation. Formal Languages, Automata
and Complexity. The Benjamin/Cummings Publishing Company, Inc.,
Reedwood City, California, USA. 1989.
[GJ 79] GAREY M., JOHNSON D. Computers and Intractability: A Guide to the
Theory of NP-Completeness. Series of Books in the Mathematical Sciences.
W. H. Freeman & Co. 1979.
[GPK 94] GRAHAM R., PATASHNIK O., KNUTH D.E. Concrete Mathematics: A
Foundation for Computer Science. Addison-Wesley Publishing Company. 2nd.
Edition. March 1994.
[Mat 97] MATTEO A. Apuntes del curso "Estructuras de Datos y Análisis de
Algoritmos" . Postgrado en Ciencias de la Computación. 1997.
19