Una función recursiva es aquella que se llama a sí misma (directa o indirectamente) para resolver un problema dividiéndolo en subproblemas más pequeños del mismo tipo. Cada llamada reduce el problema hasta llegar a una condición de parada (caso base) que se resuelve sin más recursión. Cuando cada llamada termina, la pila de ejecuciones se va desapilando y los resultados parciales se combinan.
Anatomía de la recursión
- Caso base
– Condición que detiene la recursión.
– Debe alcanzarse siempre, o la función se ejecutará indefinidamente. - Paso recursivo
– La función se invoca con una entrada más pequeña o “más cercana” al caso base.
– El problema se descompone y se delega la parte restante a la misma función. - Combinación de resultados
– Las llamadas van devolviendo valores que se combinan (sumas, concatenaciones, etc.) hasta resolver el problema original.
Ejemplo clásico: factorial
public static long Factorial(int n)
{
if (n < 0) throw new ArgumentException("n debe ser >= 0");
if (n <= 1) // 1) caso base
return 1;
return n * Factorial(n - 1); // 2) paso recursivo
}
- Base:
n <= 1
devuelve1
. - Paso recursivo: multiplica
n
porFactorial(n-1)
.
Comparación con iteración
Recursión | Iteración | |
---|---|---|
Legibilidad | Expresa la lógica de forma más cercana a la definición matemática. | Suele ser más verboso. |
Uso de memoria | Cada llamada ocupa una posición en la pila (overhead). | Un único marco de pila; menor riesgo de desbordamiento. |
Complejidad | Puede simplificar algoritmos complejos (árboles, grafos). | Más adecuada para bucles simples y cálculos grandes. |
Use recursión cuando la estructura del problema sea naturalmente recursiva (árboles, división y conquista) y sea más clara que la versión iterativa.
Más ejemplos prácticos
1. Serie de Fibonacci (recursión ingenua)
public static long Fibonacci(int n)
{
if (n < 0) throw new ArgumentException();
if (n <= 1) return n; // base
return Fibonacci(n - 1) + Fibonacci(n - 2); // recursivo
}
⚠ O(n²) y muchas llamadas repetidas. Conviene memorizar o iterar si
n
es grande.
2. Recorrido de un directorio
public static void ImprimirArchivos(string ruta)
{
foreach (var archivo in Directory.GetFiles(ruta))
Console.WriteLine(archivo);
foreach (var subdir in Directory.GetDirectories(ruta))
ImprimirArchivos(subdir); // profundiza un nivel
}
3. Búsqueda binaria recursiva
public static int Buscar(int[] datos, int objetivo, int ini, int fin)
{
if (ini > fin) return -1; // no encontrado
int medio = (ini + fin) / 2;
if (datos[medio] == objetivo) return medio;
if (objetivo < datos[medio])
return Buscar(datos, objetivo, ini, medio - 1);
else
return Buscar(datos, objetivo, medio + 1, fin);
}
Recursión de cola (tail recursion)
En otros lenguajes, los compiladores pueden optimizar la recursión de cola eliminando marcos de pila intermedios. En C# todavía no hay TCO (Tail Call Optimization) garantizada, así que no confíes en ella para evitar desbordamientos; en su lugar:
- Reescribe en forma iterativa si
n
puede ser muy grande. - O bien usa un Stack manual u otras estructuras.
Buenas prácticas al usar recursión en C#
- Define un caso base claro y alcanzable.
- Controla la profundidad máxima.
StackOverflowException
no se puede capturar. - Evita cálculos duplicados. Usa memoization o una versión iterativa si el problema tiene subproblemas superpuestos (p. ej., Fibonacci).
- Prefiere parámetros inmutables o copias; evita efectos laterales inesperados.
- Documenta la función: indica complejidad temporal y espacial.
- Unit tests: cubre casos límite (0, 1, máximos).
Errores comunes
- Olvidar el caso base ⇒ recursión infinita.
- Modificar variables globales dentro de cada llamada ⇒ produce resultados imprevistos.
- No validar argumentos negativos o nulos antes de recursar.
- Suponer optimización de cola ⇒ puede fallar en producción bajo carga.
Conclusión
Las funciones recursivas son potentes para expresar algoritmos que se descomponen naturalmente en subproblemas (por ejemplo, estructuras jerárquicas, búsquedas divide-y-vencerás). En C#, la recursión se implementa de forma sencilla, pero es crucial manejar de forma segura la pila y evitar cálculos redundantes. Con un caso base sólido, un paso recursivo correcto y pruebas adecuadas, la recursión puede hacer tu código más claro, elegante y fácil de mantener.
Contenido restringido
Comments are closed