Recursiónc++recursióncomplejidadanálisisalgoritmos

Complejidad de Algoritmos Recursivos

Aprende a analizar la complejidad temporal y espacial de funciones recursivas

OOI Oaxaca9 de febrero de 20266 min read

Análisis de recursión

Analizar la complejidad de funciones recursivas requiere:

  1. Identificar la relación de recurrencia
  2. Resolver la recurrencia o aplicar el teorema maestro

Recurrencias comunes

Recursión lineal

void f(int n) {
    if (n == 0) return;
    // O(1) trabajo
    f(n - 1);  // Una llamada
}

Recurrencia: T(n)=T(n1)+O(1)T(n) = T(n-1) + O(1)

Complejidad: O(n)O(n)

Recursión con trabajo lineal

void f(int n) {
    if (n == 0) return;
    for (int i = 0; i < n; i++) {  // O(n) trabajo
        // ...
    }
    f(n - 1);
}

Recurrencia: T(n)=T(n1)+O(n)T(n) = T(n-1) + O(n)

Complejidad: O(n2)O(n^2)

División por 2 (Búsqueda binaria)

int buscar(vector<int>& arr, int x, int l, int r) {
    if (l > r) return -1;
    int mid = (l + r) / 2;  // O(1) trabajo
    if (arr[mid] == x) return mid;
    if (arr[mid] < x) return buscar(arr, x, mid+1, r);
    return buscar(arr, x, l, mid-1);  // Una llamada
}

Recurrencia: T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)

Complejidad: O(logn)O(\log n)

Divide y vencerás (Merge Sort)

void mergeSort(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) / 2;
    mergeSort(l, mid);       // T(n/2)
    mergeSort(mid + 1, r);   // T(n/2)
    merge(l, mid, r);        // O(n) trabajo
}

Recurrencia: T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)

Complejidad: O(nlogn)O(n \log n)

El Teorema Maestro

Para recurrencias de la forma: T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)

Donde:

  • aa = número de subproblemas
  • bb = factor de reducción
  • f(n)f(n) = trabajo fuera de la recursión

Casos del teorema

Sea c=logbac = \log_b a:

  1. Si f(n)=O(ncϵ)f(n) = O(n^{c-\epsilon}): T(n)=Θ(nc)T(n) = \Theta(n^c)
  2. Si f(n)=Θ(nc)f(n) = \Theta(n^c): T(n)=Θ(nclogn)T(n) = \Theta(n^c \log n)
  3. Si f(n)=Ω(nc+ϵ)f(n) = \Omega(n^{c+\epsilon}): T(n)=Θ(f(n))T(n) = \Theta(f(n))

Ejemplos de aplicación

Recurrenciaabf(n)ncn^cCasoComplejidad
T(n)=T(n/2)+1T(n) = T(n/2) + 112112O(logn)O(\log n)
T(n)=2T(n/2)+nT(n) = 2T(n/2) + n22nn2O(nlogn)O(n \log n)
T(n)=2T(n/2)+1T(n) = 2T(n/2) + 1221n1O(n)O(n)
T(n)=4T(n/2)+nT(n) = 4T(n/2) + n42nn2n^21O(n2)O(n^2)
T(n)=4T(n/2)+n2T(n) = 4T(n/2) + n^242n2n^2n2n^22O(n2logn)O(n^2 \log n)

Fibonacci: un análisis detallado

Sin memorización

int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}

Recurrencia: T(n)=T(n1)+T(n2)+O(1)T(n) = T(n-1) + T(n-2) + O(1)

Complejidad: O(ϕn)O(1.618n)O(\phi^n) \approx O(1.618^n) - ¡Exponencial!

Cota superior: O(2n)O(2^n)

Con memorización

int memo[N];
int fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    return memo[n] = fib(n-1) + fib(n-2);
}

Complejidad: O(n)O(n) - Cada estado se calcula exactamente una vez.

Complejidad espacial

La recursión usa espacio en la pila de llamadas:

void f(int n) {
    if (n == 0) return;
    f(n - 1);  // Llamada pendiente
}

Espacio: O(n)O(n) - profundidad máxima de la pila.

Recursión de cola (optimizable)

// Puede optimizarse a O(1) espacio
int factorialCola(int n, int acc = 1) {
    if (n <= 1) return acc;
    return factorialCola(n - 1, n * acc);
}

Divide y vencerás

void mergeSort(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) / 2;
    mergeSort(l, mid);
    mergeSort(mid + 1, r);
    merge(l, mid, r);
}

Espacio en pila: O(logn)O(\log n) - profundidad del árbol de recursión.

Análisis de árboles de recursión

Método del árbol

Para T(n)=2T(n/2)+nT(n) = 2T(n/2) + n:

Nivel 0:       n                    -> n
Nivel 1:    n/2   n/2               -> n
Nivel 2:  n/4 n/4 n/4 n/4           -> n
...
Nivel k: ...                        -> n

Profundidad: log₂(n)
Total: n × log₂(n) = O(n log n)

Ejemplo: Potencia rápida

long long pot(long long a, long long n) {
    if (n == 0) return 1;
    long long medio = pot(a, n/2);  // Una llamada
    return (n % 2 == 0) ? medio * medio : medio * medio * a;
}

Análisis:

  • Una llamada recursiva
  • Problema se reduce a la mitad
  • Trabajo O(1) por nivel

Complejidad: O(logn)O(\log n)

Resumen de complejidades

PatrónRecurrenciaComplejidad
Lineal simpleT(n)=T(n1)+O(1)T(n) = T(n-1) + O(1)O(n)O(n)
Lineal con trabajo linealT(n)=T(n1)+O(n)T(n) = T(n-1) + O(n)O(n2)O(n^2)
Búsqueda binariaT(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)O(logn)O(\log n)
Merge sortT(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n)
Fibonacci ingenuoT(n)=T(n1)+T(n2)T(n) = T(n-1) + T(n-2)O(2n)O(2^n)
Árbol binarioT(n)=2T(n1)+O(1)T(n) = 2T(n-1) + O(1)O(2n)O(2^n)

Consejos para competencias

  1. Recursión lineal: Generalmente O(n)O(n) o O(n2)O(n^2)
  2. División por 2: Generalmente O(logn)O(\log n)
  3. Dos llamadas con n-1: Probablemente exponencial
  4. Memorización: Reduce a O(número de estados)

Límites prácticos

Complejidadn máximo aproximado
O(n!)O(n!)10
O(2n)O(2^n)20-25
O(n3)O(n^3)500
O(n2)O(n^2)5,000
O(nlogn)O(n \log n)1,000,000
O(n)O(n)10,000,000
O(logn)O(\log n)10^18

Ejercicios de análisis

Ejercicio 1

¿Cuál es la complejidad?

void f(int n) {
    if (n <= 1) return;
    f(n - 1);
    f(n - 1);
}
Ver respuesta

T(n)=2T(n1)+O(1)T(n) = 2T(n-1) + O(1)

Complejidad: O(2n)O(2^n)

Ejercicio 2

¿Cuál es la complejidad?

void f(int n) {
    if (n <= 1) return;
    for (int i = 0; i < n; i++) {
        cout << i << " ";
    }
    f(n / 2);
}
Ver respuesta

T(n)=T(n/2)+O(n)T(n) = T(n/2) + O(n)

Por teorema maestro: a=1a=1, b=2b=2, f(n)=nf(n)=n

nc=n0=1n^c = n^0 = 1, como f(n)=n=Ω(n0+ϵ)f(n) = n = \Omega(n^{0+\epsilon}), caso 3.

Complejidad: O(n)O(n)

Siguiente paso

Aplica estos conocimientos en Algoritmos de Ordenamiento.