La recursión es una técnica de programación donde una función se llama a sí misma para resolver un problema dividiéndolo en subproblemas más pequeños y similares. En Python, la recursión es una herramienta elegante y poderosa, especialmente útil para problemas que pueden descomponerse naturalmente en versiones más pequeñas de sí mismos.
Si estás comenzando, es importante dominar primero los fundamentos de las funciones en Python, ya que la recursión depende directamente del concepto de funciones y sus parámetros. En esta guía completa entenderás cómo funciona la recursión, cuándo usarla y cómo evitar trampas comunes como el desbordamiento de pila.
¿Qué es la Recursión?
La recursión ocurre cuando una función define su comportamiento en términos de sí misma. Imagina dos espejos enfrentados — la imagen se repite infinitamente a menos que haya una condición de parada. En computación, toda función recursiva necesita dos elementos fundamentales:
- Caso base: la condición que detiene la recursión
- Caso recursivo: la llamada de la función para una versión más pequeña del problema
Un ejemplo clásico y sencillo es el cálculo del factorial de un número. El factorial de n (representado como n!) es el producto de todos los enteros positivos menores o iguales a n. Matemáticamente, n! = n × (n-1)!, con 0! = 1. Esta definición ya es naturalmente recursiva.
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
Cuando llamamos a factorial(5), la función se llama sucesivamente con factorial(4), factorial(3), factorial(2), factorial(1) — que retorna 1 (caso base). Entonces cada llamada retorna multiplicando el resultado, formando: 1 × 2 × 3 × 4 × 5 = 120.
Cómo Funciona la Pila de Llamadas
Cada llamada recursiva se apila en la call stack (pila de llamadas) de Python. Con cada nueva llamada, un nuevo marco (frame) se añade al tope de la pila. Cuando se alcanza el caso base, los marcos comienzan a desapilarse del tope hacia abajo, retornando los resultados.
Visualizando la ejecución de factorial(5):
factorial(1) → retorna 1
factorial(2) → 2 * 1 = 2
factorial(3) → 3 * 2 = 6
factorial(4) → 4 * 6 = 24
factorial(5) → 5 * 24 = 120
Cada nivel de la pila consume memoria. Para recursiones muy profundas, Python lanza un RecursionError cuando se excede el límite de recursión. Por defecto, este límite es de 1000 llamadas, pero puede ajustarse con sys.setrecursionlimit().
import sys
print(sys.getrecursionlimit()) # 1000
sys.setrecursionlimit(3000) # aumentar a 3000
Según la documentación oficial de Python, modificar el límite de recursión debe hacerse con precaución, ya que valores muy altos pueden causar un crash del intérprete debido al desbordamiento de la pila C.
Caso Base y Caso Recursivo
Todo problema resuelto con recursión debe tener al menos un caso base bien definido. Sin él, la función se llamaría infinitamente hasta causar un desbordamiento de pila. El caso base es la instancia más pequeña del problema, que puede resolverse directamente sin recurrir a la recursión.
def cuenta_regresiva(n):
if n == 0: # caso base
print("¡Fuego!")
return
print(n) # acción
cuenta_regresiva(n - 1) # caso recursivo
cuenta_regresiva(5)
En el ejemplo anterior, cuando n llega a 0, la recursión se detiene. Cada llamada reduce n en 1, garantizando que el caso base se alcanzará eventualmente, siempre que n sea un número no negativo.
Fibonacci con Recursión
La secuencia de Fibonacci es uno de los ejemplos más usados para enseñar recursión. Cada término es la suma de los dos anteriores, con F(0) = 0 y F(1) = 1.
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
Aunque elegante, esta implementación tiene complejidad exponencial O(2ⁿ). Para n = 40, realiza millones de llamadas recursivas. Es un ejemplo clásico de recursión ingenua ineficiente. Para mejorarlo, podemos usar memoización:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci_optimizado(n):
if n <= 1:
return n
return fibonacci_optimizado(n - 1) + fibonacci_optimizado(n - 2)
print(fibonacci_optimizado(100)) # 354224848179261915075
El decorador @lru_cache almacena resultados ya calculados, reduciendo la complejidad a O(n). Real Python tiene un excelente tutorial detallado sobre recursión y memoización en Python.
Recursión vs Iteración
Toda función recursiva puede reescribirse como una función iterativa (usando bucles), y viceversa. La elección entre un enfoque y otro depende de varios factores:
| Característica | Recursión | Iteración |
|---|---|---|
| Legibilidad | Más intuitiva para problemas naturalmente recursivos | Puede ser más difícil de entender en problemas complejos |
| Memoria | Consume más memoria (pila de llamadas) | Consume menos memoria |
| Rendimiento | Puede ser más lenta (overhead de llamadas de función) | Generalmente más rápida |
| Riesgo de error | RecursionError si es muy profunda | Bucle infinito si está mal escrita |
Para problemas como recorrer árboles, procesar estructuras anidadas o implementar algoritmos de dividir y conquistar, la recursión ofrece una solución más natural y legible. Según GeeksforGeeks, la recursión es particularmente útil para problemas que pueden definirse en términos de sí mismos.
Recursión de Cola (Tail Recursion)
La recursión de cola es una forma especial de recursión donde la llamada recursiva es la última operación ejecutada por la función. En lenguajes funcionales, esto permite optimizaciones donde el compilador reutiliza el mismo marco de pila, evitando el crecimiento de la call stack. Desafortunadamente, Python no implementa optimización de recursión de cola.
# Recursión de cola (no optimizada en Python)
def factorial_cola(n, acumulador=1):
if n <= 1:
return acumulador
return factorial_cola(n - 1, n * acumulador)
El creador de Python, Guido van Rossum, explicó que la optimización de recursión de cola no se implementó porque perjudicaría la pila de depuración (stack traces menos informativos). Por lo tanto, en Python, prefiere iteración para problemas que requerirían recursión muy profunda. Programiz tiene ejemplos prácticos que ilustran bien esta limitación.
Límites de la Recursión en Python
Python define un límite máximo de profundidad para la recursión, evitando que un programa consuma toda la memoria disponible. Este límite se controla con sys.getrecursionlimit().
import sys
Verificando el límite actual
print(f"Límite actual: {sys.getrecursionlimit()}")
Aumentando el límite (¡con cuidado!)
sys.setrecursionlimit(5000)
Verificando si un valor puede causar desbordamiento
def verifica_profundidad(n):
try:
return verifica_profundidad(n + 1)
except RecursionError:
return n
print(f"Profundidad máxima alcanzable: {verifica_profundidad(0)}")
La FAQ oficial de Python aborda preguntas relacionadas con parámetros de funciones y recursión, explicando los matices del comportamiento del lenguaje.
Problemas Prácticos con Recursión
Búsqueda Binaria Recursiva
La búsqueda binaria es un algoritmo de dividir y conquistar que encuentra un elemento en una lista ordenada en tiempo O(log n). Su implementación recursiva es elegante y directa:
def busqueda_binaria(lista, objetivo, izquierda, derecha):
if izquierda > derecha:
return -1 # caso base: elemento no encontrado
medio = (izquierda + derecha) // 2
if lista[medio] == objetivo:
return medio # caso base: elemento encontrado
elif lista[medio] > objetivo:
return busqueda_binaria(lista, objetivo, izquierda, medio - 1)
else:
return busqueda_binaria(lista, objetivo, medio + 1, derecha)
numeros = [1, 3, 5, 7, 9, 11, 13, 15]
print(busqueda_binaria(numeros, 7, 0, len(numeros) - 1)) # 3
Merge Sort Recursivo
Merge Sort es otro algoritmo clásico de dividir y conquistar que usa recursión para ordenar elementos con complejidad O(n log n):
def merge_sort(lista):
if len(lista) <= 1:
return lista # caso base
medio = len(lista) // 2
izquierda = merge_sort(lista[:medio])
derecha = merge_sort(lista[medio:])
return merge(izquierda, derecha)
def merge(izquierda, derecha):
resultado = []
i = j = 0
while i < len(izquierda) and j < len(derecha):
if izquierda[i] <= derecha[j]:
resultado.append(izquierda[i])
i += 1
else:
resultado.append(derecha[j])
j += 1
resultado.extend(izquierda[i:])
resultado.extend(derecha[j:])
return resultado
print(merge_sort([38, 27, 43, 3, 9, 82, 10])) # [3, 9, 10, 27, 38, 43, 82]
FreeCodeCamp tiene un tutorial completo sobre recursión en Python con más ejemplos prácticos como estos.
Torre de Hanoi
La Torre de Hanoi es un rompecabezas matemático que se vuelve casi trivial con recursión, pero complejo de resolver iterativamente:
def torre_hanoi(n, origen, destino, auxiliar):
if n == 1:
print(f"Mover disco 1 de {origen} a {destino}")
return
torre_hanoi(n - 1, origen, auxiliar, destino)
print(f"Mover disco {n} de {origen} a {destino}")
torre_hanoi(n - 1, auxiliar, destino, origen)
torre_hanoi(3, "A", "C", "B")
Para 3 discos, el algoritmo ejecuta 7 movimientos. Para n discos, se requieren 2ⁿ - 1 movimientos. La solución recursiva refleja directamente la definición matemática del problema, mostrando el poder de la recursión.
Cuándo Usar Recursión en Python
La recursión es más adecuada cuando:
- El problema puede descomponerse naturalmente en subproblemas más pequeños y similares
- La profundidad de la recursión es predecible y limitada (decenas, no miles de niveles)
- La versión iterativa sería significativamente más compleja
- Estás procesando estructuras de datos recursivas (árboles, grafos, JSON anidado)
Wikipedia ofrece una visión completa de la recursión en ciencias de la computación, incluyendo sus aplicaciones en algoritmos y estructuras de datos.
Evita la recursión cuando:
- La profundidad puede ser grande e impredecible
- El rendimiento es crítico y el overhead de llamadas de función importa
- Existe una solución iterativa simple
Buenas Prácticas con Recursión
- Siempre define un caso base claro — sin él, tu recursión nunca termina
- Garantiza que el caso base se alcanzará — cada llamada recursiva debe acercarse al caso base
- Usa memoización —
@lru_cacheo@cache(Python 3.9+) para evitar recálculos - Prefiere iteración para problemas profundos — más de 500 niveles de recursión pueden ser riesgosos
- Documenta la lógica recursiva — la recursión puede ser difícil de leer sin comentarios adecuados
- Prueba con valores pequeños primero — verifica el caso base, luego aumenta gradualmente
- Considera alternativas — para recorrido de árboles, los iteradores pueden ser más eficientes
Depuración de Recursión
Depurar funciones recursivas puede ser un desafío. Aquí hay algunas técnicas útiles:
import sys
from functools import wraps
def debug_recursion(func):
"""Decorador que muestra la profundidad de recursión"""
nivel = [0]
@wraps(func)
def wrapper(*args, **kwargs):
indent = " " * nivel[0]
print(f"{indent}→ {func.__name__}({args})")
nivel[0] += 1
try:
resultado = func(*args, **kwargs)
print(f"{indent}← {func.__name__}({args}) = {resultado}")
return resultado
finally:
nivel[0] -= 1
return wrapper
@debug_recursion
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
factorial(4)
W3Schools ofrece una introducción simple y directa a la recursión en Python, ideal para quienes están comenzando.
Conclusión
La recursión es una herramienta indispensable en el arsenal de cualquier programador Python. Aunque no es la solución ideal para todos los problemas, dominar la recursión permite escribir código más elegante y expresivo para una amplia gama de problemas, especialmente aquellos que involucran estructuras de datos recursivas y algoritmos de dividir y conquistar.
Recuerda que la práctica lleva a la perfección. Comienza con problemas simples como factorial y Fibonacci, luego avanza a algoritmos más complejos como búsqueda binaria, Merge Sort y Torre de Hanoi. Con el tiempo, desarrollarás la intuición necesaria para identificar cuándo la recursión es la herramienta correcta para el trabajo.
Para continuar aprendiendo, explora otros temas fundamentales como las listas en Python, que se usan frecuentemente junto con algoritmos recursivos, y profundiza en estructuras de datos como árboles y grafos, donde la recursión brilla. El tutorial oficial de Python es un recurso indispensable para cualquier desarrollador.