Ejemplo de código recursivo sin condición de salida

Echemos otro vistazo a un problema recursivo. Como ejemplo, considere calcular los números de Fibonacci. Todo el mundo recordará que la secuencia de Fibonacci es una secuencia numérica en la que los dos primeros números son 0 y 1, y cada número posterior es igual a la suma de los dos números anteriores.

Escribamos código para calcular y mostrar estos números:

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacci(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }
}

Antes de la primera llamada del método recursivo printFibonacci , imprima los dos primeros números de la secuencia: cero y uno. Necesitamos hacer esto porque el método recursivo muestra solo la suma de los parámetros de entrada, no los parámetros en sí.

El código se ve bien: obtenemos dos números, calculamos su suma, la imprimimos en la consola y volvemos a llamar recursivamente al método printFibonacci . Pasamos el número anterior (anterior) y el número actual (actual) como argumentos.

En realidad, el código tiene dos errores. Puedes verlos si ejecutas el código.

El primer error es desbordar el tipo largo . El número 104 de nuestra secuencia es negativo, lo que indica que el tipo largo se desbordó.

El segundo error es diferente. Después de calcular aproximadamente 12.000 números, obtenemos:

Excepción en el hilo "principal" java.lang.StackOverflowError

Ahora es el momento apropiado para recordar qué es una pila de llamadas a métodos en Java. La máquina Java mantiene un registro de todas las llamadas a funciones. Para hacer esto, utiliza un tipo especial de colección llamada pila. Cuando una función llama a otra, la máquina Java inserta un nuevo StackTraceElement en la pila. Cuando finaliza la función, este elemento se elimina de la pila. En consecuencia, la pila siempre almacena información actualizada sobre el estado actual de la pila de llamadas a funciones. La documentación de StackTraceElement dice que "se lanza cuando se produce un desbordamiento de pila porque una aplicación se repite demasiado". Una JVM en ejecución tiene un área especial de memoria para almacenar la pila de llamadas al método. El tamaño de esta área de memoria depende de la configuración del sistema operativo y la JVM. Además de la propia pila de llamadas al método, Las variables primitivas (valores específicos de los parámetros del método) y las direcciones de las variables de referencia (en un área de memoria llamada "montón") se almacenan en esta área de memoria especial. El acceso a la pila de llamadas sigue el principio LIFO.

Ejemplo corregido con una condición de salida.

Comencemos solucionando el segundo problema en el código.

Intentemos resolver el problema de frente: si la pila es demasiado pequeña, entonces hagámosla más grande. Para hacer esto, inicie la JVM con el indicador "-Xss" y especifique cuánta memoria asignar para la pila. Intentemos asignar 5 megabytes. Así es como se verá en IDEA:

Conseguimos aumentar la longitud de la salida. Ahora puede calcular más de 49 000 números de la secuencia en lugar de estar limitado a unos 12 000 números. Pero en algún momento, todavía obtenemos un StackOverflowError .

Puede intentar aumentar el tamaño de la pila, pero eso no soluciona el problema fundamental. Entonces, busquemos un problema en la lógica. Debe haber un punto en el que la recursividad se detiene. En otras palabras, debe haber alguna condición que determine cuándo ya no se llamará al método recursivo, lo que permitirá que la pila de llamadas se relaje. Para definir tal condición, hagamos explícito nuestro objetivo: mostrar la serie de Fibonacci siempre que sus números sean menores que Integer.MAX_VALUE .

Escribamos un nuevo método printFibonacciWithCondition que tendrá en cuenta esta condición. Y llamaremos al nuevo método corregido en el método principal.

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
//        printFibonacci(0, 1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
    }
}

Después de ejecutar el código, vemos que la salida termina con el número 1836311903. El número anterior a este es 1134903170. La suma de estos números es 2_971_215_073, que es mayor que Integer.MAX_VALUE (2_147_483_647 ) .

Este cambio solucionó automáticamente el error de desbordamiento largo . Si necesita calcular más de la serie, deberá usar otros tipos de datos, como BigInteger .

Descenso recursivo y desenrollado

Analicemos paso a paso cómo se ejecuta nuestro código. Para hacer esto, agregaremos un método echo y lo llamaremos antes y después de la llamada recursiva del método printFibonacciWithCondition .

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        echo(true, penultimate, previous);
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
        echo(false, penultimate, previous);
    }

    private static void echo(boolean isBeforeRecursiveCall, long penultimate, long previous) {
        if (isBeforeRecursiveCall) {
            System.out.printf("Before method call with args: %d, %d. Current number = ", penultimate, previous);
        } else {
            System.out.printf("After method call with args: %d, %d\n", penultimate, previous);
        }
    }
}

El programa nos da esta salida:

0
1
Antes de llamar al método con argumentos: 0, 1. Número actual = 1
Antes de llamar al método con argumentos: 1, 1. Número actual = 2
Antes de llamar al método con argumentos: 1, 2. Número actual = 3
Antes de llamar al método con argumentos: 2, 3. Número actual = 5
Antes de llamar al método con argumentos: 3, 5. Número actual = 8
Antes de llamar al método con argumentos: 5, 8. Número actual = 13
Antes de llamar al método con argumentos: 8, 13. Número actual = 21
Antes de llamar al método con argumentos: 13, 21. Número actual = 34
Antes de llamar al método con argumentos: 21, 34. Número actual = 55
Antes de llamar al método con argumentos: 34, 55. Número actual = 89
Antes de llamar al método con argumentos: 55, 89. Número actual = 144
Antes de llamar al método con argumentos: 89, 144. Número actual = 233
Antes de llamar al método con argumentos: 144, 233. Número actual = 377
Antes de llamar al método con argumentos: 233, 377. Número actual = 610
Antes de llamar al método con argumentos: 377, 610. Número actual = 987
Antes de llamar al método con argumentos: 610, 987. Número actual = 1597
Antes de llamar al método con argumentos: 987, 1597. Número actual = 2584
Antes de llamar al método con argumentos: 1597, 2584. Número actual = 4181
Antes del método llamada con argumentos: 2584, 4181. Número actual = 6765
Antes de llamar al método con argumentos: 4181, 6765. Número actual = 10946
Antes de llamar al método con argumentos: 6765, 10946. Número actual = 17711
Antes de llamar al método con argumentos: 10946, 17711. Número actual = 28657
Antes de llamar al método con argumentos: 17711, 28657. Número actual = 46368
Antes de llamar al método con argumentos: 28657, 46368. Número actual = 75025
Antes de llamar al método con argumentos: 46368, 75025. Número actual = 121393
Antes de llamar al método con argumentos: 75025, 121393. Número actual = 196418
Antes de llamar al método con argumentos: 121393, 196418. Número actual = 317811
Antes de llamar al método con argumentos: 196418, 317811. Número actual = 514229
Antes de llamar al método con argumentos: 317811, 514229. Número actual = 83204 0
Antes del método llamada con argumentos: 514229, 832040. Número actual = 1346269
Antes de llamar al método con argumentos: 832040, 1346269. Número actual = 2178309
Antes de llamar al método con argumentos: 1346269, 2178309. Número actual = 3524578
Antes de llamar al método con argumentos: 2178309, 3524578. Número actual = 5702887
Antes de llamar al método con argumentos: 3524578, 5702887. Número actual = 9227465
Antes de llamar al método con argumentos: 5702887, 9227465. Número actual = 14930352
Antes de llamar al método con argumento s: 9227465, 14930352. Número actual = 24157817
Antes de llamar al método con argumentos: 14930352, 24157817. Número actual = 39088169
Antes de llamar al método con argumentos: 24157817, 39088169. Número actual = 63245986 Antes de
llamar al método con argumentos: 39088169 , 63245986. Número actual = 102334155
Antes del método llamada con argumentos: 63245986, 102334155. Número actual = 165580141
Antes de llamar al método con argumentos: 102334155, 165580141. Número actual = 267914296
Antes de llamar al método con argumentos: 165580141, 267914296. Número actual = 433494437
Antes de llamar al método con argumentos: 267914296, 433494437. Número actual = 701408733 Antes de llamar al método con argumentos: 433494437, 701408733. Número actual = 1134903170
Antes de
llamar al método con argumentos: 701408733, 1134903170. Número actual = 1836311903
Después de la llamada al método con argumentos: 701408733, 113490317
Después de la llamada al método con argumentos: 433494437, 701408733 Después de la llamada al método con argumentos: 267914296, 433494437
Después de la
llamada al método con ar gs: 165580141, 267914296
Después de llamar al método con argumentos: 102334155, 165580141
Después de llamar al método con argumentos: 63245986, 102334155
Después de llamar al método con argumentos: 39088169, 63245986
Después de la llamada al método con argumentos: 24157817, 39088169
Después de la llamada al método con argumentos: 14930352, 24157817
Después de la llamada al método con argumentos: 9227465, 14930352 Después de la llamada al método con argumentos: 5702887, 9227465
Después de la
llamada al método con argumentos: 352 4578, 5702887
Después de llamar al método con argumentos : 2178309, 3524578
Después de llamar al método con argumentos: 1346269, 2178309
Después de llamar al método con argumentos: 832040, 1346269 Después de
llamar al método con argumentos: 514229, 832040 Después de llamar al método con argumentos: 317811
, 514229 Después
de llamar al método con argumentos: 196418, 317811
después llamada al método con argumentos: 121393, 196418
Después de la llamada al método con argumentos: 75025, 121393
Después de la llamada al método con argumentos: 46368, 75025
Después de la llamada al método con argumentos: 28657, 46368
Después de la llamada al método con argumentos: 17711, 28657
Después de la llamada al método con argumentos: 10946
, 17711 Después de la llamada al método con argumentos: 6765, 10946
Después de la llamada al método con argumentos: 4181, 6765
Después de la llamada al método con argumentos : 2584, 4181
Después de la llamada al método con argumentos: 1597, 2584
Después de la llamada al método con argumentos: 987, 1597
Después de la llamada al método con argumentos: 610, 987
Después de la llamada al método con argumentos: 377, 610
Después de la llamada al método con argumentos: 233, 377
Después llamada al método con argumentos: 144, 233
Después de la llamada al método con argumentos: 89, 144
Después de la llamada al método con argumentos: 55, 89
Después de la llamada al método con argumentos: 34, 55
Después de la llamada al método con argumentos: 21, 34
Después de la llamada al método con argumentos: 13, 21
Después de la llamada al método con argumentos: 8, 13
Después de la llamada al método con argumentos: 5, 8
Después de la llamada al método con argumentos: 3, 5
Después de la llamada al método con argumentos: 2, 3
Después de la llamada al método con argumentos : 1, 2
Después de llamar al método con argumentos: 1, 1
Después de llamar al método con argumentos: 0, 1

Aquí hay una visualización de lo que está sucediendo.

Digámoslo de nuevo: se llama al método printFibonacciWithCondition . Calcula el número actual. Si nos conviene, lo mostramos y llamamos al método printFibonacciWithCondition nuevamente con nuevos argumentos.

Siempre que se llame al método recursivo, esto se denomina "descenso recursivo". Cuando finaliza recursivamente y regresan las llamadas a métodos, decimos que la pila de llamadas se está "desenrollando".

La recursividad es un tema interesante en la programación. Para manejar mejor el material, cambiemos ligeramente nuestra tarea. La nueva tarea es generar, en orden descendente, todos los números de la serie de Fibonacci que no excedan Integer.MAX_VALUE . Ya hemos escrito todo el código necesario para esta tarea. Todo lo que queda es intercambiar el orden de mostrar el número actual y llamar al método recursivo. Es decir, en el primer ejemplo, el número calculado se mostró durante el "descenso", pero ahora necesitamos "descender hasta el fondo" y luego mostrar los números "en el camino de regreso". Y, por supuesto, en el método principal , intercambiamos los dos números iniciales de la secuencia (cero y uno) después de mostrarlos después de llamar al método recursivo. Para la legibilidad,método.

public class Fibonacci {
    public static void main(String[] args) {
        printFibonacciWithCondition(0, 1);
        System.out.println(1);
        System.out.println(0);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        printFibonacciWithCondition(previous, current);
        System.out.println(current);
    }
}

La salida será:

1836311903
1134903170
701408733
433494437
267914296
165580141
102334155
63245986
39088169
24157817
14930352
9227465
5702887
3524578
2178309
1346269
832040
514229
317811
196418
121393
75025
46368
28657
17711
10946
6765
4181
2584
1597
987
610
377
233
144
89
55
34
21
13
8
5
3
2
1
1
0