Recursividad en Java
Nivel 7 · 50 min

Memoizacion, recursion de cola y iteracion

Y la verdad incomoda: Java no optimiza la recursion de cola.

Memoizacion

Si tu recursion resuelve el mismo subproblema mas de una vez, guarda el resultado la primera vez y devuelvelo desde ahi. Eso es todo. Se llama memoizacion, y convierte a Fibonacci de exponencial a lineal con tres lineas.

static Map<Integer, Long> memo = new HashMap<>();

static long fib(int n) {
    if (n <= 1) return n;
    if (memo.containsKey(n)) return memo.get(n);   // ya lo calcule

    long r = fib(n - 1) + fib(n - 2);
    memo.put(n, r);                                 // lo guardo
    return r;
}

Esto es programacion dinamica top-down. La version bottom-up con un arreglo y un bucle hace lo mismo sin usar la pila.

Recursion de cola

Una llamada es de cola cuando es lo ultimo que hace el metodo, sin ninguna operacion pendiente despues. En return n * fact(n-1) queda pendiente la multiplicacion, asi que NO es de cola. En return fact(n-1, acc*n) no queda nada pendiente: si lo es.

En Scala, Kotlin o Scheme el compilador convierte eso en un bucle y el consumo de pila se vuelve constante.

// NO es de cola: falta multiplicar al volver
static long fact(int n) {
    return n <= 1 ? 1 : n * fact(n - 1);
}

// SI es de cola: el acumulador lleva el resultado
static long fact(int n, long acc) {
    return n <= 1 ? acc : fact(n - 1, acc * n);
}

Java no la optimiza. Punto.

Esto hay que decirlo claro porque muchos libros lo dan por hecho: la JVM no elimina las llamadas de cola. El segundo metodo del ejemplo tambien revienta la pila, exactamente igual que el primero.

La razon es historica y de diseno: Java necesita la pila completa para los rastros de excepcion y para el modelo de seguridad basado en el llamador. Escribir recursion de cola en Java es un buen habito de estilo y de portabilidad a otros lenguajes, pero no te salva del StackOverflowError.

Si necesitas profundidad grande de verdad, no hay atajo: pasas a iteracion.

De recursion a iteracion

Toda recursion se puede convertir en un bucle. Si es recursion simple, casi siempre sale un for directo. Si es multiple, tienes que llevar tu propia pila con un Deque, que es exactamente lo que hacia la JVM por ti.

// Recorrido preorden sin recursion
static void preorden(Nodo raiz) {
    if (raiz == null) return;
    Deque<Nodo> pila = new ArrayDeque<>();
    pila.push(raiz);

    while (!pila.isEmpty()) {
        Nodo n = pila.pop();
        visitar(n);
        if (n.der != null) pila.push(n.der);   // derecha primero
        if (n.izq != null) pila.push(n.izq);   // para que salga izquierda
    }
}

Ejecuta y observa

Cambia los parametros, dale a Reproducir, y usa las flechas para avanzar paso a paso. La pila crece hacia arriba: el marco de arriba es el que se esta ejecutando ahora mismo.

El arbol de llamadas

Sube n de a uno y mira como crece el arbol. Cuando llegues a 10, activa la memoizacion y compara el contador de llamadas.

Errores que vas a cometer

Lee el codigo, decide que esta mal, y despues abre la correccion.

Creer que la recursion de cola arregla el StackOverflowError en Java

static long suma(int n, long acc) {
    if (n == 0) return acc;
    return suma(n - 1, acc + n);   // es de cola...
}
// suma(1_000_000, 0)  ->  StackOverflowError

Memoizar con la llave equivocada

// funcion de dos parametros, memo de uno solo
static Map<Integer, Integer> memo = new HashMap<>();
static int f(int i, int j) {
    if (memo.containsKey(i)) return memo.get(i);
    ...
}

Comprueba lo que entendiste

Necesitas el 70% para dar el nivel por visto. Responde sin devolverte a la teoria: si fallas, la explicacion te dice exactamente donde estaba el hueco.