Çıkış koşulu olmayan özyinelemeli kod örneği

Özyinelemeli bir probleme bir kez daha göz atalım. Örnek olarak, Fibonacci sayılarını hesaplamayı düşünün. Fibonacci dizisinin ilk iki sayının 0 ve 1 olduğu ve sonraki her sayının önceki iki sayının toplamına eşit olduğu bir sayısal dizi olduğunu herkes hatırlayacaktır.

Bu sayıları hesaplamak ve göstermek için kod yazalım:


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

Özyinelemeli printFibonacci yönteminin ilk çağrısından önce , dizinin ilk iki sayısını yazdırın: sıfır ve bir. Bunu yapmamız gerekiyor çünkü özyinelemeli yöntem parametrelerin kendisini değil yalnızca girdi parametrelerinin toplamını gösteriyor.

Kod iyi görünüyor: iki sayı alıyoruz, toplamlarını hesaplıyoruz, konsolda yazdırıyoruz ve tekrar tekrar printFibonacci yöntemini çağırıyoruz. Önceki numarayı (önceki) ve mevcut numarayı (mevcut) argüman olarak iletiyoruz.

Aslında, kodun iki hatası var. Kodu çalıştırırsanız bunları görebilirsiniz.

İlk hata, uzun türün taşmasıdır . Dizimizdeki 104. sayı negatiftir, bu da uzun tipin taştığını gösterir.

İkinci hata farklı. Yaklaşık 12.000 sayı hesapladıktan sonra şunu elde ederiz:

Java.lang.StackOverflowError "main" iş parçacığında istisna

Şimdi, Java'da bir yöntem çağrı yığınının ne olduğunu hatırlamak için uygun bir zaman. Java makinesi tüm işlev çağrılarının kaydını tutar. Bunu yapmak için yığın adı verilen özel bir koleksiyon türü kullanır. Bir işlev diğerini çağırdığında, Java makinesi yığına yeni bir StackTraceElement gönderir. Fonksiyon sona erdiğinde, bu eleman yığından kaldırılır. Buna göre yığın, işlev çağrı yığınının mevcut durumu hakkında her zaman güncel bilgileri depolar. StackTraceElement belgeleri, "Bir uygulama çok derin bir şekilde yinelendiğinden bir yığın taşması meydana geldiğinde atılır" olduğunu söylüyor. Çalışan bir JVM, yöntem çağrı yığınını depolamak için özel bir bellek alanına sahiptir. Bu bellek alanının boyutu işletim sistemi ve JVM ayarlarına bağlıdır. Yöntem çağrı yığınına ek olarak, ilkel değişkenler (yöntem parametrelerinin belirli değerleri) ve referans değişkenlerin adresleri ("yığın" adı verilen bir bellek alanında) bu özel bellek alanında saklanır. Çağrı yığınına erişim, LIFO ilkesini izler.

Çıkış koşuluyla düzeltilmiş örnek

Koddaki ikinci sorunu çözerek başlayalım.

Sorunu kafa kafaya çözmeye çalışalım: Yığın çok küçükse, onu büyütelim. Bunu yapmak için JVM'yi "-Xss" bayrağıyla başlatın ve yığın için ne kadar bellek ayrılacağını belirtin. 5 megabayt ayırmayı deneyelim. IDEA'da şöyle görünecek:

Çıktının uzunluğunu artırmayı başardık. Artık, yaklaşık 12.000 sayıyla sınırlı olmak yerine, dizinin 49.000'den fazla sayısını hesaplayabilir. Ancak bir noktada, hala bir StackOverflowError alıyoruz .

Yığının boyutunu artırmayı deneyebilirsiniz, ancak bu temel sorunu çözmez. Öyleyse mantıkta bir problem arayalım. Özyinelemenin durduğu bir nokta olmalıdır. Başka bir deyişle, özyinelemeli yöntemin ne zaman artık çağrılmayacağına karar veren ve çağrı yığınının gevşemesine izin veren bir koşul olmalıdır. Böyle bir koşulu tanımlamak için, amacımızı açıklığa kavuşturalım: sayıları Integer.MAX_VALUE değerinden küçük olduğu sürece Fibonacci serisini görüntüleyin .

Bu durumu dikkate alacak yeni bir printFibonacciWithCondition metodu yazalım . Ve yeni düzeltilmiş yöntemi ana yöntemde arayacağız.


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

Kodu çalıştırdıktan sonra çıktının 1836311903 sayısı ile bittiğini görüyoruz. Bundan önceki sayı 1134903170. Bu sayıların toplamı 2_971_215_073 ki bu da Integer.MAX_VALUE'den (2_147_483_647) gerçekten büyük .

Bu değişiklik, uzun taşma hatasını otomatik olarak düzeltti. Serinin daha fazlasını hesaplamanız gerekirse, BigInteger gibi diğer veri türlerini kullanmanız gerekecektir .

Yinelemeli iniş ve gevşeme

Kodumuzun nasıl yürütüldüğünü adım adım inceleyelim. Bunu yapmak için bir yankı yöntemi ekleyeceğiz ve onu printFibonacciWithCondition yönteminin özyinelemeli çağrısından önce ve sonra çağıracağız .


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 bize şu çıktıyı veriyor:

0
1
args ile yöntem çağrısından önce: 0, 1. Geçerli sayı = 1
args ile yöntem çağrısından önce: 1, 1. Geçerli sayı = 2
args ile yöntem çağrısından önce: 1, 2. Geçerli sayı = 3
args ile yöntem çağrısından önce: 2, 3. Geçerli sayı = 5
args ile yöntem çağrısından önce: 3, 5. Geçerli sayı = 8
args ile yöntem çağrısından önce: 5, 8. Geçerli sayı = 13
args ile yöntem çağrısından önce: 8, 13. Geçerli sayı = 21
args ile yöntem çağrısından önce: 13, 21. Geçerli sayı = 34
args ile yöntem çağrısından önce: 21, 34. Geçerli sayı = 55
args ile yöntem çağrısından önce: 34, 55. Geçerli sayı = 89
args ile yöntem çağrısından önce: 55, 89. Mevcut sayı = 144
args ile yöntem çağrısından önce: 89, 144. Geçerli sayı = 233
args ile yöntem çağrısından önce: 144, 233. Geçerli sayı = 377
args ile yöntem çağrısından önce: 233, 377. Geçerli sayı = 610
args ile yöntem çağrısından önce: 377, 610. Geçerli sayı = 987
args ile yöntem çağrısından önce: 610, 987. Geçerli sayı = 1597
args ile yöntem çağrısından önce: 987, 1597. Geçerli sayı = 2584
args ile yöntem çağrısından önce: 1597, 2584. Geçerli sayı = 4181
yöntemden önce args ile çağrı: 2584, 4181. Geçerli sayı = 6765
args ile yöntem çağrısından önce: 4181, 6765. Geçerli sayı = 10946
args ile yöntem çağrısından önce: 6765, 10946. Geçerli sayı = 17711
args ile yöntem çağrısından önce: 10946, 17711. Mevcut numara = 28657
args ile yöntem çağrısından önce: 17711, 28657. Geçerli sayı = 46368
args ile yöntem çağrısından önce: 28657, 46368. Geçerli sayı = 75025 args
ile yöntem çağrısından önce: 46368, 75025. Geçerli numara = 121393
args ile yöntem çağrısından önce: 75025, 121393. Geçerli numara = 196418
args ile yöntem çağrısından önce: 121393, 196418. Geçerli numara = 317811
args ile yöntem çağrısından önce: 196418, 317811. Geçerli numara = 514229
args ile yöntem çağrısından önce: 317811, 514229. Geçerli sayı = 832040
yöntemden önce args ile çağrı: 514229, 832040. Mevcut numara = 1346269
args ile metod çağrısından önce: 832040, 1346269. Mevcut numara = 2178309
args ile metod çağrısından önce: 1346269, 2178309. Mevcut numara = 3524578
args ile yöntem çağrısından önce: 2178309, 3524578. Geçerli numara = 5702887
args ile yöntem çağrısından önce: 3524578, 5702887. Geçerli numara = 9227465 args ile yöntem
çağrısından önce: 5702887, 9227465. Geçerli numara = 14930352
args ile yöntem çağrısından önce: 92274 65, 14930352. Geçerli numara = 24157817
args ile yöntem çağrısından önce: 14930352, 24157817. Geçerli numara = 39088169
args ile yöntem çağrısından önce: 24157817, 39088169. Geçerli numara = 63245986 args
ile yöntem çağrısından önce: 39088169, 6324 5986. Geçerli sayı = 102334155
Yöntemden önce args ile çağrı: 63245986, 102334155. Geçerli numara = 165580141
args ile yöntem çağrısından önce: 102334155, 165580141. Geçerli numara = 267914296
args ile yöntem çağrısından önce: 165580141, 267914296. Geçerli numara = 433494437
args ile yöntem çağrısından önce: 267914296, 433494437. Geçerli numara = 701408733 args ile yöntem çağrısından önce: 433494437, 701408733. Geçerli sayı = 11349
03170 args
ile yöntem çağrısından önce: 701408733, 1134903170. Geçerli sayı = 1836311903
args ile yöntem çağrısından sonra: 701408733, 113490317
args ile yöntem çağrısından sonra: 433494437, 701408733 args ile yöntem çağrısından sonra: 267914296, 433494437 args ile yöntem çağrısından sonra: 1655
80141
, 267914296
args ile yöntem çağrısından sonra: 102334155, 165580141
args ile yöntem çağrısından sonra: 63245986, 102334155
args ile yöntem çağrısından sonra: 39088169, 63245986
args ile yöntem çağrısından sonra: 24157817, 39088169
args ile yöntem çağrısından sonra: 14930352, 24157817
args ile yöntem çağrısından sonra: 9227465, 14930352 args ile yöntem çağrısından sonra: 5702887, 9227465 args ile
yöntem çağrısından sonra: 3524578
, 5702 887 args ile
yöntem çağrısından sonra : 2178309, 3524578
args ile yöntem çağrısından sonra: 1346269, 2178309
args ile yöntem çağrısından sonra: 832040, 1346269 args ile
yöntem çağrısından sonra: 514229, 832040 args ile yöntem çağrısından sonra: 317811
, 514229 args ile yöntem çağrısından sonra
: 19641 8, 317811
sonra args ile yöntem çağrısı: 121393, 196418
args ile yöntem çağrısından sonra: 75025, 121393
args ile yöntem çağrısından sonra: 46368, 75025
args ile yöntem çağrısından sonra: 28657, 46368
args ile yöntem çağrısından sonra: 17711, 28657
args ile yöntem çağrısından sonra: 10946, 17711
args ile yöntem çağrısından sonra: 6765, 10946
args ile yöntem çağrısından sonra: 4181, 6765
args ile yöntem çağrısından sonra : 2584, 4181
args ile yöntem çağrısından sonra: 1597, 2584
args ile yöntem çağrısından sonra: 987, 1597
args ile yöntem çağrısından sonra: 610, 987
args ile yöntem çağrısından sonra: 377, 610
args ile yöntem çağrısından sonra: 233, 377
Sonra args ile yöntem çağrısı: 144, 233
args ile yöntem çağrısından sonra: 89, 144
args ile yöntem çağrısından sonra: 55, 89
args ile yöntem çağrısından sonra: 34, 55
args ile yöntem çağrısından sonra: 21, 34
args ile yöntem çağrısından sonra: 13, 21
args ile yöntem çağrısından sonra: 8, 13
args ile yöntem çağrısından sonra: 5, 8
args ile yöntem çağrısından sonra: 3, 5 args ile yöntem çağrısından
sonra: 2, 3 args ile
yöntem çağrısından sonra : 1, 2
args ile yöntem çağrısından sonra: 1, 1
args ile yöntem çağrısından sonra: 0, 1

İşte neler olduğuna dair bir görselleştirme.

Tekrar söyleyelim: printFibonacciWithCondition yöntemi çağrılır. Mevcut sayıyı hesaplar. Bize uygunsa, onu görüntüler ve printFibonacciWithCondition yöntemini yeni argümanlarla tekrar çağırırız.

Özyinelemeli yöntem çağrıldığı sürece buna "özyinelemeli iniş" denir. Özyinelemeli sona erdiğinde ve yöntem çağrıları geri döndüğünde, çağrı yığınının "gevşediğini" söyleriz.

Özyineleme, programlamada ilginç bir konudur. Malzemeyi daha iyi kavramak için görevimizi biraz değiştirelim. Yeni görev, Fibonacci serisindeki Integer.MAX_VALUE değerini aşmayan tüm sayıları azalan düzende çıkarmaktır . Bu görev için gereken tüm kodu zaten yazdık. Geriye kalan tek şey, geçerli sayıyı görüntüleme sırasını değiştirmek ve özyinelemeli yöntemi çağırmak. Yani, ilk örnekte, hesaplanan sayı "iniş" sırasında görüntüleniyordu, ancak şimdi "en alta inmemiz" ve ardından "geri dönerken" sayıları görüntülememiz gerekiyor. Ve tabi ki main metodunda recursive metodu çağırdıktan sonra dizinin ilk iki numarasını (sıfır ve bir) ekrana getirdikten sonra yer değiştiriyoruz. Okunabilirlik için,yöntem.


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

Çıktı şöyle olacaktır:

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