Recursiónc++complejidadrecursiónanálisis

Complejidad de la Recursión

Aprende a analizar la complejidad temporal y espacial de funciones recursivas

OOI Oaxaca9 de febrero de 20264 min read

¿Por qué analizar la recursión?

Cuando escribes una función recursiva, necesitas saber: ¿cuánto tiempo tarda? ¿cuánta memoria usa? Esto es especialmente importante en competencias, donde hay límites estrictos de tiempo y memoria.

Método 1: Árbol de recursión

Dibuja todas las llamadas como un árbol y cuenta:

Factorial: O(n)O(n)

factorial(5)
└── factorial(4)
    └── factorial(3)
        └── factorial(2)
            └── factorial(1)
                └── factorial(0)  ← caso base

Es una cadena lineal de n+1n+1 llamadas. Cada llamada hace O(1)O(1) trabajo. Total: O(n)O(n).

Fibonacci (sin memo): O(2n)O(2^n)

fib(5)
├── fib(4)
│   ├── fib(3)
│   │   ├── fib(2) ──...
│   │   └── fib(1)
│   └── fib(2) ──...
└── fib(3)
    ├── fib(2) ──...
    └── fib(1)

Es un árbol binario de profundidad nn. Tiene hasta 2n2^n nodos. Total: O(2n)O(2^n).

Fibonacci (con memo): O(n)O(n)

Cada valor de 0 a n se calcula exactamente una vez. Total: O(n)O(n).

Método 2: Relación de recurrencia

Expresa el tiempo como una fórmula recursiva y resuélvela:

Ejemplo: Búsqueda binaria

int busqueda(vector<int> &v, int lo, int hi, int x) {
    if (lo > hi) return -1;
    int mid = (lo + hi) / 2;
    if (v[mid] == x) return mid;
    if (v[mid] < x) return busqueda(v, mid+1, hi, x);
    return busqueda(v, lo, mid-1, x);
}

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

En cada llamada, el problema se reduce a la mitad. Después de kk pasos, n/2k=1n / 2^k = 1, así que k=log2nk = \log_2 n.

Total: O(logn)O(\log n).

Ejemplo: Merge Sort

void mergeSort(vector<int> &v, int lo, int hi) {
    if (lo >= hi) return;
    int mid = (lo + hi) / 2;
    mergeSort(v, lo, mid);      // Primera mitad
    mergeSort(v, mid+1, hi);    // Segunda mitad
    merge(v, lo, mid, hi);      // Combinar: O(n)
}

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

Dos subproblemas de tamaño n/2n/2, más O(n)O(n) para combinar. Total: O(nlogn)O(n \log n).

Recurrencias comunes

RecurrenciaComplejidadEjemplo
T(n)=T(n1)+O(1)T(n) = T(n-1) + O(1)O(n)O(n)Factorial, búsqueda lineal recursiva
T(n)=T(n1)+O(n)T(n) = T(n-1) + O(n)O(n2)O(n^2)Selection sort recursivo
T(n)=2T(n1)+O(1)T(n) = 2T(n-1) + O(1)O(2n)O(2^n)Fibonacci, torres de Hanoi
T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)O(logn)O(\log n)Búsqueda binaria
T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n)Merge sort
T(n)=2T(n/2)+O(1)T(n) = 2T(n/2) + O(1)O(n)O(n)Recorrer árbol binario

Complejidad espacial

La recursión usa espacio en la pila de llamadas. La profundidad máxima de la pila = la profundidad del árbol de recursión.

FunciónProfundidad de recursiónEspacio
FactorialnnO(n)O(n)
Fibonacci (sin memo)nnO(n)O(n)
Búsqueda binarialogn\log nO(logn)O(\log n)
Merge sortlogn\log nO(n)O(n) (por el arreglo auxiliar)

Consejos prácticos

  1. Si la recursión es lineal (una sola llamada recursiva por nivel), la complejidad suele ser simple de analizar.

  2. Si hay dos llamadas recursivas (como Fibonacci), la complejidad crece exponencialmente sin memorización.

  3. Con memorización, la complejidad es: (número de estados) × (trabajo por estado).

  4. En competencias, si n20n \leq 20, puedes usar O(2n)O(2^n). Si n106n \leq 10^6, necesitas O(n)O(n) o O(nlogn)O(n \log n).

Ejercicio de práctica

Analiza la complejidad de esta función. ¿Cuántas veces se imprime "hola"?

void misterio(int n) {
    if (n <= 0) return;
    cout << "hola" << endl;
    misterio(n / 3);
    misterio(n / 3);
}
Ver solución

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

Usando el teorema maestro con a=2a=2, b=3b=3, c=0c=0: como log320.63>0\log_3 2 \approx 0.63 > 0, la complejidad es O(nlog32)O(n0.63)O(n^{\log_3 2}) \approx O(n^{0.63}).

En términos simples: crece más rápido que O(logn)O(\log n) pero mucho más lento que O(n)O(n).

Siguiente paso

Aprende sobre Pilas y Colas, estructuras de datos fundamentales que tienen conexión directa con la recursión.