Recursividad
Una función que se llama a sí misma con un problema más pequeño hasta llegar a un caso que se resuelve directamente. La base de divide y vencerás, del backtracking y de los recorridos de árboles.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Recursividad: el árbol de llamadas
Elige n y mira cada llamada de fib(n): cómo baja hasta los casos base, cómo vuelven los resultados y cuántas veces se calcula lo mismo.
- llamada en curso
Paso 1
fib(5) no es un caso base: necesita fib(4) y fib(3). Se queda esperando y llama primero a fib(4).
1static long fib(int n) {
2 if (n <= 1) return n;
3 return fib(n - 1) + fib(n - 2); // n = 5, profundidad = 0, llamadas = 1
4}Variables
- n
- 5
- profundidad
- 0
- llamadas
- 1
- repetidas
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Una función recursiva es una función que, para resolver un problema, se llama a sí misma con una versión más pequeña del mismo problema. El factorial es el ejemplo de siempre: 5! = 5 · 4!, y 4! = 4 · 3!, y así hasta 1! = 1, que se sabe sin calcular nada.
Toda recursividad bien hecha tiene dos partes. El caso base, que se resuelve directamente y para la recursividad (sin él, la función se llamaría para siempre). Y el caso recursivo, que reduce el problema y confía en que la llamada devolverá la respuesta correcta para el problema pequeño. El truco mental es ese «salto de fe»: no hace falta seguir todas las llamadas, basta con que el caso base sea correcto y que cada paso acerque a él.
Por dentro, cada llamada ocupa un marco en la pila de llamadas con sus parámetros y variables locales. Las llamadas se apilan mientras bajan hacia el caso base y se desapilan al volver, combinando los resultados. Por eso una recursividad muy profunda (decenas de miles de niveles en Java) desborda la pila: StackOverflowError.
No es más rápida que un bucle (suele ser algo más lenta y gasta más memoria), pero hay problemas que son recursivos por naturaleza: recorrer un árbol o una estructura anidada, dividir un problema en mitades, probar todas las combinaciones. Ahí el código recursivo es mucho más corto y claro. El peligro es la recursividad que repite cálculos, como el Fibonacci ingenuo, que se arregla guardando lo calculado (memoización).
Cuándo usarlo
- Estructuras recursivas: árboles, carpetas con subcarpetas, JSON o HTML anidados, expresiones con paréntesis.
- Problemas que se definen de forma recursiva (factorial, potencia, Fibonacci, combinaciones) o que se parten en subproblemas iguales (divide y vencerás).
- Explorar todas las posibilidades: backtracking, permutaciones, laberintos.
Cuándo no
- Si un bucle sencillo lo hace igual de claro (sumar un array, contar hasta n): la recursividad solo añade llamadas y riesgo de desbordar la pila.
- Si la profundidad puede ser enorme (una lista enlazada de un millón de nodos).
- Si repite subproblemas y no se memoiza: el coste se dispara (exponencial).
Paso a paso
- Caso base. El caso más pequeño, que se resuelve sin llamarse:
n <= 1, una cadena vacía, un nodonull. Siempre se comprueba primero. - Reducir el problema. Expresar la solución usando la misma función con un problema más pequeño:
n − 1, la mitad, el resto de la cadena, un subárbol. - Confiar en la llamada. Suponer que la llamada recursiva devuelve la respuesta correcta para el problema pequeño, y combinarla para obtener la del grande.
- Comprobar que termina. Cada llamada tiene que acercarse al caso base; si alguna rama no lo hace, la recursividad no termina.
El código
Cuatro funciones recursivas
Todas siguen el mismo esquema: un caso base y un paso que reduce el problema.
1public class Main {
2 static long factorial(int n) {
3 if (n <= 1) return 1; // caso base: se resuelve sin llamarse
4 return n * factorial(n - 1); // caso recursivo: un problema más pequeño
5 }
6
7 static int sumaDigitos(int n) {
8 if (n < 10) return n; // un solo dígito
9 return n % 10 + sumaDigitos(n / 10); // el último + la suma de los demás
10 }
11
12 static long potencia(long base, int exp) {
13 if (exp == 0) return 1;
14 return base * potencia(base, exp - 1);
15 }
16
17 static String invertir(String s) {
18 if (s.length() <= 1) return s;
19 return invertir(s.substring(1)) + s.charAt(0); // el resto invertido + el primero al final
20 }
21
22 public static void main(String[] args) {
23 System.out.println("5! = " + factorial(5));
24 System.out.println("Suma de los dígitos de 4096 = " + sumaDigitos(4096));
25 System.out.println("2^10 = " + potencia(2, 10));
26 System.out.println("«recursividad» al revés: " + invertir("recursividad"));
27 }
28}def factorial(n):
if n <= 1:
return 1 # caso base: se resuelve sin llamarse
return n * factorial(n - 1) # caso recursivo: un problema más pequeño
def suma_digitos(n):
if n < 10:
return n # un solo dígito
return n % 10 + suma_digitos(n // 10) # el último + la suma de los demás
def potencia(base, exp):
if exp == 0:
return 1
return base * potencia(base, exp - 1)
def invertir(s):
if len(s) <= 1:
return s
return invertir(s[1:]) + s[0] # el resto invertido + el primero al final
print("5! =", factorial(5))
print("Suma de los dígitos de 4096 =", suma_digitos(4096))
print("2^10 =", potencia(2, 10))
print("«recursividad» al revés:", invertir("recursividad"))function factorial(n) {
if (n <= 1) return 1; // caso base: se resuelve sin llamarse
return n * factorial(n - 1); // caso recursivo: un problema más pequeño
}
function sumaDigitos(n) {
if (n < 10) return n; // un solo dígito
return (n % 10) + sumaDigitos(Math.floor(n / 10)); // el último + la suma de los demás
}
function potencia(base, exp) {
if (exp === 0) return 1;
return base * potencia(base, exp - 1);
}
function invertir(s) {
if (s.length <= 1) return s;
return invertir(s.slice(1)) + s[0]; // el resto invertido + el primero al final
}
console.log("5! = " + factorial(5));
console.log("Suma de los dígitos de 4096 = " + sumaDigitos(4096));
console.log("2^10 = " + potencia(2, 10));
console.log("«recursividad» al revés: " + invertir("recursividad"));using System;
class Program {
static long Factorial(int n) {
if (n <= 1) return 1; // caso base: se resuelve sin llamarse
return n * Factorial(n - 1); // caso recursivo: un problema más pequeño
}
static int SumaDigitos(int n) {
if (n < 10) return n; // un solo dígito
return n % 10 + SumaDigitos(n / 10); // el último + la suma de los demás
}
static long Potencia(long b, int exp) {
if (exp == 0) return 1;
return b * Potencia(b, exp - 1);
}
static string Invertir(string s) {
if (s.Length <= 1) return s;
return Invertir(s[1..]) + s[0]; // el resto invertido + el primero al final
}
static void Main() {
Console.WriteLine("5! = " + Factorial(5));
Console.WriteLine("Suma de los dígitos de 4096 = " + SumaDigitos(4096));
Console.WriteLine("2^10 = " + Potencia(2, 10));
Console.WriteLine("«recursividad» al revés: " + Invertir("recursividad"));
}
}<?php
function factorial(int $n): int {
if ($n <= 1) return 1; // caso base: se resuelve sin llamarse
return $n * factorial($n - 1); // caso recursivo: un problema más pequeño
}
function sumaDigitos(int $n): int {
if ($n < 10) return $n; // un solo dígito
return $n % 10 + sumaDigitos(intdiv($n, 10)); // el último + la suma de los demás
}
function potencia(int $base, int $exp): int {
if ($exp === 0) return 1;
return $base * potencia($base, $exp - 1);
}
function invertir(string $s): string {
if (strlen($s) <= 1) return $s;
return invertir(substr($s, 1)) . $s[0]; // el resto invertido + el primero al final
}
echo "5! = " . factorial(5) . "\n";
echo "Suma de los dígitos de 4096 = " . sumaDigitos(4096) . "\n";
echo "2^10 = " . potencia(2, 10) . "\n";
echo "«recursividad» al revés: " . invertir("recursividad") . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
5! = 120 Suma de los dígitos de 4096 = 19 2^10 = 1024 «recursividad» al revés: dadivisrucer
Fibonacci: ingenuo frente a memoización
La misma recursividad, pero guardando cada resultado: de cientos de miles de llamadas a unas pocas decenas.
1import java.util.HashMap;
2import java.util.Map;
3
4public class Main {
5 static long llamadas = 0;
6
7 static long fib(int n) { // ingenuo: recalcula lo mismo una y otra vez
8 llamadas++;
9 if (n <= 1) return n;
10 return fib(n - 1) + fib(n - 2);
11 }
12
13 static final Map<Integer, Long> memo = new HashMap<>();
14
15 static long fibMemo(int n) { // con memoización: cada fib(k) se calcula una vez
16 llamadas++;
17 if (n <= 1) return n;
18 Long guardado = memo.get(n);
19 if (guardado != null) return guardado;
20 long r = fibMemo(n - 1) + fibMemo(n - 2);
21 memo.put(n, r);
22 return r;
23 }
24
25 public static void main(String[] args) {
26 llamadas = 0;
27 long a = fib(25);
28 System.out.println("fib(25) = " + a + " con " + llamadas + " llamadas");
29 llamadas = 0;
30 long b = fibMemo(25);
31 System.out.println("fibMemo(25) = " + b + " con " + llamadas + " llamadas");
32 }
33}llamadas = 0
def fib(n): # ingenuo: recalcula lo mismo una y otra vez
global llamadas
llamadas += 1
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
memo = {}
def fib_memo(n): # con memoización: cada fib(k) se calcula una vez
global llamadas
llamadas += 1
if n <= 1:
return n
if n in memo:
return memo[n]
memo[n] = fib_memo(n - 1) + fib_memo(n - 2)
return memo[n]
llamadas = 0
a = fib(25)
print(f"fib(25) = {a} con {llamadas} llamadas")
llamadas = 0
b = fib_memo(25)
print(f"fibMemo(25) = {b} con {llamadas} llamadas")let llamadas = 0;
function fib(n) { // ingenuo: recalcula lo mismo una y otra vez
llamadas++;
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const memo = new Map();
function fibMemo(n) { // con memoización: cada fib(k) se calcula una vez
llamadas++;
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const r = fibMemo(n - 1) + fibMemo(n - 2);
memo.set(n, r);
return r;
}
llamadas = 0;
const a = fib(25);
console.log(`fib(25) = ${a} con ${llamadas} llamadas`);
llamadas = 0;
const b = fibMemo(25);
console.log(`fibMemo(25) = ${b} con ${llamadas} llamadas`);using System;
using System.Collections.Generic;
class Program {
static long llamadas = 0;
static long Fib(int n) { // ingenuo: recalcula lo mismo una y otra vez
llamadas++;
if (n <= 1) return n;
return Fib(n - 1) + Fib(n - 2);
}
static readonly Dictionary<int, long> memo = new();
static long FibMemo(int n) { // con memoización: cada fib(k) se calcula una vez
llamadas++;
if (n <= 1) return n;
if (memo.TryGetValue(n, out long guardado)) return guardado;
long r = FibMemo(n - 1) + FibMemo(n - 2);
memo[n] = r;
return r;
}
static void Main() {
llamadas = 0;
long a = Fib(25);
Console.WriteLine(quot;fib(25) = {a} con {llamadas} llamadas");
llamadas = 0;
long b = FibMemo(25);
Console.WriteLine(quot;fibMemo(25) = {b} con {llamadas} llamadas");
}
}<?php
$llamadas = 0;
function fib(int $n): int { // ingenuo: recalcula lo mismo una y otra vez
global $llamadas;
$llamadas++;
if ($n <= 1) return $n;
return fib($n - 1) + fib($n - 2);
}
$memo = [];
function fibMemo(int $n): int { // con memoización: cada fib(k) se calcula una vez
global $llamadas, $memo;
$llamadas++;
if ($n <= 1) return $n;
if (isset($memo[$n])) return $memo[$n];
return $memo[$n] = fibMemo($n - 1) + fibMemo($n - 2);
}
$llamadas = 0;
$a = fib(25);
echo "fib(25) = $a con $llamadas llamadas\n";
$llamadas = 0;
$b = fibMemo(25);
echo "fibMemo(25) = $b con $llamadas llamadas\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
fib(25) = 75025 con 242785 llamadas fibMemo(25) = 75025 con 49 llamadas
Traza: factorial(4): las llamadas bajan y los resultados suben
| Llamada | Qué hace | Devuelve |
|---|---|---|
| factorial(4) | 4 · factorial(3) | espera |
| factorial(3) | 3 · factorial(2) | espera |
| factorial(2) | 2 · factorial(1) | espera |
| factorial(1) | caso base | 1 |
| factorial(2) | 2 · 1 | 2 |
| factorial(3) | 3 · 2 | 6 |
| factorial(4) | 4 · 6 | 24 |
Las cuatro llamadas están a la vez en la pila de llamadas hasta que factorial(1) devuelve; después se resuelven en orden inverso.
Complejidad
| Función | Llamadas | Tiempo | Memoria (pila) |
|---|---|---|---|
| factorial(n) | n | O(n) | O(n) |
| potencia rápida(x, n) | log n | O(log n) | O(log n) |
| búsqueda binaria recursiva | log n | O(log n) | O(log n) |
| fib(n) ingenuo | ≈ 1,6ⁿ | O(2ⁿ) | O(n) |
| fib(n) con memoización | 2n − 1 | O(n) | O(n) |
La memoria de una recursividad es su profundidad máxima (los marcos que llegan a estar en la pila a la vez), no el número total de llamadas.
- Mejor caso: O(2ⁿ)
- Caso medio: O(2ⁿ)
- Peor caso: O(2ⁿ)
El fib recursivo ingenuo hace del orden de 2ⁿ llamadas (en realidad φⁿ ≈ 1,6ⁿ). La recursividad en sí no es lenta: lo es repetir subproblemas. Las curvas grises son las demás clases, para comparar.
En la práctica
- Recorrer carpetas y subcarpetas (
Files.walk), árboles DOM o JSON y estructuras de menús. - Los analizadores de los compiladores y de los lenguajes de consulta se escriben como funciones recursivas, una por cada regla de la gramática.
- Mergesort, quicksort y la búsqueda en árboles son recursivos.
- En programación funcional (y en Kotlin, Scala o Haskell) la recursividad sustituye a los bucles.
Errores típicos
- Olvidar el caso base o ponerlo después de la llamada recursiva: recursividad infinita y
StackOverflowError. - Que el paso recursivo no se acerque al caso base (llamar con
nen vez den − 1, o conn − 2cuando el caso base solo esn == 0y n es impar). - Ignorar lo que devuelve la llamada (
factorial(n − 1);sin usar el resultado). - Recursividad que repite subproblemas sin memoizar: el Fibonacci ingenuo con n = 50 hace más de 20.000 millones de llamadas.
- Usar recursividad para recorrer algo lineal muy largo: un bucle no tiene límite de profundidad.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Recursividad sin bucles
Completa cuatro funciones recursivas, sin usar ningún bucle: la suma de las cifras de un número, invertir un texto, comprobar si un texto es palíndromo y contar cuántas veces aparece un carácter. El main ya lee las órdenes y escribe los resultados.
- Órdenes:
digitos 4096,invertir hola,palindromo reconocer,cuenta a banana. - Salidas:
Suma de las cifras de 4096: 19,hola al revés: aloh,reconocer es palíndromo(ono es palíndromo),«a» aparece 3 veces en banana. - Otra orden:
Orden no válida: «…».
Ejemplo
digitos 4096 invertir hola palindromo reconocer cuenta a banana
Suma de las cifras de 4096: 19 hola al revés: aloh reconocer es palíndromo «a» aparece 3 veces en banana
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Suma de las cifras de n (n >= 0). */
5 static int sumaDigitos(long n) {
6 if (n < 10) return (int) n;
7 return (int) (n % 10) + sumaDigitos(n / 10);
8 }
9
10 /** El texto al revés. */
11 static String invertir(String s) {
12 if (s.length() <= 1) return s;
13 return invertir(s.substring(1)) + s.charAt(0);
14 }
15
16 /** ¿Se lee igual al derecho que al revés? Compara los extremos y sigue con lo de dentro. */
17 static boolean palindromo(String s) {
18 if (s.length() <= 1) return true;
19 if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
20 return palindromo(s.substring(1, s.length() - 1));
21 }
22
23 /** Cuántas veces aparece el carácter c en s. */
24 static int contar(char c, String s) {
25 if (s.isEmpty()) return 0;
26 return (s.charAt(0) == c ? 1 : 0) + contar(c, s.substring(1));
27 }
28
29 public static void main(String[] args) {
30 Scanner sc = new Scanner(System.in);
31 while (sc.hasNextLine()) {
32 String linea = sc.nextLine().trim();
33 if (linea.isEmpty()) continue;
34 String[] p = linea.split("\\s+");
35 if (p.length == 2 && p[0].equals("digitos") && p[1].matches("\\d{1,18}"))
36 System.out.println("Suma de las cifras de " + p[1] + ": " + sumaDigitos(Long.parseLong(p[1])));
37 else if (p.length == 2 && p[0].equals("invertir"))
38 System.out.println(p[1] + " al revés: " + invertir(p[1]));
39 else if (p.length == 2 && p[0].equals("palindromo"))
40 System.out.println(p[1] + (palindromo(p[1]) ? " es palíndromo" : " no es palíndromo"));
41 else if (p.length == 3 && p[0].equals("cuenta") && p[1].length() == 1)
42 System.out.println("«" + p[1] + "» aparece " + contar(p[1].charAt(0), p[2]) + " veces en " + p[2]);
43 else System.out.println("Orden no válida: «" + linea + "»");
44 }
45 }
46}Las cuatro siguen la misma plantilla: el caso más pequeño se responde directamente y el resto se reduce a la misma pregunta sobre un problema una posición más pequeño.
Con textos, substring crea una cadena nueva en cada llamada: está bien para aprender, pero con textos largos es más eficiente pasar índices (palindromo(s, i, j)).
2. Listas anidadas
Cada línea es una lista que puede contener números y otras listas, como [1, [2, 3], [[4]], 5]. Escríbela aplanada, la suma de todos sus números y su profundidad máxima (la lista de fuera es la profundidad 1). Hay que leerla con una función recursiva que procesa una lista y se llama a sí misma por cada lista que encuentra dentro. La lectura de números y de caracteres ya está escrita: completa leerLista.
- Entrada: una lista por línea; los espacios se ignoran.
- Salida:
[1, 2, 3, 4, 5] · suma 15 · profundidad 3; la lista vacía[]da[] · suma 0 · profundidad 1. - Si está mal escrita (falta un corchete, sobra una coma, hay algo que no es un número…):
Expresión no válida: «…».
Ejemplo
[1, [2, 3], [[4]], 5]
[1, 2, 3, 4, 5] · suma 15 · profundidad 3
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static String s; // la expresión, sin espacios
5 static int pos; // por dónde va la lectura
6 static final List<Integer> plana = new ArrayList<>();
7 static int profundidadMaxima = 0;
8
9 /** Lee un entero (con signo) desde pos. */
10 static void leerNumero() {
11 int ini = pos;
12 if (pos < s.length() && s.charAt(pos) == '-') pos++;
13 while (pos < s.length() && Character.isDigit(s.charAt(pos))) pos++;
14 if (pos == ini || s.substring(ini, pos).equals("-")) throw new IllegalArgumentException();
15 plana.add(Integer.parseInt(s.substring(ini, pos)));
16 }
17
18 /** ¿El carácter de pos es c? Si lo es, lo salta. */
19 static boolean saltar(char c) {
20 if (pos < s.length() && s.charAt(pos) == c) {
21 pos++;
22 return true;
23 }
24 return false;
25 }
26
27 /** Lee una lista que empieza en pos ('[') y está a la profundidad prof: sus elementos son números o
28 listas, separados por comas. Añade los números a plana y apunta la profundidad máxima.
29 Si la expresión está mal escrita, lanza IllegalArgumentException. */
30 static void leerLista(int prof) {
31 if (!saltar('[')) throw new IllegalArgumentException();
32 profundidadMaxima = Math.max(profundidadMaxima, prof);
33 if (saltar(']')) return; // lista vacía
34 do {
35 if (pos < s.length() && s.charAt(pos) == '[') leerLista(prof + 1);
36 else leerNumero();
37 } while (saltar(','));
38 if (!saltar(']')) throw new IllegalArgumentException();
39 }
40
41 public static void main(String[] args) {
42 Scanner sc = new Scanner(System.in);
43 while (sc.hasNextLine()) {
44 String linea = sc.nextLine();
45 if (linea.isBlank()) continue;
46 s = linea.replace(" ", "");
47 pos = 0;
48 plana.clear();
49 profundidadMaxima = 0;
50 try {
51 leerLista(1);
52 if (pos != s.length()) throw new IllegalArgumentException();
53 int suma = 0;
54 for (int x : plana) suma += x;
55 System.out.println(plana + " · suma " + suma + " · profundidad " + profundidadMaxima);
56 } catch (IllegalArgumentException e) {
57 System.out.println("Expresión no válida: «" + linea.trim() + "»");
58 }
59 }
60 }
61}La estructura del código copia la de los datos: una lista contiene elementos y un elemento puede ser una lista. Por eso la función se llama a sí misma exactamente donde aparece una lista dentro de otra. Es un analizador de descenso recursivo, como los que usan los compiladores o los lectores de JSON.
La profundidad no hay que calcularla aparte: cada llamada sabe a qué nivel está porque se lo pasa quien la llama.
Test
Test: Recursividad
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué pasa si una función recursiva no tiene caso base?
2.¿Cuánto devuelve f(4) si f(n) = n + f(n − 1) y f(0) = 0?
3.¿Cuánta memoria de pila usa factorial(n) recursivo?
4.¿Por qué el Fibonacci recursivo ingenuo es tan lento?
5.¿Qué técnica arregla una recursividad que repite subproblemas?