C

C

Tutorial C: Recursividad

Aprende recursividad en C con ejemplos claros de funciones, casos base y cálculo del factorial. Domina esta técnica esencial para resolver problemas complejos.

Aprende C y certifícate

Función que se llama a sí misma

La recursividad es una técnica de programación donde una función se llama a sí misma para resolver un problema. Esta técnica es especialmente útil cuando podemos dividir un problema complejo en versiones más pequeñas del mismo problema.

Imagina que tienes una función llamada calcular() que dentro de su código incluye una llamada a calcular(). Esto puede parecer extraño al principio, pero es una herramienta muy poderosa en programación.

Estructura básica de una función recursiva

Una función recursiva en C tiene la siguiente estructura:

tipo_retorno nombre_funcion(parametros) {
    // Verificación del caso base
    if (condicion_de_terminacion) {
        return valor_final;
    }
    
    // Llamada recursiva
    return nombre_funcion(parametros_modificados);
}

Los elementos clave de esta estructura son:

  • La función se define de manera normal
  • Dentro de la función, existe una condición de terminación (caso base)
  • La función se llama a sí misma con parámetros diferentes

Ejemplo simple de recursividad

Veamos un ejemplo sencillo: una función que cuenta regresivamente desde un número hasta cero:

#include <stdio.h>

void cuenta_regresiva(int numero) {
    // Mostramos el número actual
    printf("%d\n", numero);
    
    // Verificamos si hemos llegado al final
    if (numero <= 0) {
        // Caso base: terminamos la recursión
        return;
    }
    
    // Llamada recursiva con un valor menor
    cuenta_regresiva(numero - 1);
}

int main() {
    cuenta_regresiva(5);
    return 0;
}

Este programa imprimirá:

5
4
3
2
1
0

Visualizando la recursividad

Para entender mejor cómo funciona la recursividad, podemos visualizar las llamadas a la función cuenta_regresiva(5):

  1. cuenta_regresiva(5) imprime 5 y llama a cuenta_regresiva(4)
  2. cuenta_regresiva(4) imprime 4 y llama a cuenta_regresiva(3)
  3. cuenta_regresiva(3) imprime 3 y llama a cuenta_regresiva(2)
  4. cuenta_regresiva(2) imprime 2 y llama a cuenta_regresiva(1)
  5. cuenta_regresiva(1) imprime 1 y llama a cuenta_regresiva(0)
  6. cuenta_regresiva(0) imprime 0 y termina (caso base)

Cada llamada a la función crea un nuevo contexto de ejecución que se almacena en la pila de llamadas (stack). Cuando se alcanza el caso base, las funciones se completan en orden inverso.

Otro ejemplo: Potencia de un número

Veamos otro ejemplo donde calculamos la potencia de un número usando recursividad:

#include <stdio.h>

int potencia(int base, int exponente) {
    // Caso base: cualquier número elevado a 0 es 1
    if (exponente == 0) {
        return 1;
    }
    
    // Llamada recursiva: base * base^(exponente-1)
    return base * potencia(base, exponente - 1);
}

int main() {
    int resultado = potencia(2, 4);  // Calcula 2^4 = 16
    printf("2^4 = %d\n", resultado);
    return 0;
}

En este ejemplo:

  • Si el exponente es 0, devolvemos 1 (caso base)
  • De lo contrario, multiplicamos la base por el resultado de elevar la misma base a (exponente - 1)

Consideraciones importantes

Al trabajar con funciones recursivas, debes tener en cuenta:

  • Cada llamada recursiva consume memoria en la pila de llamadas
  • Si la recursión es muy profunda, puede ocurrir un desbordamiento de pila (stack overflow)
  • Siempre debe existir un caso base que detenga la recursión
  • La recursión debe acercarse al caso base en cada llamada

Ventajas de la recursividad

  • Hace que el código sea más claro para ciertos problemas
  • Permite expresar algoritmos complejos de forma elegante
  • Es ideal para estructuras de datos jerárquicas (árboles, grafos)
  • Refleja directamente la definición matemática de muchos problemas

La recursividad es una herramienta fundamental en programación que te permitirá resolver problemas complejos de manera elegante. Aunque al principio puede parecer confusa, con práctica se vuelve una técnica natural y poderosa en tu arsenal de programación.

Caso base para detener recursión

El caso base es el componente más crítico de cualquier función recursiva. Sin él, la recursión continuaría indefinidamente, provocando un desbordamiento de pila (stack overflow) que haría que nuestro programa fallara. Podemos entender el caso base como la condición que indica cuándo debe detenerse el proceso recursivo.

Cuando diseñamos una solución recursiva, siempre debemos preguntarnos: "¿Cuál es el problema más simple que puedo resolver directamente, sin más recursión?". La respuesta a esta pregunta será nuestro caso base.

Características de un buen caso base

Un caso base efectivo debe cumplir estas características:

  • Ser simple y resolverse sin llamadas recursivas adicionales
  • Detener la cadena de llamadas recursivas
  • Ser alcanzable eventualmente desde cualquier llamada inicial
  • Devolver un valor concreto que permita construir la solución completa

Identificando el caso base

Para identificar correctamente el caso base, debemos analizar el problema y encontrar su versión más simple. Algunos ejemplos comunes:

  • En problemas numéricos descendentes: cuando llegamos a 0 o 1
  • En recorridos de cadenas o arrays: cuando llegamos al final o al principio
  • En estructuras de árbol: cuando llegamos a una hoja (nodo sin hijos)

Ejemplo práctico: Suma de los primeros n números

Veamos cómo implementar una función recursiva que sume todos los números desde 1 hasta n:

#include <stdio.h>

int suma_hasta_n(int n) {
    // Caso base: la suma hasta 0 es 0
    if (n == 0) {
        return 0;
    }
    
    // Llamada recursiva: n + suma de los números hasta n-1
    return n + suma_hasta_n(n - 1);
}

int main() {
    int resultado = suma_hasta_n(5);  // Calcula 1+2+3+4+5 = 15
    printf("La suma de los números del 1 al 5 es: %d\n", resultado);
    return 0;
}

En este ejemplo:

  • El caso base es cuando n == 0, donde devolvemos 0
  • Para cualquier otro valor, sumamos n al resultado de sumar todos los números hasta n-1

Múltiples casos base

Algunos problemas requieren más de un caso base. Por ejemplo, en la secuencia de Fibonacci:

#include <stdio.h>

int fibonacci(int n) {
    // Casos base: los dos primeros números de Fibonacci son 0 y 1
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    
    // Llamada recursiva: Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    printf("Fibonacci(6) = %d\n", fibonacci(6));  // Resultado: 8
    return 0;
}

Aquí tenemos dos casos base:

  • Cuando n == 0, devolvemos 0
  • Cuando n == 1, devolvemos 1

Errores comunes al definir casos base

Algunos errores frecuentes que debes evitar:

  • Olvidar el caso base: Esto causa un bucle infinito y un desbordamiento de pila
  • Caso base inalcanzable: Si la condición nunca se cumple, la recursión no se detendrá
  • Caso base incorrecto: Puede llevar a resultados erróneos

Verificación de parámetros

Es una buena práctica verificar que los parámetros de entrada sean válidos antes de proceder con la recursión:

#include <stdio.h>

int factorial(int n) {
    // Verificación de parámetros
    if (n < 0) {
        printf("Error: No existe factorial de números negativos\n");
        return -1;  // Indicar error
    }
    
    // Caso base: factorial de 0 es 1
    if (n == 0) {
        return 1;
    }
    
    // Llamada recursiva
    return n * factorial(n - 1);
}

int main() {
    printf("Factorial de 4: %d\n", factorial(4));
    printf("Factorial de -3: %d\n", factorial(-3));
    return 0;
}

En este ejemplo, verificamos que n no sea negativo antes de proceder con la recursión.

Optimización del caso base

A veces, podemos optimizar nuestra función añadiendo casos base adicionales que resuelvan directamente problemas pequeños:

#include <stdio.h>

int fibonacci_optimizado(int n) {
    // Casos base originales
    if (n == 0) return 0;
    if (n == 1) return 1;
    
    // Casos base adicionales para optimización
    if (n == 2) return 1;
    if (n == 3) return 2;
    
    // Llamada recursiva para casos más complejos
    return fibonacci_optimizado(n - 1) + fibonacci_optimizado(n - 2);
}

Estos casos base adicionales pueden mejorar el rendimiento al reducir el número total de llamadas recursivas necesarias.

Progresión hacia el caso base

Es fundamental que cada llamada recursiva nos acerque al caso base. Esto generalmente implica:

  • Reducir el tamaño del problema en cada llamada
  • Modificar los parámetros para aproximarnos a la condición del caso base
  • Asegurar que eventualmente alcanzaremos el caso base

Si la recursión no progresa hacia el caso base, tendremos un bucle infinito que eventualmente causará un error en nuestro programa.

Ejemplo factorial

El factorial de un número es un ejemplo perfecto para ilustrar la recursividad en acción. Matemáticamente, el factorial de un número entero positivo n (representado como n!) se define como el producto de todos los enteros positivos desde 1 hasta n.

Por ejemplo:

  • 3! = 3 × 2 × 1 = 6
  • 4! = 4 × 3 × 2 × 1 = 24
  • 5! = 5 × 4 × 3 × 2 × 1 = 120

Esta operación tiene una propiedad interesante que la hace ideal para la recursividad: podemos expresar el factorial de n en términos del factorial de n-1:

n! = n × (n-1)!

Además, sabemos que 0! = 1 (por definición matemática), lo que nos proporciona un perfecto caso base.

Implementación recursiva del factorial

Veamos cómo implementar una función recursiva para calcular el factorial:

#include <stdio.h>

int factorial(int n) {
    // Caso base: factorial de 0 es 1
    if (n == 0) {
        return 1;
    }
    
    // Llamada recursiva: n! = n * (n-1)!
    return n * factorial(n - 1);
}

int main() {
    int numero = 5;
    printf("El factorial de %d es: %d\n", numero, factorial(numero));
    return 0;
}

Este programa calculará e imprimirá:

El factorial de 5 es: 120

Seguimiento de la ejecución

Para entender mejor cómo funciona la recursividad en este ejemplo, veamos paso a paso qué ocurre cuando llamamos a factorial(5):

  1. factorial(5) verifica si n == 0. Como no es cierto, ejecuta return 5 * factorial(4)
  2. factorial(4) verifica si n == 0. Como no es cierto, ejecuta return 4 * factorial(3)
  3. factorial(3) verifica si n == 0. Como no es cierto, ejecuta return 3 * factorial(2)
  4. factorial(2) verifica si n == 0. Como no es cierto, ejecuta return 2 * factorial(1)
  5. factorial(1) verifica si n == 0. Como no es cierto, ejecuta return 1 * factorial(0)
  6. factorial(0) verifica si n == 0. Como es cierto, retorna 1 (caso base)

Ahora, las llamadas se resuelven en orden inverso:

  1. factorial(0) devuelve 1
  2. factorial(1) devuelve 1 * 1 = 1
  3. factorial(2) devuelve 2 * 1 = 2
  4. factorial(3) devuelve 3 * 2 = 6
  5. factorial(4) devuelve 4 * 6 = 24
  6. factorial(5) devuelve 5 * 24 = 120

El resultado final es 120, que es el factorial de 5.

Visualización de la pila de llamadas

Cuando ejecutamos una función recursiva, cada llamada se almacena en la pila de llamadas (stack). Podemos visualizarlo así para el cálculo de factorial(5):

Pila de llamadas:
[factorial(0)]
[factorial(1)]
[factorial(2)]
[factorial(3)]
[factorial(4)]
[factorial(5)] <- Inicio

A medida que se resuelve cada llamada, se va "desapilando":

Pila de llamadas después de resolver factorial(0):
[factorial(1)] <- factorial(0) devolvió 1
[factorial(2)]
[factorial(3)]
[factorial(4)]
[factorial(5)]

Y así sucesivamente hasta que la pila queda vacía y tenemos nuestro resultado final.

Mejorando la implementación

Nuestra implementación básica funciona bien, pero podemos mejorarla añadiendo algunas verificaciones:

#include <stdio.h>

int factorial(int n) {
    // Verificación de entrada
    if (n < 0) {
        printf("Error: No se puede calcular el factorial de un número negativo\n");
        return -1;  // Indicar error
    }
    
    // Caso base: factorial de 0 es 1
    if (n == 0) {
        return 1;
    }
    
    // Llamada recursiva
    return n * factorial(n - 1);
}

int main() {
    int numeros[] = {5, 0, -3};
    int i;
    
    for (i = 0; i < 3; i++) {
        int resultado = factorial(numeros[i]);
        if (resultado >= 0) {
            printf("El factorial de %d es: %d\n", numeros[i], resultado);
        }
    }
    
    return 0;
}

Este programa manejará correctamente los casos especiales, como intentar calcular el factorial de un número negativo.

Limitaciones prácticas

Es importante tener en cuenta que el factorial crece muy rápidamente. El tipo int en C tiene un límite, y el factorial de números relativamente pequeños puede excederlo:

  • 12! = 479,001,600 (cerca del límite de un int de 32 bits)
  • 13! = 6,227,020,800 (excede el límite de un int de 32 bits)

Para valores más grandes, podríamos usar long long int o implementar una solución con aritmética de precisión arbitraria.

#include <stdio.h>

long long int factorial_grande(int n) {
    if (n < 0) {
        printf("Error: No se puede calcular el factorial de un número negativo\n");
        return -1;
    }
    
    if (n == 0) {
        return 1;
    }
    
    return n * factorial_grande(n - 1);
}

int main() {
    int numero = 13;
    printf("El factorial de %d es: %lld\n", numero, factorial_grande(numero));
    return 0;
}

Comparación con implementación iterativa

El factorial también puede calcularse de forma iterativa (usando bucles). Comparemos ambas implementaciones:

#include <stdio.h>

// Versión recursiva
int factorial_recursivo(int n) {
    if (n == 0) {
        return 1;
    }
    return n * factorial_recursivo(n - 1);
}

// Versión iterativa
int factorial_iterativo(int n) {
    int resultado = 1;
    int i;
    
    for (i = 1; i <= n; i++) {
        resultado *= i;
    }
    
    return resultado;
}

int main() {
    int numero = 5;
    
    printf("Factorial recursivo de %d: %d\n", numero, factorial_recursivo(numero));
    printf("Factorial iterativo de %d: %d\n", numero, factorial_iterativo(numero));
    
    return 0;
}

Ambas implementaciones producen el mismo resultado, pero tienen características diferentes:

  • La versión recursiva es más elegante y refleja directamente la definición matemática
  • La versión iterativa es más eficiente en términos de uso de memoria, ya que no necesita almacenar múltiples llamadas en la pila

El factorial es un ejemplo clásico que demuestra cómo la recursividad puede expresar de manera clara y concisa ciertos algoritmos, especialmente aquellos que tienen una definición recursiva natural en matemáticas.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende C online

Todas las lecciones de C

Accede a todas las lecciones de C y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a C y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de recursividad y su estructura básica en funciones.
  • Identificar y definir correctamente el caso base para detener la recursión.
  • Implementar funciones recursivas para problemas comunes como cuenta regresiva, suma, potencia, Fibonacci y factorial.
  • Analizar la gestión de la pila de llamadas y las limitaciones de la recursividad.
  • Comparar implementaciones recursivas e iterativas y entender sus ventajas y desventajas.