Eğitim Portalı/Java/Recursion (Özyineleme)
Java01-java/69-recursion

Recursion (Özyineleme)

Özyineleme (recursion), bir metodun kendini çağırmasıdır. İlk bakışta tuhaf görünse de, doğası gereği "kendine benzeyen" problemler için (ağaç gezme, böl-ve-fethet, geri izleme) son derece doğal ve okunaklı bir araçtı…

Recursion (Özyineleme)

Özyineleme (recursion), bir metodun kendini çağırmasıdır. İlk bakışta tuhaf görünse de, doğası gereği "kendine benzeyen" problemler için (ağaç gezme, böl-ve-fethet, geri izleme) son derece doğal ve okunaklı bir araçtır. Bir problemi, aynı problemin daha küçük bir hâli cinsinden ifade edersin; bu küçülme bir "taban duruma" ulaşınca durur.

İki olmazsa olmaz parça

Her özyinelemeli çözümün iki bileşeni vardır:

  1. Taban durum (base case): Özyinelemeyi durduran koşul. Olmazsa metot sonsuza kadar kendini çağırır ve StackOverflowError atar.
  2. Özyinelemeli adım: Problemi daha küçük bir hâline indirgeyip kendini çağırma.
long faktoriyel(int n) {
    if (n <= 1) return 1;              // taban durum
    return n * faktoriyel(n - 1);     // özyinelemeli adım (daha küçük probleme)
}

Örnek 1 (./Ornek1.java) faktöriyel ve fibonacci ile bunu gösterir.

Performans tuzağı ve memoization

Saf (naive) özyineleme bazen aynı alt problemleri tekrar tekrar hesaplar. Klasik örnek fibonacci: fib(40) naive hâliyle milyonlarca tekrar çağrı yapar (üstel). Çözüm memoization: hesaplanan sonuçları sakla, tekrar isteneni doğrudan döndür (doğrusal):

long fib(int n, Map<Integer,Long> bellek) {
    if (n < 2) return n;
    if (bellek.containsKey(n)) return bellek.get(n);
    long r = fib(n-1, bellek) + fib(n-2, bellek);
    bellek.put(n, r);
    return r;
}

Örnek 1, naive ile memoized fibonacci'yi ölçerek karşılaştırır; aradaki fark dramatiktir.

Ağaç/iç içe yapıları gezmek

Özyinelemenin en doğal kullanımı ağaç yapılarıdır: dosya sistemi, JSON, DOM, organizasyon şeması... Her düğüm için "kendini + çocuklarını işle" dersin:

long toplamBoyut(Klasor k) {
    long toplam = k.dosyaBoyutu;
    for (Klasor alt : k.altKlasorler) toplam += toplamBoyut(alt); // her çocuk için kendini çağır
    return toplam;
}

Örnek 2 (./Ornek2.java) bir klasör ağacını gezip toplam boyutu hesaplar ve girintili yazdırır.

Sınırlar: yığın ve iteratif alternatif

Her özyinelemeli çağrı çağrı yığınında (call stack) yer kaplar. Çok derin (veya sonsuz) özyineleme yığını taşırır → StackOverflowError. Önemli bir gerçek:

Java, tail-call optimizasyonu (kuyruk özyineleme) YAPMAZ. Yani "son işlem özyinelemeli çağrı" olsa bile yığın büyür. Çok derin durumlarda iteratif (döngü + gerekirse açık bir yığın/Deque) çözüm tercih edilir.

Örnek 2 hem StackOverflowError'ı (taban durumsuz çağrı) hem iteratif faktöriyel alternatifini gösterir.

Ne zaman özyineleme, ne zaman döngü?

  • Özyineleme: Problem doğal olarak özyinelemeli (ağaç/grafik, böl-ve-fethet, backtracking) ve derinlik makulse — kod çok daha okunaklı olur.
  • İterasyon: Derinlik büyük/kontrolsüzse, basit doğrusal işse veya performans kritikse — yığın taşma riski yoktur.

Özet

Özyinelemenin taban durum + özyinelemeli adımdan oluştuğunu; performans tuzağını ve memoization çözümünü (Örnek 1); ağaç gezmedeki doğal kullanımını, StackOverflowError sınırını ve iteratif alternatifi (Örnek 2) öğrendik; Java'nın tail-call optimizasyonu yapmadığını vurguladık. Sırada, dış programları çalıştırma: Process API.

Kod Örnekleri(2)

Ornek1

çalıştırılabilir
Ornek1.java
1// Ornek1: Özyineleme (recursion) — kendini çağıran metotlar.
2// Çalıştırma: java Ornek1.java
3import java.util.HashMap;
4import java.util.Map;
5
6public class Ornek1 {
7
8    // Faktöriyel: n! = n * (n-1)!  — taban durum: 0! = 1
9    static long faktoriyel(int n) {
10        if (n <= 1) return 1;          // TABAN DURUM (recursion'ı durdurur)
11        return n * faktoriyel(n - 1);  // ÖZYİNELEMELİ ADIM (probleme daha küçük hali)
12    }
13
14    // Fibonacci (naive): üstel, çok yavaş -> aynı alt problemleri tekrar tekrar hesaplar.
15    static long fibNaive(int n) {
16        if (n < 2) return n;
17        return fibNaive(n - 1) + fibNaive(n - 2);
18    }
19
20    // Fibonacci (memoization): hesaplananı sakla -> doğrusal, hızlı.
21    static long fibMemo(int n, Map<Integer, Long> bellek) {
22        if (n < 2) return n;
23        if (bellek.containsKey(n)) return bellek.get(n);
24        long sonuc = fibMemo(n - 1, bellek) + fibMemo(n - 2, bellek);
25        bellek.put(n, sonuc);
26        return sonuc;
27    }
28
29    public static void main(String[] args) {
30        System.out.println("5! = " + faktoriyel(5));
31        System.out.println("10! = " + faktoriyel(10));
32
33        System.out.println("\nFibonacci(40):");
34        long t1 = System.currentTimeMillis();
35        System.out.println("  naive  = " + fibNaive(40) + "  (~" + (System.currentTimeMillis() - t1) + " ms)");
36        long t2 = System.currentTimeMillis();
37        System.out.println("  memo   = " + fibMemo(40, new HashMap<>()) + "  (~" + (System.currentTimeMillis() - t2) + " ms)");
38
39        System.out.println("""
40
41                --- Özyineleme (recursion) ---
42                Bir metodun kendini çağırmasıdır. İki parçası vardır:
43                  TABAN DURUM: özyinelemeyi durduran koşul (yoksa sonsuz döngü -> StackOverflowError).
44                  ÖZYİNELEMELİ ADIM: problemi daha küçük bir haline indirgeyip kendini çağırma.
45                Naive fibonacci üsteldir (aynı işi tekrar yapar); MEMOIZATION ile doğrusala iner.
46                Özyineleme; ağaç/grafik gezme, böl-ve-fethet, geri izleme (backtracking) için doğaldır.""");
47    }
48}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek2

çalıştırılabilir
Ornek2.java
1// Ornek2: Özyineleme ile ağaç gezme + StackOverflow ve iteratif alternatif.
2// Çalıştırma: java Ornek2.java
3import java.util.ArrayList;
4import java.util.List;
5
6public class Ornek2 {
7
8    // İç içe (ağaç) yapı: bir klasör, alt klasörler ve dosyalar içerir.
9    static class Klasor {
10        String ad;
11        long dosyaBoyutu;
12        List<Klasor> altKlasorler = new ArrayList<>();
13        Klasor(String ad, long boyut) { this.ad = ad; this.dosyaBoyutu = boyut; }
14        Klasor ekle(Klasor k) { altKlasorler.add(k); return this; }
15    }
16
17    // Özyinelemeli toplam: bu klasör + tüm alt klasörlerin boyutu.
18    static long toplamBoyut(Klasor k) {
19        long toplam = k.dosyaBoyutu;
20        for (Klasor alt : k.altKlasorler) {
21            toplam += toplamBoyut(alt);     // her alt klasör için kendini çağır
22        }
23        return toplam;
24    }
25
26    // Özyinelemeli yazdırma (girintiyle ağaç görünümü).
27    static void yazdir(Klasor k, int derinlik) {
28        System.out.println("  ".repeat(derinlik) + "📁 " + k.ad + " (" + k.dosyaBoyutu + ")");
29        for (Klasor alt : k.altKlasorler) yazdir(alt, derinlik + 1);
30    }
31
32    public static void main(String[] args) {
33        Klasor kok = new Klasor("proje", 0)
34                .ekle(new Klasor("src", 100)
35                        .ekle(new Klasor("main", 500))
36                        .ekle(new Klasor("test", 300)))
37                .ekle(new Klasor("docs", 200));
38
39        System.out.println("Klasör ağacı:");
40        yazdir(kok, 0);
41        System.out.println("Toplam boyut: " + toplamBoyut(kok));
42
43        // Derin özyineleme StackOverflowError'a yol açabilir; iteratif çözüm güvenlidir.
44        System.out.println("\nDerin özyineleme riski:");
45        try {
46            sonsuzDerin(0);
47        } catch (StackOverflowError e) {
48            System.out.println("  StackOverflowError yakalandı! Çok derin özyineleme yığını taşırdı.");
49        }
50        // İteratif faktöriyel (özyinelemesiz) — yığın taşma riski yok:
51        long f = 1; for (int i = 1; i <= 20; i++) f *= i;
52        System.out.println("  iteratif 20! = " + f);
53
54        System.out.println("""
55
56                --- Ağaç gezme + özyineleme sınırları ---
57                Özyineleme, iç içe (ağaç/grafik) yapılar için DOĞAL araçtır: her düğüm için kendini çağır.
58                ANCAK her çağrı yığında (stack) yer kaplar; çok derin/sonsuz özyineleme StackOverflowError verir.
59                Java tail-call optimizasyonu YAPMAZ; çok derin durumlarda İTERATİF çözüm (döngü+yığın) tercih edilir.
60                Kural: özyineleme okunabilirlik kazandırır; derinlik kontrol edilemiyorsa iteratif yaz.""");
61    }
62
63    static void sonsuzDerin(int n) { sonsuzDerin(n + 1); } // taban durum YOK -> taşar
64}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.