Saltar al contenido
Topos Uranos

Resumen

Una función puede llamarse a sí misma: la recursión resuelve un problema reduciéndolo a una instancia menor del mismo problema. Esta clase traduce a C++ definiciones recursivas de la matemática (el factorial, la potencia, la suma de cifras), cada una con su caso base y su caso recursivo; muestra que la inducción garantiza su corrección y que una medida que decrece garantiza su terminación; sigue con la pila de llamadas los marcos que se apilan y se deshacen, y explica cuándo la pila se agota. Revisa el límite del tipo del resultado, que desborda mucho antes que la pila; compara la versión recursiva y la iterativa de Fibonacci contando llamadas, y cierra con un criterio para elegir entre la recursión y un bucle.

Objetivos de aprendizaje

  1. Escribir una función recursiva, con su caso base y su caso recursivo, a partir de una definición recursiva.
  2. Justificar la corrección y la terminación de una función recursiva por inducción sobre el tamaño del problema.
  3. Seguir con la pila de llamadas la ejecución de una función recursiva y explicar el desbordamiento de la pila.
  4. Comparar el costo de la versión recursiva y la iterativa de Fibonacci contando sus llamadas y sus operaciones.

Definiciones recursivas

El factorial

El factorial de un natural nn es el producto de los naturales de 1 a nn: n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n, con la convención 0!=10! = 1. Los puntos suspensivos de esa fórmula esconden un bucle, y la clase Bucles enseñó a escribirlo. Ahora bien, la misma definición admite otra forma, que no menciona ningún bucle: el factorial de nn es nn veces el factorial de n−1n - 1, y el de 0 es 1.

0!=1,n!=n⋅(n−1)!si n≥1.0! = 1, \qquad n! = n \cdot (n - 1)! \quad \text{si } n \geq 1.

Esta es una definición recursiva: define el factorial de nn por medio del factorial de un número menor. No es circular, porque cada aplicación de la segunda regla baja un escalón y la escalera termina en la primera: 3!=3⋅2!=3⋅2⋅1!=3⋅2⋅1⋅0!=63! = 3 \cdot 2! = 3 \cdot 2 \cdot 1! = 3 \cdot 2 \cdot 1 \cdot 0! = 6. Las dos reglas tienen nombre. La primera es el caso base, que da el resultado sin referirse a nada más; la segunda es el caso recursivo, que reduce el problema a una instancia menor del mismo problema.

C++ permite que una función se llame a sí misma, y con ello la definición se traduce casi palabra por palabra.

EjemploEl factorial recursivo
Programa en C++
#include <print>​// Factorial de n.// Precondición: 0 <= n <= 12 (la cota se justifica más adelante).// Poscondición: devuelve n! = 1 · 2 · ... · n, con 0! = 1.int factorial(int n){    if (n == 0) {        return 1;                   // caso base    }    return n * factorial(n - 1);    // caso recursivo}​int main(){    for (int n{0}; n <= 6; ++n) {        std::println("{}! = {}", n, factorial(n));    }}
Salida
0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720

La función tiene exactamente la forma de la definición: un if que atiende el caso base y un return que aplica el caso recursivo. La llamada factorial(n - 1) no es distinta de cualquier otra llamada de la clase Funciones: se evalúa su argumento, se crea un marco nuevo con su propio n y, al terminar, el valor devuelto se multiplica por el n de quien llamó.

El contrato no es decorativo: con n=−1n = -1 el caso recursivo llamaría con −2-2, después con −3-3, y nunca alcanzaría el 0; y 13!13! no cabe en un int. Muchos textos introductorios escriben este factorial con el caso base if (n == 0 | n == 1)|, redundante pero inofensivo, y sin decir nada de los negativos ni del tamaño del resultado: su código no termina con n<0n < 0 y da un resultado sin sentido desde 13!13!. Ambos defectos se examinarán en su sección.

Caso base y caso recursivo

Toda función recursiva tiene uno o varios casos base, que se resuelven directamente, y uno o varios casos recursivos, que llaman a la misma función con un argumento menor en algún sentido preciso y combinan lo que devuelve.

La potencia xnx^n, con nn natural, se define por x0=1x^0 = 1 y xn=x⋅xn−1x^n = x \cdot x^{n - 1}: el tamaño es el exponente. La suma de las cifras de un natural nn (la de 2026 es 2+0+2+6=102 + 0 + 2 + 6 = 10) se define así: si n<10n < 10, la suma es nn, porque tiene una sola cifra; si no, es la suma de las cifras de ⌊n/10⌋\lfloor n / 10 \rfloor, que es nn sin su última cifra, más esa última cifra, n mod 10n \bmod 10. Aquí el tamaño es el número de cifras.

EjemploLa potencia y la suma de cifras
Programa en C++
#include <print>​// x elevado a n. Precondición: n >= 0.double power(double x, int n){    if (n == 0) {        return 1.0;    }    return x * power(x, n - 1);}​// Suma de las cifras decimales de n. Precondición: n >= 0.int digitSum(int n){    if (n < 10) {        return n;    }    return digitSum(n / 10) + n % 10;}​int main(){    std::println("2^10 = {}", power(2.0, 10));    std::println("1.5^3 = {}", power(1.5, 3));    std::println("suma de las cifras de 2026: {}", digitSum(2026));}
Salida
2^10 = 10241.5^3 = 3.375suma de las cifras de 2026: 10

En digitSum, la división entera n / 10 quita la última cifra y n % 10 la extrae, como en el cambio de base de la clase Algoritmos numéricos. Para n=2026n = 2026, la función pide la suma de las cifras de 202, que pide la de 20, que pide la de 2; esta es un caso base y devuelve 2, y los resultados se componen al volver: 2+0=22 + 0 = 2, 2+2=42 + 2 = 4, 4+6=104 + 6 = 10.

Escribir una función recursiva consiste, por tanto, en responder dos preguntas: qué casos se resuelven sin ayuda, y cómo se obtiene la solución de un caso a partir de la de uno menor. Si ambas respuestas son correctas, la función lo es, y la sección siguiente lo demuestra.