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
- Escribir una función recursiva, con su caso base y su caso recursivo, a partir de una definición recursiva.
- Justificar la corrección y la terminación de una función recursiva por inducción sobre el tamaño del problema.
- Seguir con la pila de llamadas la ejecución de una función recursiva y explicar el desbordamiento de la pila.
- 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 es el producto de los naturales de 1 a : , con la convención . 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 es veces el factorial de , y el de 0 es 1.
Esta es una definición recursiva: define el factorial de 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: . 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.
#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)); }}0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720La 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 el caso recursivo llamaría con , después con , y nunca alcanzaría el 0; y 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 y da un resultado sin sentido desde . 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 , con natural, se define por y : el tamaño es el exponente. La suma de las cifras de un natural (la de 2026 es ) se define así: si , la suma es , porque tiene una sola cifra; si no, es la suma de las cifras de , que es sin su última cifra, más esa última cifra, . Aquí el tamaño es el número de cifras.
#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));}2^10 = 10241.5^3 = 3.375suma de las cifras de 2026: 10En 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 , 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: , , .
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.
Cargando el contenido…