Complejidad Temporal y Espacial
Módulo inicial del curso: introduce el análisis de recursos, el conteo de operaciones elementales y la construcción de T(n).
1. Complejidad Temporal y Espacial
El análisis de algoritmos estudia cómo se comportan los algoritmos en términos de los recursos que consumen: principalmente tiempo (velocidad de ejecución) y espacio (memoria utilizada). El objetivo es poder comparar algoritmos de forma independiente a la máquina o al programador que los implemente.
1.1 Principio fundamental
Si dos programadores N y D implementan el mismo algoritmo de forma diferente, ambas implementaciones tendran el mismo orden de crecimiento. La diferencia entre ellas sera, a lo sumo, una constante multiplicativa:
Esto significa que dos implementaciones del mismo algoritmo solo difieren por un factor constante. Por eso, cuando hablamos de eficiencia, desligamos el algoritmo de su implementacion.
Siempre hay que analizar el algoritmo, no el programa. El lenguaje de programacion y el estilo del programador afectan las constantes, pero no el orden de crecimiento.
1.2 Complejidad Temporal
La complejidad temporal mide cuántas operaciones ejecuta un algoritmo en función del tamaño de la entrada n. Se calcula contando, línea por línea, cuántas veces se ejecuta cada instrucción y a que costo.
¿Qué son las operaciones elementales (OE)?
Son operaciones cuyo costo se considera proporcionalmente estable para el análisis. El modelo RAM (Random Access Machine) define cuáles son:
Operaciones elementales segun el modelo RAM
| Tipo de OE | Ejemplos | Costo |
|---|---|---|
| Aritmeticas | +, -, *, / | 1 OE |
| Asignacion / Acceso | x <- 5, A[i] | 1 OE |
| Comparaciones | <, >, =, <=, >=, <> | 1 OE |
| Booleanas | and, or, not | 1 OE |
| Llamado a subrutina | llamar(funcion) | 1 OE (el cuerpo se analiza aparte) |
Los end (endfor, endwhile, endif) no se cuentan cómo operaciones. El cuerpo de un ciclo siempre se ejecuta una vez menos que la cabecera.
Estructura de un algoritmo (pseudocódigo)
Un algoritmo en pseudocódigo tiene la siguiente estructura:
# Variables globales (si las hay)
tipo NombreAlgoritmo (E tipo param1, E/S tipo param2) {
# Área de variables locales
tipo var1, var2
# Cuerpo del algoritmo
instrucciones...
return valor
# Si retorna algo: es FUNCIÓN
# Si no retorna: es PROCEDIMIENTO
}1.3 Ejemplo 1 - Algoritmo con un ciclo for (AlgoritmoSinMente)
Determinar la función de eficiencia T(n) del siguiente algoritmo.
Pseudocódigo
int AlgoritmoSM (E int n) {
int i, suma
suma <- 0 # 1 vez --> 1 * C1
for i <- 1 to n do # n+1 veces --> (n+1) * C2
suma <- suma+i # n veces --> n * C3
endfor
return suma # 1 vez --> 1 * C4
}1 Seguimiento línea por línea
Seguimiento línea por línea del algoritmo con un ciclo for
| Línea de código | Veces que se ejecuta | OE por ejecución | Costo total |
|---|---|---|---|
| suma <- 0 | 1 | 1 | 1 * C1 |
| for i <- 1 to n do | n + 1 | 3 | (n+1) * C2 |
| suma <- suma + i | n | 2 | n * C3 |
| return suma | 1 | 1 | 1 * C4 |
¿Por qué el for tiene 3 OE? Porque en cada pasada ocurren tres cosas: se evalúa la comparación i <= n, al terminar el cuerpo se incrementa i <- i+1 y la primera vez se hace la asignación i <- 1. Por eso la cabecera cuesta 3 OE por pasada.
El cuerpo de un ciclo for se ejecuta siempre una vez menos que la cabecera: si la cabecera va n+1 veces, el cuerpo va n veces. La ultima pasada de la cabecera es la que falla la condicion y no ejecuta el cuerpo.
2 Análisis tabulando (verificación empírica)
Corremos el algoritmo mentalmente para valores pequeños de n y contamos cuántas OE ocurren. Esto nos da una idea de cómo crece T(n) antes de calcularla formalmente.
Verificación empírica para valores pequeños de n
| n | suma<-0 (1 vez) | for cabecera (n+1) veces * 3 | suma<-suma+i (n veces) * 2 | return (1 vez) | T(n) total |
|---|---|---|---|---|---|
| 1 | 1 | 2*3 = 6 | 1*2 = 2 | 1 | 10 |
| 2 | 1 | 3*3 = 9 | 2*2 = 4 | 1 | 15 |
| 3 | 1 | 4*3 = 12 | 3*2 = 6 | 1 | 20 |
| 4 | 1 | 5*3 = 15 | 4*2 = 8 | 1 | 25 |
| 5 | 1 | 6*3 = 18 | 5*2 = 10 | 1 | 30 |
| n | 1 | (n+1)*3 | n*2 | 1 | 5n + 5 |
Observa que cuando n aumenta en 1, T(n) siempre aumenta en 5. Eso confirma que el crecimiento es lineal: cada unidad adicional de n cuesta siempre lo mismo.
Si la diferencia entre filas consecutivas de T(n) es constante, la función es lineal. Si la diferencia crece linealmente, es cuadrática. Si crece exponencialmente, es exponencial.
3 Tecnica de sumatorias para obtener T(n)
Este algoritmo no requiere sumatorias complejas porque cada línea se ejecuta un número fijo de veces. Simplemente sumamos los costos de todas las líneas:
Donde a = C2 + C3 y b = C1 + C2 + C4 son constantes positivas. El resultado es una función lineal en n.
Para ver propiedades de sumatorias simples y cómo resolvero, consulta la sección 9 de este documento.
-> orden-> crecimiento lineal.
1.4 Ejemplo 2 - Ciclos anidados (AlgoritmoSM2)
Determinar la función de eficiencia de dos ciclos for anidados donde ambos van de 1 a n (límite fijo).
Pseudocódigo
AlgoritmoSM2 (E int A[n]) {
x <- 1 # 1 vez --> 1*1
for i <- 1 to n do # n+1 veces --> (n+1)*3
for j <- 1 to n do # n(n+1) veces --> n(n+1)*3
x <- x + i # n*n veces --> n^2 * 2
endfor
endfor
}1 Seguimiento línea por línea
Costos por línea en ciclos anidados con límite fijo
| Línea de código | Veces que se ejecuta | OE por ejecución | Costo total |
|---|---|---|---|
| x <- 1 | 1 | 1 | 1 |
| for i <- 1 to n do | n+1 | 3 | (n+1) * 3 |
| for j <- 1 to n do | n*(n+1) | 3 | n*(n+1) * 3 |
| x <- x + i | n*n | 2 |
¿Por qué el for interno se ejecuta n*(n+1) veces? El for externo tiene n+1 pasadas de cabecera. Para cada una de esas pasadas, el for interno también tiene n+1 pasadas propias. Pero el cuerpo externo solo ocurre n veces, así que el for interno se lanza n veces, y cada lanzamiento tiene n+1 pasadas de cabecera. Total: n * (n+1).
2 Análisis tabulando (verificación empírica)
Contamos cuántas veces se ejecuta la línea más interna x <- x + i para cada n:
Verificación empírica para ciclos anidados con límite fijo
| n | Iteraciones (for externo) | Iteraciones (for interno por cada externo) | Total x<-x+i (n * n) | T(n) aprox. |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1+6+6+2 = 15 |
| 2 | 2 | 2 | 4 | 1+9+18+8 = 36 |
| 3 | 3 | 3 | 9 | 1+12+36+18 = 67 |
| 4 | 4 | 4 | 16 | 1+15+60+32 = 108 |
| 5 | 5 | 5 | 25 | 1+18+90+50 = 159 |
| n | n | n |
La diferencia entre filas crece: 21, 31, 41, 51... (aumenta en 10 cada vez). Diferencia de la diferencia = 10 = constante, función cuadrática confirmada.
3 Tecnica de sumatorias
Como ambos límites son fijos (no dependen del otro índice), no necesitamos sumatorias anidadas complejas. El cuerpo se ejecuta exactamenteveces:
Y armamos T(n) sumando todos los costos:
La formulase explica en la sección 9, tabla de Sumatorias Dobles con límites fijos.
-> orden-> crecimiento cuadrático.
1.5 Ejemplo 3 - For anidado con límite variable
Ahora el ciclo interno va de 1 a i (límite variable que depende del externo). Este es el caso más frecuente en el análisis de algoritmos de ordenamiento.
Pseudocódigo
AlgoritmoSM2 (E int A[n]) {
x <- 1 # 1 vez
for i <- 1 to n do # n+1 veces
for j <- 1 to i do # LÍMITE VARIABLE: depende de i
x <- x + i
endfor
endfor
}1 Seguimiento detallado (prueba de escritorio con n=5)
Rastreamos manualmente que valores toma j para cada valor de i, y contamos ejecuciones:
Prueba de escritorio con n=5
| i | j recorre valores | Ejecuciones cabecera for j (i+1 veces) | Ejecuciones cuerpo x<-x+i (i veces) |
|---|---|---|---|
| 1 | j = 1, 2 (sale en 2) | 2 | 1 |
| 2 | j = 1, 2, 3 (sale en 3) | 3 | 2 |
| 3 | j = 1, 2, 3, 4 (sale en 4) | 4 | 3 |
| 4 | j = 1, 2, 3, 4, 5 (sale en 5) | 5 | 4 |
| 5 | j = 1, 2, 3, 4, 5, 6 | 6 | 5 |
| TOTAL (n=5) | - | 2+3+4+5+6 = 20 | 1+2+3+4+5 = 15 |
Observacion: Cuando i=1, j recorre 1 y 2 (la cabecera se ejecuta 2 veces = i+1, el cuerpo 1 vez = i). La cabecera siempre se ejecuta una vez mas que el cuerpo.
2 Análisis tabulando — verificar el patrón cuadrático
Verificación del patrón cuadrático
| n | Ejecuciones cabecera for j SUM(i+1) | Ejecuciones cuerpo x<-x+i SUM(i) | T(n) aprox. |
|---|---|---|---|
| 1 | 2 | 1 | pequena |
| 2 | 2+3=5 | 1+2=3 | mayor |
| 3 | 2+3+4=9 | 1+2+3=6 | mayor |
| 4 | 2+3+4+5=14 | 1+2+3+4=10 | mayor |
| 5 | 20 | 15 | mayor |
| n | n(n+1)/2 + n | n(n+1)/2 |
Las columnas del centro crecen como n(n+1)/2, una expresión cuadrática. Eso confirma que el algoritmo es cuadrático.
3 Resolucion formal con sumatorias
Sumatoria de la cabecera del for interno: el for j <- 1 to i tiene su cabecera ejecutandose i+1 veces (i pasadas que cumplen + 1 que falla).
Sumatoria del cuerpo x <- x + i:
Este es el truco mas usado en el curso. Escribimos la suma dos veces, una normal y otra invertida, y las sumamos:
Las formulasyestán en la sección 9. También la propiedad de linealidad.
Construccion de T(n): sumamos los costos de cada linea:
-> orden-> crecimiento cuadrático.
1.6 Ejemplo 4 - For con while interno (crecimiento logarítmico)
Este es el caso más rico: el ciclo interno es un while que duplica j, lo que produce un comportamiento logarítmico. Es el patrón que explica por qué algoritmos como búsqueda binaria o Merge Sort son tan eficientes.
Pseudocódigo
AlgoritmoXXX (E int n) {
x <- 1 # 1 vez --> 1*1
for i <- 1 to n do # n+1 veces --> (n+1)*3
j <- 1 # n veces --> n*1
while (j <= n) do # n*(log2(n)+2) veces --> n*(log2(n)+2)*1
j <- j*2 # n*(log2(n)+1) veces --> n*(log2(n)+1)*2
endwhile
endfor
}1 Seguimiento detallado — ¿cuántas veces se ejecuta el while?
La clave es entender que hace j <- j*2. Cada vez que pasa el while, j se duplica:
El while termina cuando j > n, es decir cuando 2^k > n. ¿Cuántas veces se duplica antes de superar n?
- +1 porque j empieza en 1 (= 2^0), no en 2 (= 2^1). La primera iteración no avanza hacia n, solo establece la posición inicial.
- +1 porque la cabecera del while se evalúa una vez más al final: es la evaluación que falla la condición y causa la salida.
Por tanto, la cabecera del while se ejecutaveces por cada iteracion de i, y el cuerpoveces.
Conteo de cabecera y cuerpo del while
| n | Valores que toma j (para un i fijo) | Veces cabecera while (log2(n)+2) | Veces cuerpo j<-j*2 (log2(n)+1) |
|---|---|---|---|
| 1 | j=1 -> sale | 2 | 1 |
| 2 | j=1, 2 -> sale en j=4 | 3 | 2 |
| 4 | j=1, 2, 4 -> sale en j=8 | 4 | 3 |
| 8 | j=1, 2, 4, 8 -> sale en j=16 | 5 | 4 |
| 16 | j=1, 2, 4, 8, 16 -> sale en j=32 | 6 | 5 |
| 32 | j=1, 2, 4, 8, 16, 32 -> sale en j=64 | 7 | 6 |
| n | valores |
Comono siempre es entero (ej:), usamos la función piso:. El piso de 2.32 es 2. Esto garantiza que contamos un número entero de iteraciones. Para el análisis asintótico,yson del mismo orden.
2 Análisis tabulando — verificar el crecimiento n*log(n)
Como el while tieneiteraciones por cada uno de losvalores de i:
Verificación del crecimiento n log n
| n | log2(n) (aprox.) | While por cada i | Total while (n * pasos) | T(n) aprox. |
|---|---|---|---|---|
| 1 | 0 | 2 | 1*2 = 2 | pequena |
| 2 | 1 | 3 | 2*3 = 6 | mediana |
| 4 | 2 | 4 | 4*4 = 16 | mayor |
| 8 | 3 | 5 | 8*5 = 40 | mayor |
| 16 | 4 | 6 | 16*6 = 96 | mayor |
| 32 | 5 | 7 | 32*7 = 224 | mayor |
| n | log n | log n + 2 | n*(log n+2) | ~ n*log(n) |
Cada vez que n se duplica, el while agrega un paso mas y el for el doble de iteraciones. El resultado: el costo crece cómo, no cómo.
3 Resolucion formal con sumatorias
El patrón clave: para una j que empieza en 1 y se duplica, el número de iteraciones del while es. Como i recorrevalores, la línea del while se ejecuta en total:
El cuerpo del while j <- j*2:
Construccion de T(n):
La unica diferencia seria la base del logaritmo: se convertiria en. Como todos los logaritmos son del mismo orden, el orden asintótico sigue siendo.
La sumatoriaestá en la sección 9, tabla de Sumatorias Dobles con ciclos logarítmicos.
-> orden-> crecimiento linealitmico.
1.7 Complejidad Espacial
La complejidad espacial mide cuanta memoria adicional requiere un algoritmo. Se cuentan las variables locales, parametros y estructuras de datos utilizadas.
Casos frecuentes de complejidad espacial
| Caso | Descripcion | Ejemplo |
|---|---|---|
| - Constante | Usa un número fijo de variables, sin importar n | Variables simples: i, suma, x |
| - Lineal | Usa un arreglo o estructura de tamaño proporcional a n | Arreglo A[n] |
| - Cuadratica | Usa una matriz n x n | Matriz A[n][n] |
| - Logaritmica | Pila de recursion en divide y venceras | Busqueda binaria recursiva |