Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Análisis de Complejidad Algorítmica

Fundamentos matemáticos del análisis asintótico

Universidad Nacional de Río Negro

Referencias y Lecturas Complementarias

Introducción

Desarrollo

Introducción: El Estudio de la Eficiencia

El análisis de algoritmos es una disciplina fundamental en la ciencia de la computación que se enfoca en cuantificar los recursos que un algoritmo consume. Su propósito es predecir, de manera formal y rigurosa, cómo se comportará un algoritmo en términos de tiempo de ejecución y uso de memoria a medida que el tamaño de la entrada de datos crece.

El objetivo principal no es obtener tiempos exactos en segundos —una métrica volátil que depende del hardware, el compilador y el sistema operativo— sino establecer una base teórica para:

  1. Comparar algoritmos: Determinar objetivamente cuál de dos algoritmos es más eficiente para resolver un mismo problema.

  2. Predecir la escalabilidad: Entender si un algoritmo seguirá siendo viable cuando el volumen de datos aumente de miles a millones o miles de millones de registros.

  3. Optimizar el código: Identificar los cuellos de botella y las partes críticas de un programa que más impactan en su rendimiento.

Para lograr esto, la herramienta central es el análisis asintótico.

Análisis Asintótico: Enfocándose en lo que Importa

El análisis asintótico es una metodología matemática que describe el comportamiento de una función en su límite, es decir, cuando el tamaño de la entrada (nn) se vuelve arbitrariamente grande (tiende al infinito). Este enfoque nos permite abstraernos de los detalles de la implementación y del hardware, y concentrarnos en la tasa de crecimiento intrínseca del algoritmo.

Al analizar una función de costo como T(n)=3n2+100n+500T(n) = 3n^2 + 100n + 500, observamos que para valores grandes de nn, el término 3n23n^2 domina a los demás. El análisis asintótico nos permite simplificar esta expresión a su orden de crecimiento, que es n2n^2, ignorando constantes multiplicativas (3) y términos de menor orden (100n+500100n + 500).

Las Notaciones Asintóticas: O, Ω, y Θ

Para formalizar este análisis, utilizamos un conjunto de notaciones que describen los límites del crecimiento de la función de costo de un algoritmo.

1. Notación Big O (O) - Cota Superior (Peor Caso)

La notación Big O es la más utilizada en la práctica, ya que describe una cota superior asintótica. Nos ofrece una garantía sobre el rendimiento del algoritmo: nunca será peor que esta cota.

Representación gráfica de la cota superior asintótica f(n) \in O(g(n)). A
partir de n_0, la función c \cdot g(n) es siempre mayor o igual a f(n).

Figure 1:Representación gráfica de la cota superior asintótica f(n)O(g(n))f(n) \in O(g(n)). A partir de n0n_0, la función cg(n)c \cdot g(n) es siempre mayor o igual a f(n)f(n).

2. Notación Omega (Ω) - Cota Inferior (Mejor Caso)

La notación Omega describe una cota inferior asintótica. Nos garantiza que el rendimiento del algoritmo nunca será mejor que esta cota.

3. Notación Theta (Θ) - Cota Ajustada (Caso Exacto)

La notación Theta proporciona la descripción más precisa del comportamiento de un algoritmo, acotándolo tanto por arriba como por abajo.

Representación gráfica de la cota ajustada asintótica f(n) \in \Theta(g(n)).
La función f(n) queda atrapada entre las cotas c_1 \cdot g(n) y c_2 \cdot
g(n) para todo n \ge n_0.

Figure 2:Representación gráfica de la cota ajustada asintótica f(n)Θ(g(n))f(n) \in \Theta(g(n)). La función f(n)f(n) queda atrapada entre las cotas c1g(n)c_1 \cdot g(n) y c2g(n)c_2 \cdot g(n) para todo nn0n \ge n_0.

Notaciones Menos Comunes
Little-o (Límite Asintótico Estricto)

f(n)o(g(n))f(n) \in o(g(n)) si para toda constante c>0c > 0, existe n0n_0 tal que:

0f(n)<cg(n)nn00 \leq f(n) < c \cdot g(n) \quad \forall n \geq n_0

Equivalentemente: limnf(n)g(n)=0\lim_{n \to \infty} \frac{f(n)}{g(n)} = 0

Ejemplo: no(n2)n \in o(n^2) pero no(n)n \notin o(n)

Little-omega (Límite Inferior Estricto)

f(n)ω(g(n))f(n) \in \omega(g(n)) si para toda constante c>0c > 0, existe n0n_0 tal que:

0cg(n)<f(n)nn00 \leq c \cdot g(n) < f(n) \quad \forall n \geq n_0
Propiedades Algebraicas

Las notaciones asintóticas tienen propiedades útiles:

  1. Transitividad: Si fO(g)f \in O(g) y gO(h)g \in O(h), entonces fO(h)f \in O(h)

  2. Reflexividad: fΘ(f)f \in \Theta(f)

  3. Simetría: Si fΘ(g)f \in \Theta(g), entonces gΘ(f)g \in \Theta(f)

  4. Suma: O(f)+O(g)=O(max(f,g))O(f) + O(g) = O(\max(f, g))

  5. Producto: O(f)O(g)=O(fg)O(f) \cdot O(g) = O(f \cdot g)

Ejercicios de Notaciones Asintóticas

Jerarquía de Complejidades

Jerarquía de las clases de complejidad más comunes, ordenadas de más eficiente a
menos eficiente.

Figure 3:Jerarquía de las clases de complejidad más comunes, ordenadas de más eficiente a menos eficiente.

Clasificación Detallada
Constante: O(1)O(1)

Características:

Código ejemplo:

1
2
3
int obtener_primero(int arr[], int n) {
    return arr[0];  // O(1): una operación, independiente de n
}
Logarítmica: O(logn)O(\log n)

Características:

Ejemplos: búsqueda binaria, operaciones en árboles balanceados

Código ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Búsqueda binaria: O(log n)
// Precondición: el arreglo 'arr' debe estar ordenado de menor a mayor.
int busqueda_binaria(int arr[], int n, int clave) {
    int izq = 0, der = n - 1;
    
    while (izq <= der) {  // Se reduce a la mitad en cada lazo
        int medio = izq + (der - izq) / 2;
        
        if (arr[medio] == clave) {
            return medio;
        }
        
        if (arr[medio] < clave) {
            izq = medio + 1;
        } else {
            der = medio - 1;
        }
    }
    
    return -1;
}

Análisis:

En cada iteración del lazo, el espacio de búsqueda se reduce a la mitad. Si inicialmente hay nn elementos, después de kk lazos quedan n2k\frac{n}{2^k}. El algoritmo termina cuando n2k=1\frac{n}{2^k} = 1, es decir, k=log2nk = \log_2 n.

Lineal: O(n)O(n)

Características:

Ejemplos: búsqueda secuencial, recorrer un arreglo, suma de elementos

Código ejemplo:

1
2
3
4
5
6
7
8
9
10
// Suma de elementos: O(n)
int sumar_elementos(int arr[], int n) {
    int suma = 0;
    
    for (int i = 0; i < n; i++) {  // n iteraciones
        suma += arr[i];  // O(1) por iteración
    }
    
    return suma;
}
Log-Lineal: O(nlogn)O(n \log n)

Características:

Ejemplos: Merge Sort, Heap Sort, Quick Sort (promedio)

Código ejemplo (Merge Sort):

1
2
3
4
5
6
7
8
9
10
// Merge Sort: O(n log n)
void merge_sort(int arr[], int izq, int der) {
    if (izq < der) {
        int medio = izq + (der - izq) / 2;
        
        merge_sort(arr, izq, medio);      // T(n/2)
        merge_sort(arr, medio + 1, der);  // T(n/2)
        merge(arr, izq, medio, der);      // O(n)
    }
}

Análisis: La recurrencia es T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n), que resuelve a T(n)=O(nlogn)T(n) = O(n \log n) por el Teorema Maestro.

Cuadrática: O(n2)O(n^2)

Características:

Ejemplos: Bubble Sort, Selection Sort, Insertion Sort

Código ejemplo:

1
2
3
4
5
6
7
8
9
10
// Bubble Sort: O(n²)
void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {           // n iteraciones
        for (int j = 0; j < n - i - 1; j++) {   // n-i iteraciones
            if (arr[j] > arr[j + 1]) {
                intercambiar(&arr[j], &arr[j + 1]);  // O(1)
            }
        }
    }
}

Análisis: Total de comparaciones = i=0n1(ni)=n(n1)2Θ(n2)\sum_{i=0}^{n-1} (n-i) = \frac{n(n-1)}{2} \in \Theta(n^2)

Cúbica: O(n3)O(n^3)

Características:

Ejemplos: multiplicación ingenua de matrices, algunos algoritmos de grafos

Código ejemplo:

1
2
3
4
5
6
7
8
9
10
11
// Multiplicación de matrices: O(n³)
void multiplicar_matrices(int A[][N], int B[][N], int C[][N], int n) {
    for (int i = 0; i < n; i++) {         // n iteraciones
        for (int j = 0; j < n; j++) {     // n iteraciones
            C[i][j] = 0;
            for (int k = 0; k < n; k++) { // n iteraciones
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
}
Exponencial: O(2n)O(2^n)

Características:

Ejemplos: subconjuntos de un conjunto, Torre de Hanoi, algunos problemas NP-completos

Código ejemplo:

1
2
3
4
5
6
7
// Fibonacci recursivo ingenuo: O(2^n)
int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);  // Dos llamadas recursivas
}

Análisis: La relación de recurrencia para el tiempo de ejecución es T(n)=T(n1)+T(n2)+O(1)T(n) = T(n-1) + T(n-2) + O(1). Para resolver la parte homogénea de esta ecuación de diferencias, T(n)T(n1)T(n2)=0T(n) - T(n-1) - T(n-2) = 0, proponemos una solución de la forma T(n)=rnT(n) = r^n. Al sustituir, obtenemos la ecuación característica:

r2r1=0r^2 - r - 1 = 0

cuyas raíces son r1=1+52=ϕ1.618r_1 = \frac{1+\sqrt{5}}{2} = \phi \approx 1.618 (la razón áurea) y r2=1520.618r_2 = \frac{1-\sqrt{5}}{2} \approx -0.618. La solución general de la recurrencia es una combinación lineal de ambas potencias, dominada asintóticamente por la raíz de mayor magnitud, por lo que T(n)Θ(ϕn)T(n) \in \Theta(\phi^n).

Factorial: O(n!)O(n!)

Características:

Ejemplos: generar todas las permutaciones, problema del viajante (fuerza bruta)

Código ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Generar permutaciones: O(n!)
void generar_permutaciones(int arr[], int inicio, int fin) {
    if (inicio == fin) {
        imprimir(arr, fin + 1);
        return;
    }
    
    for (int i = inicio; i <= fin; i++) {
        intercambiar(&arr[inicio], &arr[i]);
        generar_permutaciones(arr, inicio + 1, fin);  // (n-1)! llamadas
        intercambiar(&arr[inicio], &arr[i]);
    }
}
Comparación del crecimiento de diferentes funciones de complejidad para valores
de n hasta 100.

Figure 4:Comparación del crecimiento de diferentes funciones de complejidad para valores de nn hasta 100.

Tabla Comparativa de Crecimiento
nnlogn\log nnnnlognn \log nn2n^2n3n^32n2^nn!n!
10310331001K1K3.6M
20420864008K1M2.4×10182.4 \times 10^{18}
3053014790027K1B2.7×10322.7 \times 10^{32}
100710066410K1M1.3×10301.3 \times 10^{30}9.3×101579.3 \times 10^{157}
1000101K9.9K1M1B
Ejercicios de Jerarquía de Complejidades

Técnicas de Análisis

Análisis de Lazos
Lazo Simple
1
2
3
for (int i = 0; i < n; i++) {
    // Operación O(1)
}

Análisis: i=0n1O(1)=O(n)\sum_{i=0}^{n-1} O(1) = O(n)

Lazos Anidados
1
2
3
4
5
for (int i = 0; i < n; i++) {       // n iteraciones
    for (int j = 0; j < n; j++) {   // n iteraciones
        // Operación O(1)
    }
}

Análisis: i=0n1j=0n1O(1)=nnO(1)=O(n2)\sum_{i=0}^{n-1} \sum_{j=0}^{n-1} O(1) = n \cdot n \cdot O(1) = O(n^2)

Lazos con Dependencia
1
2
3
4
5
for (int i = 0; i < n; i++) {
    for (int j = i; j < n; j++) {  // Depende de i
        // Operación O(1)
    }
}

Análisis:

i=0n1j=in1O(1)=i=0n1(ni)=k=1nk=n(n+1)2O(n2)\sum_{i=0}^{n-1} \sum_{j=i}^{n-1} O(1) = \sum_{i=0}^{n-1} (n-i) = \sum_{k=1}^{n} k = \frac{n(n+1)}{2} \in O(n^2)
Lazo Logarítmico
1
2
3
for (int i = 1; i < n; i *= 2) {
    // Operación O(1)
}

Análisis: Si ii comienza en 1 y se duplica cada iteración, el lazo ejecuta kk veces donde 2k=n2^k = n, es decir, k=log2nk = \log_2 n. Por tanto, O(logn)O(\log n).

Análisis de Recursión
Método de Sustitución

El método de sustitución (o método de inducción matemática) se utiliza para resolver recurrencias mediante dos etapas:

  1. Expandir (desarrollar) la relación de recurrencia para adivinar el patrón de la solución.

  2. Probar la solución por inducción matemática para verificar su exactitud formal.

Ejemplo de desarrollo paso a paso: Consideremos la recurrencia T(n)=T(n1)+cT(n) = T(n-1) + c, donde cc es el costo constante de la operación básica (O(1)O(1)), con el caso base T(1)=dT(1) = d (donde dd es otra constante).

  1. Expansión por sustitución sucesiva: Comenzamos sustituyendo recursivamente la fórmula:

    • Paso 1: T(n)=T(n1)+cT(n) = T(n-1) + c

    • Paso 2: Sustituimos T(n1)T(n-1) usando la misma definición: T(n1)=T(n2)+cT(n-1) = T(n-2) + c.

      T(n)=(T(n2)+c)+c=T(n2)+2cT(n) = (T(n-2) + c) + c = T(n-2) + 2c
    • Paso 3: Sustituimos T(n2)=T(n3)+cT(n-2) = T(n-3) + c:

      T(n)=(T(n3)+c)+2c=T(n3)+3cT(n) = (T(n-3) + c) + 2c = T(n-3) + 3c
  2. Generalización del patrón: Podemos generalizar la expresión para el paso kk:

    T(n)=T(nk)+kcT(n) = T(n-k) + k \cdot c
  3. Aplicación del caso base: Deseamos alcanzar el caso base T(1)T(1). Para ello, definimos nk=1n - k = 1, lo que implica k=n1k = n - 1. Sustituyendo kk en nuestra ecuación generalizada:

    T(n)=T(1)+(n1)cT(n) = T(1) + (n-1) \cdot c

    T(n)=d+cncT(n) = d + c \cdot n - c

    T(n)=cn+(dc)T(n) = c \cdot n + (d - c)

Dado que cc y dd son constantes, la función de costo se reduce a una ecuación lineal:

T(n)Θ(n)T(n) \in \Theta(n)
Método del Árbol de Recursión

Antes de enunciar el Teorema Maestro, es fundamental visualizar cómo se distribuye el trabajo en un algoritmo recursivo de tipo divide y vencerás. El árbol de recursión es una herramienta gráfica donde:

Árbol de recursión ilustrando el Teorema Maestro y cómo se distribuye el trabajo
en cada nivel del árbol.

Figure 5:Árbol de recursión ilustrando el Teorema Maestro y cómo se distribuye el trabajo en cada nivel del árbol.

Ejemplo de análisis con árbol: Consideremos la recurrencia T(n)=2T(n/2)+nT(n) = 2T(n/2) + n (con caso base T(1)=O(1)T(1) = O(1)):

Nivel 0:                n              → costo: n
                       / \
Nivel 1:            n/2   n/2          → costo: n
                     / \   / \
Nivel 2:         n/4 n/4 n/4 n/4       → costo: n
                  ...
Teorema Maestro

El Teorema Maestro es una receta matemática que sistematiza este análisis para recurrencias de la forma general:

T(n)=aT(nb)+f(n)T(n) = aT\left(\frac{n}{b}\right) + f(n)

donde:

Al comparar el trabajo en las hojas del árbol (que es Θ(nlogba)\Theta(n^{\log_b a})) con el trabajo no recursivo en la raíz (f(n)f(n)), el Teorema Maestro determina cuál de los dos domina la complejidad asintótica:

Caso 1 (Dominan las hojas): Si f(n)O(nlogbaϵ)f(n) \in O(n^{\log_b a - \epsilon}) para algún ϵ>0\epsilon > 0, entonces:

T(n)Θ(nlogba)T(n) \in \Theta(n^{\log_b a})

Caso 2 (Trabajo balanceado): Si f(n)Θ(nlogbalogkn)f(n) \in \Theta(n^{\log_b a} \log^k n) para algún k0k \geq 0, entonces:

T(n)Θ(nlogbalogk+1n)T(n) \in \Theta(n^{\log_b a} \log^{k+1} n)

Caso 3 (Domina la raíz): Si f(n)Ω(nlogba+ϵ)f(n) \in \Omega(n^{\log_b a + \epsilon}) para algún ϵ>0\epsilon > 0, y se cumple la condición de regularidad (af(n/b)cf(n)a f(n/b) \leq c f(n) para alguna constante c<1c < 1 y nn suficientemente grande), entonces:

T(n)Θ(f(n))T(n) \in \Theta(f(n))

La condición de regularidad garantiza que la tasa de trabajo no recursivo decrezca geométricamente a medida que se desciende en el árbol de recursión. Si no se satisface esta condición, no se puede aplicar el Caso 3 del Teorema Maestro.

Ejemplos de aplicación:

  1. Merge Sort: T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)

    • a=2,b=2,f(n)=na=2, b=2, f(n)=n

    • logba=log22=1\log_b a = \log_2 2 = 1

    • f(n)=nΘ(n1)f(n) = n \in \Theta(n^1)Caso 2 con k=0k=0

    • Solución: T(n)Θ(nlogn)T(n) \in \Theta(n \log n)

  2. Búsqueda Binaria: T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)

    • a=1,b=2,f(n)=1a=1, b=2, f(n)=1

    • logba=0\log_b a = 0

    • f(n)=1Θ(n0)f(n) = 1 \in \Theta(n^0)Caso 2 con k=0k=0

    • Solución: T(n)Θ(logn)T(n) \in \Theta(\log n)

  3. Multiplicación de Karatsuba: T(n)=3T(n/2)+O(n)T(n) = 3T(n/2) + O(n)

    • a=3,b=2,f(n)=na=3, b=2, f(n)=n

    • logba=log231.585\log_b a = \log_2 3 \approx 1.585

    • f(n)=nO(n1.585ϵ)f(n) = n \in O(n^{1.585-\epsilon})Caso 1

    • Solución: T(n)Θ(nlog23)Θ(n1.585)T(n) \in \Theta(n^{\log_2 3}) \approx \Theta(n^{1.585})

Análisis Amortizado

El análisis amortizado considera el costo promedio de una secuencia de operaciones, permitiendo que algunas operaciones sean costosas si la mayoría son baratas.

Método del Agregado

Ejemplo: Arreglo dinámico redimensionable en C.

Supongamos que implementamos un arreglo dinámico en C mediante una estructura que almacena un puntero, el tamaño actual y la capacidad máxima de almacenamiento. Cuando el arreglo alcanza su capacidad límite, duplicamos su tamaño utilizando realloc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
    int *datos;
    size_t tamaño;
    size_t capacidad;
} arreglo_dinamico_t;

bool insertar_arreglo(arreglo_dinamico_t *arr, int valor) {
    if (arr->tamaño >= arr->capacidad) {
        size_t nueva_capacidad = arr->capacidad * 2;
        int *nuevo_espacio = realloc(arr->datos, nueva_capacidad * sizeof(int));
        if (nuevo_espacio == NULL) {
            return false;
        }
        arr->datos = nuevo_espacio;
        arr->capacidad = nueva_capacidad;
    }
    arr->datos[arr->tamaño] = valor;
    arr->tamaño++;
    return true;
}

Analicemos el costo de una secuencia de nn inserciones consecutivas en el lazo de carga, comenzando con una capacidad inicial de 1:

Para nn inserciones (donde nn es una potencia de 2), el costo total acumulado es la suma de los accesos normales y los costos de copia por redimensionamiento:

Costo Total=n+j=0log2n2j=n+(2log2n+11)=n+2n1<3n\text{Costo Total} = n + \sum_{j=0}^{\log_2 n} 2^j = n + (2^{\log_2 n + 1} - 1) = n + 2n - 1 < 3n

Costo amortizado: Al dividir el costo total por la cantidad de operaciones, obtenemos 3nn=O(1)\frac{3n}{n} = O(1) por cada inserción individual.

Método del Potencial

El método del potencial analiza la complejidad amortizada definiendo una función potencial Φ\Phi sobre los estados de la estructura de datos. Esta función asocia un número real no negativo Φ(Di)\Phi(D_i) a la estructura tras la operación ii.

El costo amortizado c^i\hat{c}_i de la ii-ésima operación se define como:

c^i=ci+Φ(Di)Φ(Di1)\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})

donde cic_i es el costo real de la operación y ΔΦi=Φ(Di)Φ(Di1)\Delta\Phi_i = \Phi(D_i) - \Phi(D_{i-1}) es el cambio en el potencial.

Análisis del Arreglo Dinámico Para un arreglo dinámico, definimos la función potencial después de la operación ii como:

Φi=2tici\Phi_i = 2 \cdot t_i - c_i

donde tit_i es el tamaño actual (número de elementos) y cic_i es la capacidad actual.

Precondición de validez: Como la capacidad es a lo sumo el doble del tamaño y al menos igual, tenemos que Φi0\Phi_i \ge 0. Inicialmente, con un arreglo vacío, t0=0t_0 = 0 y c0=0c_0 = 0, por lo que Φ0=0\Phi_0 = 0.

Analicemos los dos escenarios posibles para la ii-ésima inserción:

Escenario 1: Inserción sin Redimensionamiento

El arreglo tiene espacio libre (ti1<ci1t_{i-1} < c_{i-1}).

  1. El costo real es constante: ci=1c_i = 1 (copiar el elemento en el arreglo).

  2. El tamaño aumenta en uno (ti=ti1+1t_i = t_{i-1} + 1), y la capacidad permanece constante (ci=ci1c_i = c_{i-1}).

  3. El cambio en el potencial es:

    ΔΦi=ΦiΦi1=(2tici)(2ti1ci1)\Delta\Phi_i = \Phi_i - \Phi_{i-1} = (2 \cdot t_i - c_i) - (2 \cdot t_{i-1} - c_{i-1})

    ΔΦi=(2(ti1+1)ci1)(2ti1ci1)=2\Delta\Phi_i = (2(t_{i-1} + 1) - c_{i-1}) - (2 \cdot t_{i-1} - c_{i-1}) = 2
  4. El costo amortizado calculado es:

    c^i=ci+ΔΦi=1+2=3\hat{c}_i = c_i + \Delta\Phi_i = 1 + 2 = 3

Escenario 2: Inserción con Redimensionamiento

El arreglo está lleno (ti1=ci1t_{i-1} = c_{i-1}). Para insertar, se debe duplicar la capacidad: ci=2ci1c_i = 2 \cdot c_{i-1}.

  1. El costo real de esta inserción implica alocar nueva memoria y copiar todos los elementos existentes más el nuevo: ci=ti1+1c_i = t_{i-1} + 1.

  2. El tamaño aumenta en uno (ti=ti1+1t_i = t_{i-1} + 1), y la capacidad se duplica (ci=2ti1c_i = 2 \cdot t_{i-1}).

  3. Calculamos la variación del potencial ΔΦi\Delta\Phi_i:

    Φi1=2ti1ci1=2ti1ti1=ti1\Phi_{i-1} = 2 \cdot t_{i-1} - c_{i-1} = 2 \cdot t_{i-1} - t_{i-1} = t_{i-1}

    Φi=2tici=2(ti1+1)2ti1=2\Phi_i = 2 \cdot t_i - c_i = 2(t_{i-1} + 1) - 2 \cdot t_{i-1} = 2

    ΔΦi=ΦiΦi1=2ti1\Delta\Phi_i = \Phi_i - \Phi_{i-1} = 2 - t_{i-1}
  4. El costo amortizado calculado es:

    c^i=ci+ΔΦi=(ti1+1)+(2ti1)=3\hat{c}_i = c_i + \Delta\Phi_i = (t_{i-1} + 1) + (2 - t_{i-1}) = 3

Conclusión En ambos escenarios (con o sin redimensionamiento), el costo amortizado de una inserción en el arreglo dinámico es exactamente 3, lo que demuestra formalmente que la operación de inserción tiene una complejidad de amortización constante:

c^iΘ(1)\hat{c}_i \in \Theta(1)
Ejercicios de Técnicas de Análisis

Complejidad Espacial

La complejidad espacial mide la cantidad de memoria adicional que un algoritmo requiere.

Clasificación
Recursión y Pila de Llamadas

Cada llamada recursiva ocupa espacio en la pila. La profundidad máxima de recursión determina la complejidad espacial.

Ejemplo: Fibonacci recursivo

1
2
3
4
5
6
int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
Trade-off Tiempo-Espacio

A menudo es posible reducir tiempo usando más espacio (memoización) o viceversa.

Ejemplo: Fibonacci con memoización

1
2
3
4
5
6
7
8
9
10
11
int fibonacci_memo(int n, int memo[]) {
    if (n <= 1) {
        return n;
    }
    if (memo[n] != -1) {
        return memo[n];
    }
    
    memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo);
    return memo[n];
}
Ilustración del trade-off entre tiempo y espacio en el problema de Fibonacci.

Figure 6:Ilustración del trade-off entre tiempo y espacio en el problema de Fibonacci.

Ejercicios de Complejidad Espacial

Límites Inferiores y Óptimalidad

Límites Inferiores Basados en Información

Un límite inferior establece que ningún algoritmo puede resolver un problema más rápido que cierta complejidad.

Teorema: Ordenamiento por Comparación

Enunciado: Cualquier algoritmo que ordene nn elementos mediante comparaciones requiere al menos Ω(nlogn)\Omega(n \log n) comparaciones en el peor caso.

Demostración (árbol de decisión):

  1. Un algoritmo de ordenamiento por comparación puede modelarse como un árbol binario de decisión.

  2. Cada hoja representa una permutación posible de los nn elementos de entrada.

  3. Hay n!n! permutaciones posibles, por tanto, el árbol debe tener al menos n!n! hojas.

  4. Un árbol binario de altura hh tiene como máximo 2h2^h hojas.

  5. Para que el árbol pueda representar todas las salidas válidas, se requiere que 2hn!2^h \geq n!, lo que implica hlog2(n!)h \geq \log_2(n!).

  6. Demostramos la cota inferior de log2(n!)\log_2(n!) expandiendo la sumatoria y acotándola inferiormente desde su término medio: $$ \log_2(n!) = \sum_{i=1}^n \log_2 i \geq \sum_{i=n/2}^n \log_2 i \geq \sum_{i=n/2}^n \log_2(n/2) = \frac{n}{2} \log_2(n/2) = \frac{n}{2} (\log_2 n

      1. \in \Omega(n \log n) $$

Conclusión: Cualquier algoritmo basado en comparaciones requiere al menos Ω(nlogn)\Omega(n \log n) comparaciones en el peor caso. Algoritmos como Merge Sort y Heap Sort son, por lo tanto, óptimos.

Algoritmos Óptimos

Un algoritmo es asintóticamente óptimo si su complejidad coincide con el límite inferior teórico del problema.

Ejemplos:

Más Allá: Una Introducción a la Teoría de la Complejidad (P vs. NP)

Mientras que el análisis de algoritmos se enfoca en determinar la eficiencia de una solución específica, la teoría de la complejidad aborda una pregunta más fundamental: ¿cuál es la dificultad inherente de un problema? No se pregunta “¿cuán rápido es mi algoritmo para ordenar?”, sino “¿cuán rápido puede ser cualquier algoritmo que ordene?”.

Esta disciplina clasifica los problemas computacionales en clases de complejidad basadas en los recursos (tiempo y memoria) que se requieren para resolverlos en el peor de los casos, independientemente del algoritmo específico utilizado.

Conceptos de Complejidad: Intratabilidad y las Clases P y NP

En el análisis de algoritmos, no solo nos interesa determinar la complejidad asintótica exacta, sino también clasificar los problemas según si son resolubles de forma eficiente en la práctica. Esta distinción introduce la noción de intratabilidad.

Problemas Tratables vs. Intratables
Las Clases P y NP

Para formalizar esta clasificación, la teoría de la complejidad define conjuntos de problemas llamados clases de complejidad:

La Cuestión PNPP \neq NP y los Problemas NP-Completos

La relación entre estas clases plantea uno de los interrogantes abiertos más importantes de la ciencia de la computación: ¿Es P=NPP = NP? Es decir: si la solución a un problema se puede verificar eficientemente, ¿se puede también encontrar de forma eficiente?

El consenso científico generalizado es que PNPP \neq NP, lo que significa que verificar soluciones es fundamentalmente más sencillo que crearlas.

Dentro de la clase NP, existen problemas denominados NP-Completos. Estos problemas representan los elementos más difíciles de NP. Tienen la propiedad de que si se encontrara un algoritmo eficiente (polinomial) para resolver cualquiera de ellos, ese algoritmo podría adaptarse inmediatamente para resolver todos los problemas de la clase NP en tiempo polinomial, demostrando que P=NPP = NP.

Ejemplos clásicos:

Visualización de las Clases de Complejidad

Asumiendo que P ≠ NP, la relación entre estas clases se puede visualizar de la siguiente manera:

Frente a la intratabilidad de los problemas NP-Completos, en el desarrollo práctico de software se emplean algoritmos de aproximación, heurísticas o restricciones del dominio para hallar soluciones aceptables en tiempos razonables, sabiendo que una solución óptima general y rápida no es viable.

Ejemplos Detallados de Análisis

Ejemplo 1: Búsqueda del Máximo
1
2
3
4
5
6
7
8
9
10
11
int buscar_maximo(int arr[], int n) {
    int max = arr[0];          // O(1)
    
    for (int i = 1; i < n; i++) {  // n-1 iteraciones
        if (arr[i] > max) {    // O(1)
            max = arr[i];      // O(1)
        }
    }
    
    return max;  // O(1)
}

Análisis:

Ejemplo 2: Búsqueda de Duplicados
1
2
3
4
5
6
7
8
9
10
11
// Versión ingenua: O(n²)
bool tiene_duplicados_ingenuo(int arr[], int n) {
    for (int i = 0; i < n; i++) {           // n iteraciones
        for (int j = i + 1; j < n; j++) {   // (n-i-1) iteraciones
            if (arr[i] == arr[j]) {
                return true;  // O(1)
            }
        }
    }
    return false;
}

Análisis:

T(n)=i=0n1j=i+1n1O(1)=i=0n1(ni1)=n(n1)2O(n2)T(n) = \sum_{i=0}^{n-1} \sum_{j=i+1}^{n-1} O(1) = \sum_{i=0}^{n-1} (n-i-1) = \frac{n(n-1)}{2} \in O(n^2)
1
2
3
4
5
6
7
8
9
10
11
12
// Versión optimizada: O(n log n) con ordenamiento previo
bool tiene_duplicados_ordenado(int arr[], int n) {
    qsort(arr, n, sizeof(int), comparar);  // O(n log n)
    
    for (int i = 0; i < n - 1; i++) {      // O(n)
        if (arr[i] == arr[i + 1]) {
            return true;
        }
    }
    
    return false;
}

Análisis: T(n)=O(nlogn)+O(n)=O(nlogn)T(n) = O(n \log n) + O(n) = O(n \log n)

Ejemplo 3: Torres de Hanoi
1
2
3
4
5
6
7
8
9
10
void hanoi(int n, char origen, char destino, char auxiliar) {
    if (n == 1) {
        printf("Mover disco 1 de %c a %c\n", origen, destino);
        return;
    }
    
    hanoi(n - 1, origen, auxiliar, destino);
    printf("Mover disco %d de %c a %c\n", n, origen, destino);
    hanoi(n - 1, auxiliar, destino, origen);
}

Análisis mediante recurrencia: $$

T(n)=2T(n1)+1T(1)=1\begin{align} T(n) &= 2T(n-1) + 1 \\ T(1) &= 1 \end{align}

$$

Solución por sustitución: $$

T(n)=2T(n1)+1=2(2T(n2)+1)+1=4T(n2)+2+1=8T(n3)+4+2+1=2kT(nk)+(2k1+2k2++2+1)=2kT(nk)+(2k1)\begin{align} T(n) &= 2T(n-1) + 1 \\ &= 2(2T(n-2) + 1) + 1 = 4T(n-2) + 2 + 1 \\ &= 8T(n-3) + 4 + 2 + 1 \\ &= 2^k T(n-k) + (2^{k-1} + 2^{k-2} + \cdots + 2 + 1) \\ &= 2^k T(n-k) + (2^k - 1) \end{align}

$$

Cuando k=n1k = n-1:

T(n)=2n1T(1)+2n11=2n1+2n11=2n1Θ(2n)T(n) = 2^{n-1}T(1) + 2^{n-1} - 1 = 2^{n-1} + 2^{n-1} - 1 = 2^n - 1 \in \Theta(2^n)

Conclusión: Torres de Hanoi es inherentemente exponencial. No existe solución más eficiente.

Ejercicios de Autoevaluación

Solution to Exercise 1

Debemos encontrar constantes c1,c2,n0>0c_1, c_2, n_0 > 0 tales que:

c1n23n2+5n+2c2n2nn0c_1 n^2 \leq 3n^2 + 5n + 2 \leq c_2 n^2 \quad \forall n \geq n_0

Cota inferior (c1n23n2+5n+2c_1 n^2 \leq 3n^2 + 5n + 2):

  • Tomemos c1=3c_1 = 3.

  • Para n1n \geq 1: 3n23n2+5n+23n^2 \leq 3n^2 + 5n + 2 es verdadero ya que 5n+2>05n + 2 > 0.

Cota superior (3n2+5n+2c2n23n^2 + 5n + 2 \leq c_2 n^2):

  • Necesitamos un c2c_2 tal que la desigualdad se mantenga.

  • Para n1n \geq 1, se cumple que 5n5n25n \leq 5n^2 y 22n22 \leq 2n^2.

  • Entonces: 3n2+5n+23n2+5n2+2n2=10n23n^2 + 5n + 2 \leq 3n^2 + 5n^2 + 2n^2 = 10n^2.

  • Tomemos c2=10c_2 = 10.

Conclusión: Con c1=3c_1 = 3, c2=10c_2 = 10 y n0=1n_0 = 1, se cumple:

3n23n2+5n+210n2n13n^2 \leq 3n^2 + 5n + 2 \leq 10n^2 \quad \forall n \geq 1

Por lo tanto, por definición formal, f(n)Θ(n2)f(n) \in \Theta(n^2).

Solution to Exercise 2

Utilizando la fórmula de cambio de base para logaritmos, sabemos que:

logan=logbnlogba=(1logba)logbn\log_a n = \frac{\log_b n}{\log_b a} = \left(\frac{1}{\log_b a}\right) \log_b n

Dado que aa y bb son constantes mayores que 1, el término k=1logbak = \frac{1}{\log_b a} es una constante positiva fija.

Por definición de Big-Theta, una función f(n)Θ(g(n))f(n) \in \Theta(g(n)) si existen constantes c1,c2,n0>0c_1, c_2, n_0 > 0 tales que:

c1g(n)f(n)c2g(n)nn0c_1 \cdot g(n) \le f(n) \le c_2 \cdot g(n) \quad \forall n \ge n_0

Si elegimos c1=kc_1 = k, c2=kc_2 = k y n0=1n_0 = 1, se cumple la igualdad:

klogbnklogbnklogbnn1k \cdot \log_b n \le k \cdot \log_b n \le k \cdot \log_b n \quad \forall n \ge 1

Lo que demuestra formalmente que loganΘ(logbn)\log_a n \in \Theta(\log_b n). Por ende, en el análisis asintótico la base del logaritmo no afecta a la clase de complejidad y se escribe simplemente O(logn)O(\log n).

Solution to Exercise 3

La relación es verdadera.

Por definición, f(n)o(g(n))f(n) \in o(g(n)) si el límite del cociente de ambas funciones tiende a cero cuando nn tiende a infinito:

limnf(n)g(n)=0\lim_{n \to \infty} \frac{f(n)}{g(n)} = 0

Sustituyendo las funciones correspondientes:

limnnlognn2=limnlognn\lim_{n \to \infty} \frac{n \log n}{n^2} = \lim_{n \to \infty} \frac{\log n}{n}

Aplicando la regla de L’Hôpital (derivando numerador y denominador respecto a nn):

limn1n1=limn1n=0\lim_{n \to \infty} \frac{\frac{1}{n}}{1} = \lim_{n \to \infty} \frac{1}{n} = 0

Como el límite es 0, se cumple formalmente que nlogno(n2)n \log n \in o(n^2), lo que significa que nlognn \log n crece estrictamente más lento que n2n^2.

Solution to Exercise 4

El orden de crecimiento asintótico de menor eficiencia (crecimiento más rápido) a mayor eficiencia (crecimiento más lento) es:

n!>2n>n3>n2>nlogn>n>1000n! > 2^n > n^3 > n^2 > n \log n > \sqrt{n} > 1000
  • n!n!: Crecimiento factorial (inviable para n>15n > 15).

  • 2n2^n: Crecimiento exponencial (inviable para n>30n > 30).

  • n3n^3: Crecimiento cúbico.

  • n2n^2: Crecimiento cuadrático.

  • nlognn \log n: Crecimiento log-lineal.

  • n\sqrt{n}: Crecimiento sublineal (n0.5n^{0.5}).

  • 1000: Crecimiento constante (O(1)O(1)).

Solution to Exercise 5
  • a) O(n)O(n) (Lineal): El lazo visita cada uno de los nn elementos exactamente una vez.

  • b) O(1)O(1) (Constante): El acceso por índice calcula la dirección de memoria en tiempo fijo, independientemente del tamaño nn.

  • c) O(logn)O(\log n) (Logarítmica): En cada iteración del lazo se descarta la mitad de los elementos restantes.

  • d) O(n2)O(n^2) (Cuadrática): El lazo interno se ejecuta nn veces por cada iteración del lazo externo, acumulando n2n^2 operaciones.

Solution to Exercise 6

El tiempo de ejecución T(n)T(n) se puede modelar como T(n)=k2nT(n) = k \cdot 2^n para alguna constante kk.

Sabemos que para n=10n = 10:

T(10)=k210=1 ms    k=11024 msT(10) = k \cdot 2^{10} = 1 \text{ ms} \implies k = \frac{1}{1024} \text{ ms}

Para n=40n = 40:

T(40)=k240=1210240 ms=230 msT(40) = k \cdot 2^{40} = \frac{1}{2^{10}} \cdot 2^{40} \text{ ms} = 2^{30} \text{ ms}

Realizamos la conversión a unidades más comprensibles:

  • 230 ms=1.073.741.824 ms2^{30} \text{ ms} = 1.073.741.824 \text{ ms}

  • En segundos: 23010001.073.741 s\frac{2^{30}}{1000} \approx 1.073.741 \text{ s}

  • En horas: 1.073.7413600298.26 h\frac{1.073.741}{3600} \approx 298.26 \text{ h}

  • En días: 298.262412.4 dıˊas\frac{298.26}{24} \approx 12.4 \text{ días}

Por lo tanto, resolver el problema para n=40n = 40 tomará aproximadamente 12,4 días, lo cual ilustra la intratabilidad práctica de los algoritmos de complejidad exponencial.

Solution to Exercise 7
  • El lazo externo ejecuta nn iteraciones, con la variable ii tomando valores de 0 a n1n-1.

  • Para cada iteración del lazo externo, el lazo interno se ejecuta exactamente ii veces (con jj desde 0 hasta i1i-1).

  • El número total de ejecuciones del cuerpo del lazo interno se calcula mediante la sumatoria: $$\sum_{i=0}^{n-1} i = 0 + 1 + 2 + \dots + (n-1) = \frac{(n-1)n}{2} = \frac{n^2

  • n}{2}$$

  • Al descartar las constantes multiplicativas y los términos de menor orden, la complejidad temporal resultante es O(n2)O(n^2).

Solution to Exercise 8
  • Lazo externo: La variable de control ii se triplica en cada iteración (1,3,9,27,1, 3, 9, 27, \dots). El lazo finaliza cuando 3kn3^k \ge n, lo que implica que realiza k=log3nk = \lceil \log_3 n \rceil iteraciones. Su complejidad es O(logn)O(\log n).

  • Lazo interno: Para cada iteración del lazo externo, este lazo se ejecuta de forma lineal exactamente nn veces, realizando una operación elemental de tiempo constante O(1)O(1).

  • Complejidad total: Dado que los lazos están anidados de forma independiente, multiplicamos el costo de ambos:

    T(n)=log3nnO(nlogn)T(n) = \log_3 n \cdot n \in O(n \log n)
Solution to Exercise 9

Planteamos la relación de recurrencia para el tiempo de ejecución:

T(n)=2T(n/3)+f(n)T(n) = 2T(n/3) + f(n)

Donde:

  • a=2a = 2: Se realizan dos llamadas recursivas por nivel.

  • b=3b = 3: El tamaño de la entrada se divide por 3 en cada llamada.

  • f(n)=O(n)f(n) = O(n): El lazo for realiza nn iteraciones de costo constante.

Comparamos f(n)f(n) con nlogban^{\log_b a}:

nlog32n0.63n^{\log_3 2} \approx n^{0.63}

Dado que f(n)=n1f(n) = n^1 y 1>0.631 > 0.63, el trabajo no recursivo en la raíz del árbol domina la complejidad.

Verificamos la condición de regularidad: af(n/b)cf(n)a f(n/b) \le c f(n) para algún c<1c < 1.

2n3=23ncn2 \cdot \frac{n}{3} = \frac{2}{3} n \le c \cdot n

Esta desigualdad se satisface para cualquier c2/3c \ge 2/3.

Por lo tanto, aplicando el Caso 3 del Teorema Maestro, la complejidad es:

T(n)Θ(f(n))=Θ(n)T(n) \in \Theta(f(n)) = \Theta(n)
Solution to Exercise 10
  1. Versión recursiva ingenua:

    • Aunque realiza un número exponencial de llamadas en total (O(2n)O(2^n)), la pila del sistema solo almacena una rama del árbol de llamadas a la vez.

    • La profundidad máxima de la pila es nn marcos de activación. Por lo tanto, su complejidad espacial es O(n)O(n).

  2. Versión con memoización:

    • Requiere un arreglo auxiliar de tamaño n+1n + 1 para almacenar los resultados previamente computados.

    • La profundidad máxima de la pila de llamadas también es nn.

    • En consecuencia, consume O(n)O(n) de memoria para el arreglo de memoización y O(n)O(n) en la pila de ejecución, lo que totaliza una complejidad espacial de O(n)O(n).

Ambas versiones requieren espacio lineal O(n)O(n), pero la versión con memoización reduce la complejidad temporal de exponencial a lineal (O(n)O(n)) a cambio de un uso explícito de memoria.

Solution to Exercise 11
  1. Versión iterativa:

    long long factorial_iterativo(int n) {
        long long resultado = 1;
        for (int i = 2; i <= n; i++) {
            resultado *= i;
        }
        return resultado;
    }

    Esta función solo requiere almacenar las variables locales de control (resultado, i), cuyo tamaño en memoria es constante e independiente de la entrada nn. Su complejidad espacial es O(1)O(1).

  2. Versión recursiva:

    long long factorial_recursivo(int n) {
        if (n <= 1) return 1;
        return n * factorial_recursivo(n - 1);
    }

    Cada llamada recursiva introduce un nuevo marco de activación en la pila del sistema para guardar el parámetro n y la dirección de retorno. Como se realizan nn llamadas recursivas anidadas consecutivas antes de alcanzar el caso base, la pila crece linealmente. Su complejidad espacial es O(n)O(n).

Solution to Exercise 12
  • La matriz de tamaño n×nn \times n tiene un total de n2n^2 celdas. Como el espacio crece cuadráticamente respecto al tamaño de la entrada, la complejidad espacial es O(n2)O(n^2).

  • Evaluamos la viabilidad para n=100.000n = 100.000:

    Cantidad de celdas=n2=(105)2=1010 enteros\text{Cantidad de celdas} = n^2 = (10^5)^2 = 10^{10} \text{ enteros}
  • Multiplicando por el tamaño de un entero (4 bytes):

    1010×4 bytes=4×1010 bytes40 GB10^{10} \times 4 \text{ bytes} = 4 \times 10^{10} \text{ bytes} \approx 40 \text{ GB}
  • Conclusión: Esta solución es inviable en computadoras hogareñas estándar, ya que supera ampliamente la capacidad promedio de memoria RAM, provocando un desbordamiento o fallo por falta de memoria (out of memory).

Solution to Exercise 13
  • La clase P agrupa a los problemas de decisión que se pueden resolver de forma eficiente en tiempo polinomial (por ejemplo, determinar si un elemento pertenece a un arreglo).

  • La clase NP agrupa a los problemas de decisión para los cuales, dada una posible solución (certificado), se puede verificar su validez en tiempo polinomial, aunque encontrarla inicialmente pueda requerir tiempo exponencial.

  • El problema de factorización de enteros (dado un entero NN, hallar sus factores primos) es de gran interés porque:

    1. Pertenece a la clase NP (es trivial verificar si un conjunto de factores es correcto simplemente multiplicándolos en tiempo polinomial).

    2. No se conoce ningún algoritmo eficiente en computación clásica para resolverlo en tiempo polinomial.

    3. No se ha demostrado que sea NP-Completo, situándose en una categoría intermedia (NP-Intermedio) bajo la hipótesis de que PNPP \neq NP.

Solution to Exercise 14
  • Un problema es NP-Completo si cumple con dos condiciones:

    1. Pertenece a la clase NP (es verificable en tiempo polinomial).

    2. Es al menos tan difícil como cualquier otro problema en NP. Esto significa que cualquier problema en NP puede reducirse polinomialmente a él.

  • Si se encontrara un algoritmo que resolviera un único problema NP-Completo en tiempo polinomial, todos los demás problemas de la clase NP también podrían resolverse en tiempo polinomial mediante su correspondiente reducción.

  • Esto demostraría matemáticamente la igualdad P=NPP = NP, colapsando la jerarquía de complejidad. Tendría consecuencias masivas, rompiendo la seguridad de la criptografía moderna de clave pública y permitiendo optimizaciones óptimas inmediatas en logística y diseño de chips.

Solution to Exercise 15

Para verificar un recorrido propuesto (el certificado) en tiempo polinomial, se realiza el siguiente algoritmo:

  1. Certificado: La solución propuesta consiste en una secuencia ordenada de ciudades: C1,C2,,CnC_1, C_2, \dots, C_n.

  2. Validación de ciudades: Se verifica que la secuencia contenga exactamente todas las ciudades del problema sin repeticiones (a excepción del retorno a la primera ciudad). Esto toma O(n)O(n) tiempo.

  3. Cálculo de distancias: Se recorre la secuencia y se suman las distancias entre elementos consecutivos de la matriz de distancias:

    Costo=i=1n1distancia(Ci,Ci+1)+distancia(Cn,C1)\text{Costo} = \sum_{i=1}^{n-1} \text{distancia}(C_i, C_{i+1}) + \text{distancia}(C_n, C_1)

    Dado que acceder a cada celda de la matriz toma O(1)O(1) tiempo, la sumatoria toma O(n)O(n) operaciones.

  4. Comparación: Se comprueba si el CostoD\text{Costo} \leq D, lo cual toma O(1)O(1) tiempo.

Dado que todos los pasos de verificación descritos se ejecutan en tiempo lineal O(n)O(n), el problema pertenece a la clase NP.

Glosario

Complejidad Algorítmica
Medida del crecimiento de recursos (tiempo/espacio) respecto al tamaño de entrada.
Notación Big-O
Notación matemática que describe el límite superior del crecimiento de una función.
Análisis Asintótico
Método para describir el comportamiento de algoritmos cuando la entrada tiende a infinito.

Síntesis y Resumen

Resumen

El análisis de complejidad es fundamental para:

  1. Predecir rendimiento: Saber si un algoritmo será viable para el tamaño de entrada esperado

  2. Comparar algoritmos: Elegir el más eficiente para cada situación

  3. Identificar cuellos de botella: Localizar partes del código que necesitan optimización

  4. Establecer límites teóricos: Determinar si un algoritmo es óptimo o puede mejorarse

Puntos Clave
Guía Práctica de Decisión

Para elegir un algoritmo:

  1. ¿Cuál es el tamaño típico de entrada?

    • Pequeño (n<100n < 100): Casi cualquier complejidad funciona

    • Mediano (n104n \sim 10^4): Evitar O(n3)O(n^3) o peor

    • Grande (n>106n > 10^6): Necesario O(n)O(n) o O(nlogn)O(n \log n)

  2. ¿Importa más tiempo o espacio?

    • Tiempo crítico: Considera usar más memoria (memoización, tablas hash)

    • Espacio limitado: Acepta algoritmos más lentos si usan menos memoria

  3. ¿Es un problema conocido?

    • Usa algoritmos estándar óptimos cuando existan

    • Para problemas NP-completos, considera aproximaciones o heurísticas

El análisis de complejidad no reemplaza la medición empírica, pero proporciona garantías teóricas esenciales para el diseño de software robusto y escalable.

Referencias y Lecturas de Complejidad Algorítmica

Referencias y Lecturas de Complejidad Algorítmica

Textos Fundamentales
Recursos Complementarios
Recursos en Línea
References
  1. Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
  2. Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Addison-Wesley. https://algs4.cs.princeton.edu/
  3. Knuth, D. E. (1974). Structured Programming with go to Statements. ACM Computing Surveys, 6(4), 261–301. 10.1145/356635.356640
  4. Bentley, J. (1999). Programming Pearls (2nd ed.). Addison-Wesley.
  5. Bryant, R. E., & O’Hallaron, D. R. (2015). Computer Systems: A Programmer’s Perspective (3rd ed.). Pearson.