Exemplo de código recursivo sem condição de saída

Vamos dar outra olhada em um problema recursivo. Como exemplo, considere o cálculo dos números de Fibonacci. Todos devem se lembrar que a sequência de Fibonacci é uma sequência numérica na qual os dois primeiros números são 0 e 1, e cada número subseqüente é igual à soma dos dois números anteriores.

Vamos escrever um código para calcular e exibir esses 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 da primeira chamada do método printFibonacci recursivo , imprima os dois primeiros números da sequência: zero e um. Precisamos fazer isso porque o método recursivo exibe apenas a soma dos parâmetros de entrada, não os próprios parâmetros.

O código parece bom: obtemos dois números, calculamos sua soma, imprimimos no console e chamamos o método printFibonacci recursivamente novamente. Passamos o número anterior (anterior) e o número atual (atual) como argumentos.

Na verdade, o código tem dois erros. Você pode vê-los se executar o código.

O primeiro erro está transbordando o tipo longo . O 104º número em nossa sequência é negativo, indicando que o tipo long estourou.

O segundo erro é diferente. Depois de calcular cerca de 12.000 números, obtemos:

Exceção no thread "principal" java.lang.StackOverflowError

Agora é um momento apropriado para relembrar o que é uma pilha de chamadas de método em Java. A máquina Java mantém um registro de todas as chamadas de função. Para fazer isso, ele usa um tipo especial de coleção chamado pilha. Quando uma função chama outra, a máquina Java coloca um novo StackTraceElement na pilha. Quando a função termina, esse elemento é removido da pilha. Da mesma forma, a pilha sempre armazena informações atualizadas sobre o estado atual da pilha de chamada de função. A documentação do StackTraceElement diz que ele é "lançado quando ocorre um estouro de pilha porque um aplicativo recursa muito profundamente". Uma JVM em execução possui uma área especial de memória para armazenar a pilha de chamadas de método. O tamanho dessa área de memória depende das configurações do sistema operacional e da JVM. Além da própria pilha de chamadas de método, as variáveis ​​primitivas (valores específicos dos parâmetros do método) e os endereços das variáveis ​​de referência (em uma área de memória chamada "heap") são armazenados nesta área de memória especial. O acesso à pilha de chamadas segue o princípio LIFO.

Exemplo corrigido com uma condição de saída

Vamos começar corrigindo o segundo problema no código.

Vamos tentar resolver o problema de frente: se a pilha for muito pequena, vamos aumentá-la. Para fazer isso, inicie a JVM com o sinalizador "-Xss" e especifique quanta memória alocar para a pilha. Vamos tentar alocar 5 megabytes. É assim que ficará no IDEA:

Conseguimos aumentar o comprimento da saída. Agora pode calcular mais de 49.000 números da sequência, em vez de se limitar a cerca de 12.000 números. Mas, em algum momento, ainda obtemos um StackOverflowError .

Você pode tentar aumentar o tamanho da pilha, mas isso não resolve o problema fundamental. Então, vamos procurar um problema de lógica. Deve haver um ponto em que a recursão para. Em outras palavras, deve haver alguma condição que determine quando o método recursivo não será mais chamado, permitindo que a pilha de chamadas se desenrole. Para definir tal condição, vamos deixar nosso objetivo explícito: exibir a série de Fibonacci desde que seus números sejam menores que Integer.MAX_VALUE .

Vamos escrever um novo método printFibonacciWithCondition que levará em consideração essa condição. E chamaremos o novo método corrigido no 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);
    }
}

Depois de executar o código, vemos que a saída termina com o número 1836311903. O número anterior a este é 1134903170. A soma desses números é 2_971_215_073, que é realmente maior que Integer.MAX_VALUE (2_147_483_647 ) .

Essa alteração corrigiu automaticamente o bug de estouro longo . Se você precisar calcular mais da série, precisará usar outros tipos de dados, como BigInteger .

Descida recursiva e desenrolamento

Vamos analisar passo a passo como nosso código é executado. Para fazer isso, vamos adicionar um método echo e chamá-lo antes e depois da chamada recursiva do 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);
        }
    }
}

O programa nos dá esta saída:

0
1
Antes da chamada do método com args: 0, 1. Número atual = 1
Antes da chamada do método com args: 1, 1. Número atual = 2
Antes da chamada do método com args: 1, 2. Número atual = 3
Antes da chamada do método com args: 2, 3. Número atual = 5
Antes da chamada do método com argumentos: 3, 5. Número atual = 8
Antes da chamada do método com argumentos: 5, 8. Número atual = 13
Antes da chamada do método com argumentos: 8, 13. Número atual = 21
Antes da chamada do método com argumentos: 13, 21. Número atual = 34
Antes da chamada do método com argumentos: 21, 34. Número atual = 55
Antes da chamada do método com argumentos: 34, 55. Número atual = 89
Antes da chamada do método com argumentos: 55, 89. Número atual = 144
Antes da chamada do método com argumentos: 89, 144. Número atual = 233
Antes da chamada do método com argumentos: 144, 233. Número atual = 377
Antes da chamada do método com argumentos: 233, 377. Número atual = 610
Antes da chamada do método com argumentos: 377, 610. Número atual = 987
Antes da chamada do método com argumentos: 610, 987. Número atual = 1597
Antes da chamada do método com argumentos: 987, 1597. Número atual = 2584
Antes da chamada do método com argumentos: 1597, 2584. Número atual = 4181
Antes do método call with args: 2584, 4181. Current number = 6765
Before method call with args: 4181, 6765. Current number = 10946
Before method call with args: 6765, 10946. Current number = 17711
Before method call with args: 10946, 17711. Número atual = 28657
Antes da chamada do método com argumentos: 17711, 28657. Número atual = 46368
Antes da chamada do método com argumentos: 28657, 46368. Número atual = 75025
Antes da chamada do método com argumentos: 46368, 75025. Número atual = 121393
Antes da chamada do método com argumentos: 75025, 121393. Número atual = 196418
Antes da chamada do método com argumentos: 121393, 196418. Número atual = 317811
Antes da chamada do método com argumentos: 196418, 317811. Número atual = 514229
Antes da chamada do método com argumentos: 317811, 514229. Número atual = 832040
Antes do método chamada com argumentos: 514229, 832040. Número atual = 1346269
Antes da chamada do método com argumentos: 832040, 1346269. Número atual = 2178309
Antes da chamada do método com argumentos: 1346269, 2178309. Número atual = 3524578
Antes da chamada do método com argumentos: 2178309, 3524578. Número atual = 5702887
Antes da chamada do método com argumentos: 3524578, 5702887. Número atual = 9227465
Antes da chamada do método com argumentos: 5702887, 9227465. Número atual = 14930352
Antes da chamada do método com argumentos: 92274 65, 14930352. Número atual = 24157817
Antes da chamada do método com argumentos: 14930352, 24157817. Número atual = 39088169
Antes da chamada do método com argumentos: 24157817, 39088169. Número atual = 63245986 Antes da
chamada do método com argumentos: 39088169, 6324 5986. Número atual = 102334155
Antes do método chamada com argumentos: 63245986, 102334155. Número atual = 165580141
Antes da chamada do método com argumentos: 102334155, 165580141. Número atual = 267914296
Antes da chamada do método com argumentos: 165580141, 267914296. Número atual = 433494437
Antes da chamada do método com argumentos: 267914296, 433494437. Número atual = 701408733 Antes da chamada do método com argumentos: 433494437, 701408733. Número atual =
11349 03170
Antes da chamada do método com args: 701408733, 1134903170. Número atual = 1836311903
Após chamada de método com argumentos: 701408733, 113490317
Após chamada de método com argumentos: 433494437, 701408733 Após chamada de método com argumentos: 267914296,
433494437
Após chamada de método com argumentos: 1655 80141, 267914296
Após chamada de método com args: 102334155, 165580141
Após chamada de método com argumentos: 63245986, 102334155
Após chamada de método com argumentos: 39088169, 63245986
Após chamada de método com argumentos: 24157817, 39088169
Após chamada de método com argumentos: 14930352, 24157817
Após chamada de método com argumentos: 9227465, 14930352 Após chamada de método com argumentos: 5702887, 9227465
Após
chamada de método com argumentos: 3524578, 5702 887
Após chamada de método com args : 2178309, 3524578
Após chamada de método com argumentos: 1346269, 2178309
Após chamada de método com argumentos: 832040, 1346269
Após chamada de método com argumentos: 514229, 832040 Após chamada de método com argumentos: 317811
, 514229
Após chamada de método com argumentos: 19641 8, 317811
Depois chamada de método com argumentos: 121393, 196418
Após chamada de método com argumentos: 75025, 121393
Após chamada de método com argumentos: 46368, 75025
Após chamada de método com argumentos: 28657, 46368
Após chamada de método com argumentos: 17711, 28657
Após chamada de método com argumentos: 10946
, 17711 Após chamada de método com argumentos: 6765, 10946
Após chamada de método com argumentos: 4181, 6765
Após chamada de método com argumentos : 2584, 4181
Após chamada de método com argumentos: 1597, 2584
Após chamada de método com argumentos: 987, 1597 Após chamada de método
com argumentos: 610, 987
Após chamada de método com argumentos: 377, 610
Após chamada de método com argumentos: 233, 377
Após chamada de método com argumentos: 144, 233
Após chamada de método com argumentos: 89, 144
Após chamada de método com argumentos: 55, 89
Após chamada de método com argumentos: 34, 55
Após chamada de método com argumentos: 21, 34
Após chamada de método com argumentos: 13, 21
Após chamada de método com argumentos: 8, 13
Após chamada de método com argumentos: 5, 8
Após chamada de método com argumentos: 3, 5
Após chamada de método com argumentos: 2, 3
Após chamada de método com argumentos : 1, 2
Após a chamada do método com args: 1, 1
Após a chamada do método com args: 0, 1

Aqui está uma visualização do que está acontecendo.

Vamos repetir: o método printFibonacciWithCondition é chamado. Ele calcula o número atual. Se for conveniente, exibimos e chamamos o método printFibonacciWithCondition novamente com novos argumentos.

Enquanto o método recursivo estiver sendo chamado, isso é chamado de "descida recursiva". Quando o recursivo termina e as chamadas de método retornam, dizemos que a pilha de chamadas está "desenrolando".

Recursão é um tópico interessante em programação. Para entender melhor o material, vamos mudar um pouco nossa tarefa. A nova tarefa é gerar, em ordem decrescente, todos os números da série Fibonacci que não excedam Integer.MAX_VALUE . Já escrevemos todo o código necessário para esta tarefa. Tudo o que resta é trocar a ordem de exibição do número atual e chamar o método recursivo. Ou seja, no primeiro exemplo, o número calculado foi exibido durante a "descida", mas agora precisamos "descer até o fundo" e depois exibir os números "no caminho de volta". E claro, no método main , trocamos os dois números iniciais da sequência (zero e um) após exibi-los após chamarmos o método recursivo. Para facilitar a leitura,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);
    }
}

A saída 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