Contoh kode rekursif tanpa kondisi keluar

Mari kita lihat lagi masalah rekursif. Sebagai contoh, pertimbangkan untuk menghitung angka Fibonacci. Semua orang akan ingat bahwa deret Fibonacci adalah deret numerik di mana dua angka pertama adalah 0 dan 1, dan setiap angka berikutnya sama dengan jumlah dari dua angka sebelumnya.

Mari tulis kode untuk menghitung dan menampilkan angka-angka ini:

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);
    }
}

Sebelum panggilan pertama dari metode printFibonacci rekursif , cetak dua angka pertama dari urutan: nol dan satu. Kita perlu melakukan ini karena metode rekursif hanya menampilkan jumlah dari parameter masukan, bukan parameter itu sendiri.

Kode terlihat oke: kita mendapatkan dua angka, menghitung jumlahnya, mencetaknya di konsol, dan memanggil metode printFibonacci secara rekursif lagi. Kami meneruskan nomor sebelumnya (sebelumnya) dan nomor saat ini (saat ini) sebagai argumen.

Sebenarnya, kode tersebut memiliki dua kesalahan. Anda dapat melihatnya jika Anda menjalankan kode.

Kesalahan pertama meluap tipe panjang . Angka ke-104 dalam deret kita adalah negatif, menunjukkan bahwa tipe long overflow.

Kesalahan kedua berbeda. Setelah menghitung kira-kira 12.000 angka, kami mendapatkan:

Pengecualian di utas "utama" java.lang.StackOverflowError

Sekarang adalah waktu yang tepat untuk mengingat apa itu tumpukan pemanggilan metode di Jawa. Mesin Java mencatat semua panggilan fungsi. Untuk melakukan ini, ia menggunakan koleksi khusus yang disebut tumpukan. Saat satu fungsi memanggil yang lain, mesin Java mendorong StackTraceElement baru ke tumpukan. Saat fungsi berakhir, elemen ini dihapus dari tumpukan. Oleh karena itu, stack selalu menyimpan informasi terkini tentang keadaan saat ini dari stack pemanggilan fungsi. Dokumentasi untuk StackTraceElement mengatakan bahwa itu adalah "Dilempar ketika stack overflow terjadi karena aplikasi berulang terlalu dalam." JVM yang sedang berjalan memiliki area memori khusus untuk menyimpan tumpukan pemanggilan metode. Ukuran area memori ini bergantung pada pengaturan OS dan JVM. Selain tumpukan pemanggilan metode itu sendiri, variabel primitif (nilai spesifik dari parameter metode) dan alamat variabel referensi (di area memori yang disebut "heap") disimpan di area memori khusus ini. Akses ke tumpukan panggilan mengikuti prinsip LIFO.

Contoh yang diperbaiki dengan kondisi keluar

Mari kita mulai dengan memperbaiki masalah kedua pada kode.

Mari kita coba selesaikan masalah secara langsung: jika tumpukan terlalu kecil, mari kita buat lebih besar. Untuk melakukannya, mulai JVM dengan flag "-Xss" dan tentukan berapa banyak memori yang akan dialokasikan untuk stack. Mari kita coba alokasikan 5 megabyte. Ini akan terlihat seperti di IDEA:

Kami berhasil menambah panjang output. Sekarang dapat menghitung lebih dari 49.000 angka urutan daripada dibatasi sekitar 12.000 angka. Namun pada titik tertentu, kami masih mendapatkan StackOverflowError .

Anda dapat mencoba menambah ukuran tumpukan, tetapi itu tidak menyelesaikan masalah mendasar. Jadi, mari kita cari masalah dalam logika. Harus ada titik ketika rekursi berhenti. Dengan kata lain, harus ada beberapa kondisi yang menentukan kapan metode rekursif tidak lagi dipanggil, memungkinkan tumpukan panggilan untuk bersantai. Untuk menentukan kondisi seperti itu, mari buat tujuan kita eksplisit: tampilkan deret Fibonacci selama angkanya kurang dari Integer.MAX_VALUE .

Mari tulis metode printFibonacciWithCondition baru yang akan mempertimbangkan kondisi ini. Dan kami akan memanggil metode baru yang dikoreksi dalam metode utama.

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);
    }
}

Setelah menjalankan kode, kita melihat bahwa output diakhiri dengan angka 1836311903. Angka sebelum ini adalah 1134903170. Jumlah dari angka-angka ini adalah 2_971_215_073, yang memang lebih besar dari Integer.MAX_VALUE (2_147_483_647 ) .

Perubahan ini secara otomatis memperbaiki bug long overflow. Jika Anda perlu menghitung lebih banyak seri, Anda perlu menggunakan tipe data lain, seperti BigInteger .

Keturunan rekursif dan pelepasan

Mari menganalisis langkah demi langkah bagaimana kode kita dieksekusi. Untuk melakukannya, kita akan menambahkan metode gema dan memanggilnya sebelum dan sesudah pemanggilan rekursif dari metode 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);
        }
    }
}

Program memberi kita output ini:

0
1
Sebelum pemanggilan metode dengan args: 0, 1. Angka saat ini = 1
Sebelum pemanggilan metode dengan args: 1, 1. Nomor saat ini = 2
Sebelum pemanggilan metode dengan args: 1, 2. Nomor saat ini = 3
Sebelum pemanggilan metode dengan args: 2, 3. Nomor saat ini = 5
Sebelum pemanggilan metode dengan args: 3, 5. Nomor saat ini = 8
Sebelum pemanggilan metode dengan args: 5, 8. Nomor saat ini = 13
Sebelum pemanggilan metode dengan args: 8, 13. Nomor saat ini = 21
Sebelum pemanggilan metode dengan argumen: 13, 21. Angka saat ini = 34
Sebelum pemanggilan metode dengan argumen: 21, 34. Nomor saat ini = 55
Sebelum pemanggilan metode dengan argumen: 34, 55. Nomor saat ini = 89
Sebelum pemanggilan metode dengan argumen: 55, 89. Angka saat ini = 144
Sebelum pemanggilan metode dengan argumen: 89, 144. Angka saat ini = 233
Sebelum pemanggilan metode dengan argumen: 144, 233. Nomor saat ini = 377
Sebelum pemanggilan metode dengan argumen: 233, 377. Nomor saat ini = 610
Sebelum pemanggilan metode dengan argumen: 377, 610. Nomor saat ini = 987
Sebelum pemanggilan metode dengan args: 610, 987. Nomor saat ini = 1597
Sebelum pemanggilan metode dengan args: 987, 1597. Nomor saat ini = 2584
Sebelum pemanggilan metode dengan args: 1597, 2584. Nomor saat ini = 4181
Sebelum metode panggil dengan args: 2584, 4181. Nomor sekarang = 6765
Sebelum pemanggilan metode dengan args: 4181, 6765. Nomor sekarang = 10946
Sebelum pemanggilan metode dengan args: 6765, 10946. Nomor sekarang = 17711
Sebelum pemanggilan metode dengan args: 10946, 17711. Nomor saat ini = 28657
Sebelum pemanggilan metode dengan args: 17711, 28657. Nomor saat ini = 46368
Sebelum pemanggilan metode dengan args: 28657, 46368. Nomor saat ini = 75025
Sebelum pemanggilan metode dengan args: 46368, 75025. Nomor saat ini = 121393
Sebelum pemanggilan metode dengan args: 75025, 121393. Nomor Saat Ini = 196418
Sebelum Metode Panggilan dengan Args: 121393, 196418. Nomor Saat Ini = 317811
Sebelum Metode Panggilan dengan Args: 196418, 317811. Nomor Saat Ini = 514229 Sebelum Metode Panggilan dengan Args: 317811, 514229. Nomor saat ini = 832040 Sebelum metode
ARGS: 317811, 514229.
panggil dengan args: 514229, 832040. Nomor sekarang = 1346269
Sebelum pemanggilan metode dengan args: 832040, 1346269. Nomor sekarang = 2178309
Sebelum pemanggilan metode dengan args: 1346269, 2178309. Nomor sekarang = 3524578
Sebelum pemanggilan metode dengan args: 2178309, 3524578. Nomor sekarang = 5702887
Sebelum pemanggilan metode dengan args: 3524578, 5702887. Nomor sekarang = 9227465
Sebelum pemanggilan metode dengan args: 5702887, 9227465. Nomor sekarang = 14930352
Sebelum pemanggilan metode dengan args: 9 227465, 14930352. Nomor saat ini = 24157817
Sebelum pemanggilan metode dengan args: 14930352, 24157817. Nomor saat ini = 39088169 Sebelum
pemanggilan metode dengan args: 24157817, 39088169. Nomor saat ini = 63245986 Sebelum
pemanggilan metode dengan args: 39088169, 6 3245986. Angka saat ini = 102334155
Sebelum metode panggil dengan args: 63245986, 102334155. Nomor saat ini = 165580141
Sebelum panggilan metode dengan args: 102334155, 165580141. Nomor saat ini = 267914296
Sebelum pemanggilan metode dengan args: 165580141, 267914296. Nomor saat ini = 433494437
Sebelum pemanggilan metode dengan args: 267914296, 433494437. Nomor saat ini = 701408733 Sebelum pemanggilan metode dengan args: 433494437, 701408733. Nomor saat ini = 11
34903170
Sebelum pemanggilan metode dengan argumen: 701408733, 1134903170. Nomor sekarang = 1836311903
Setelah pemanggilan metode dengan args: 701408733, 113490317
Setelah pemanggilan metode dengan args: 433494437, 701408733 Setelah pemanggilan metode dengan args: 267914296, 433494437
Setelah
pemanggilan metode dengan args: 165580141, 267914296
Setelah pemanggilan metode dengan argumen: 102334155, 165580141
Setelah pemanggilan metode dengan args: 63245986, 102334155
Setelah pemanggilan metode dengan args: 39088169, 63245986
Setelah pemanggilan metode dengan args: 24157817, 39088169
Setelah pemanggilan metode dengan args: 14930352, 24157817
Setelah pemanggilan metode dengan args: 9227465, 14930352
Setelah pemanggilan metode dengan args: 5702887, 9227465 Setelah pemanggilan metode dengan args: 3524578
, 5702887
Setelah pemanggilan metode dengan args : 2178309, 3524578
Setelah pemanggilan metode dengan args: 1346269, 2178309
Setelah
pemanggilan metode dengan args: 832040, 1346269 Setelah pemanggilan metode dengan args: 514229, 832040 Setelah
pemanggilan metode dengan args: 317811, 514229 Setelah pemanggilan metode dengan args: 196418
, 317811
Setelah pemanggilan metode dengan args: 121393, 196418
Setelah pemanggilan metode dengan args: 75025, 121393
Setelah pemanggilan metode dengan args: 46368, 75025
Setelah pemanggilan metode dengan args: 28657, 46368
Setelah pemanggilan metode dengan args: 17711, 28657
Setelah pemanggilan metode dengan args: 10946,
17711 Setelah pemanggilan metode dengan args: 6765, 10946
Setelah pemanggilan metode dengan args: 4181, 6765
Setelah pemanggilan metode dengan args : 2584, 4181
Setelah pemanggilan metode dengan args: 1597, 2584
Setelah pemanggilan metode dengan args: 987, 1597
Setelah pemanggilan metode dengan args: 610, 987
Setelah pemanggilan metode dengan args: 377, 610
Setelah pemanggilan metode dengan args: 233, 377
Setelah pemanggilan metode dengan args: 144, 233
Setelah pemanggilan metode dengan args: 89, 144
Setelah pemanggilan metode dengan args: 55, 89
Setelah pemanggilan metode dengan args: 34, 55
Setelah pemanggilan metode dengan args: 21, 34
Setelah pemanggilan metode dengan args: 13, 21
Setelah pemanggilan metode dengan args: 8, 13
Setelah pemanggilan metode dengan args: 5, 8
Setelah pemanggilan metode dengan args: 3, 5
Setelah pemanggilan metode dengan args: 2, 3
Setelah pemanggilan metode dengan args : 1, 2
Setelah pemanggilan metode dengan args: 1, 1
Setelah pemanggilan metode dengan args: 0, 1

Inilah visualisasi dari apa yang terjadi.

Katakan lagi: metode printFibonacciWithCondition dipanggil. Ini menghitung jumlah saat ini. Jika cocok untuk kita, maka kita tampilkan dan panggil kembali metode printFibonacciWithCondition dengan argumen baru.

Selama metode rekursif dipanggil, ini disebut "keturunan rekursif". Ketika rekursif berakhir dan pemanggilan metode kembali, kita mengatakan bahwa tumpukan panggilan "melepas".

Rekursi adalah topik yang menarik dalam pemrograman. Untuk memahami materi dengan lebih baik, mari ubah sedikit tugas kita. Tugas baru adalah menampilkan, dalam urutan menurun, semua angka dalam deret Fibonacci yang tidak melebihi Integer.MAX_VALUE . Kami telah menulis semua kode yang diperlukan untuk tugas ini. Yang tersisa hanyalah menukar urutan menampilkan nomor saat ini dan memanggil metode rekursif. Artinya, pada contoh pertama, angka yang dihitung ditampilkan selama "turun", tetapi sekarang kita perlu "turun ke paling bawah" dan kemudian menampilkan angka "dalam perjalanan kembali". Dan tentu saja, dalam metode utama , kami menukar dua angka awal dari urutan (nol dan satu) setelah menampilkannya setelah kami memanggil metode rekursif. Untuk keterbacaan,metode.

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);
    }
}

Outputnya adalah:

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