Bölüm 3.5: Özyineleme

Java ile Nesne Yönelimli Programlama
Taban durumÇağrı yığınıFaktöriyel ve toplamFibonacci, Lucas, Catalan, Bell Palindrom ve EBOBHanoi kuleleriVerimlilik ve döngülerKuyruk özyinelemesiBellekleme
Sercan KÜLCÜ · Giresun Üniversitesi · Elektrik-Elektronik Mühendisliği

Giriş

Öğrenme Çıktıları

Kavram

Özyinelemeyi (recursion), taban durumu ve özyinelemeli adımı tanımlamak; bir problemi kendisinin küçük bir kopyasına indirgemek.

Çalışma biçimi

Çağrı yığınında çerçevelerin nasıl biriktiğini ve geri döndüğünü adım adım izlemek; çağrı ağacı çizmek.

Yazma

Faktöriyel, toplam, seri, rakamlar toplamı, palindrom, EBOB ve Hanoi kuleleri için özyinelemeli Java metotları yazmak.

Hatalar

Sonsuz özyinelemeyi, StackOverflowError'ı ve eksik return hatalarını tanımak ve önlemek.

Verimlilik

Özyinelemeli ve ilerlemeli (iterative) çözümleri karşılaştırmak; gerektiğinde döngüye çevirmek.

İleri teknikler

Kuyruk özyinelemesini ve belleklemeyi (memoization) açıklamak; tekrarlanan hesaplamaları ortadan kaldırmak.

Özyineleme Kavramı

Özyineleme Nedir?

  • Özyineleme (Recursion): bir metodun, görevini küçük parçalara bölerek kendini çağırmasıdır.
  • Her çağrı, problemin daha küçük ama aynı tipten bir kopyasını çözer; en küçük kopya doğrudan çözülür.
  • Tekrarlama süreci kendini benzer biçimde tekrar eder: bir yapının içinde aynı tipten bir yapı bulunur.
  • Bazı problemleri (ağaçlar, seriler, böl ve fethet) döngülerden daha anlaşılır ve temiz çözer.
Java'da özyineleme için özel bir anahtar sözcük yoktur: metot, gövdesinde kendi adını çağırır.
Matruşka bebekleriHer bebeğin içinde daha küçük bir bebek vardır; en küçüğü açılmaz (taban durum).
Sinemada kaçıncı sıra?Herkes öndekine sorar, o da öndekine… İlk sıradaki "1" der; cevaplar geri gelirken her kişi 1 ekler.
Karşılıklı aynalarGörüntünün içinde görüntü: taban durumu olmayan özyineleme sonsuza gider.

Özyineleme Kavramı

Taban Durum ve Özyinelemeli Adım

1. Taban durum (base case)Cevabı doğrudan bilinen en küçük problem. Burada metot kendini çağırmaz ve özyineleme durur. Örn. 1! = 1.
2. Özyinelemeli adım (recursive step)Problemi bir adım küçültüp kendine sorar, gelen cevabı kullanarak kendi cevabını kurar. Örn. 4! = 4 × 3!.

Matematikteki tümevarımsal tanımın birebir karşılığıdır:
n! = 1 (n ≤ 1 ise),   n! = n × (n − 1)! (n > 1 ise)

faktoriyel(4) = 4 × faktoriyel(3)faktoriyel(3) = 3 × faktoriyel(2)faktoriyel(2) = 2 × faktoriyel(1)faktoriyel(1) = 1taban durum: kendini çağırmaz

Özyineleme Kavramı

Çağrı Yığını

  • Her metot çağrısı için bellekte bir çerçeve (stack frame) açılır: parametreler, yerel değişkenler ve dönülecek yer burada tutulur.
  • Çerçeveler çağrı yığınına (call stack) üst üste konur; yalnızca tepedeki çerçeve çalışır, alttakiler bekler.
  • Özyinelemede aynı metodun birden çok çerçevesi aynı anda yaşar; her birinin kendi n'si vardır.
  • Metot return ettiğinde çerçevesi silinir ve değer, bir alttaki çerçeveye iletilir (son giren ilk çıkar: LIFO).
  • Yığının boyutu sınırlıdır; çok derin özyineleme StackOverflowError ile biter.
main()faktoriyel(4) sonucunu bekliyorfaktoriyel(4)n = 4 · 4 * ? bekliyorfaktoriyel(3)n = 3 · 3 * ? bekliyorfaktoriyel(2)n = 2 · 2 * ? bekliyorfaktoriyel(1)n = 1 · return 1çağrı: çerçeve eklenirdönüş: çerçeve siliniryığının tepesi (şimdi çalışan)

Faktöriyel ve Toplam

Faktöriyel Alma

int faktoriyel(int n) {
    if (n == 0 || n == 1) {
        return 1;   // taban durum
    } else {
        return n * faktoriyel(n - 1); // özyineleme
    }
}
Matematikn! = 1 × 2 × … × n
n! = n × (n − 1)!,  0! = 1! = 1
Satır 2–3: taban durum0! = 1! = 1; burada özyineleme durur.
Satır 5: özyinelemeli adımn!, (n − 1)!'in n ile çarpımıdır.
İlerlemeHer çağrıda n 1 azalır → n ≥ 0 için taban duruma mutlaka ulaşılır.
Taşmaint 13!'i taşıyamaz: faktoriyel(13) = 1932053504 (yanlış!). long 20!'e kadar yeter.

Çağrı: faktoriyel(5) → 120 · faktoriyel(0) → 1

Faktöriyel ve Toplam

faktoriyel(4) Açılımı

faktoriyel(4)
  = 4 * faktoriyel(3)
  = 4 * (3 * faktoriyel(2))
  = 4 * (3 * (2 * faktoriyel(1)))
  = 4 * (3 * (2 * 1))
  = 4 * (3 * 2)
  = 4 * 6
  = 24
İniş (çağrılar)İlk dört satırda her çağrı yeni bir çağrı açar; çarpımlar bekletilir, çünkü sağ taraftaki değer henüz bilinmiyor.
Dip (taban durum)faktoriyel(1) doğrudan 1 döndürür. Kod n == 1'de durduğu için faktoriyel(0) hiç çağrılmaz.
Çıkış (dönüşler)Bekleyen çarpımlar içten dışa doğru tamamlanır: 2 × 1, 3 × 2, 4 × 6.

Faktöriyel ve Toplam · Adım adım kod izleme

faktoriyel(4): Kodun Adım Adım İzlenmesi

faktoriyel(4)faktoriyel(3)faktoriyel(2)faktoriyel(1)

main, faktoriyel(4) çağrısını yapar; sonucu yazdırmak için beklemeye başlar.

faktoriyel(4)n = 4faktoriyel(3)faktoriyel(2)faktoriyel(1)

Yeni çerçeve: n = 4. Koşul n == 0 || n == 1 yanlış → taban durum değil.

faktoriyel(4)4 * ?faktoriyel(3)faktoriyel(2)faktoriyel(1)

4 * faktoriyel(3) hesaplanmalı. Çarpım bekler; önce faktoriyel(3) çağrılır.

faktoriyel(4)4 * ?faktoriyel(3)n = 3faktoriyel(2)faktoriyel(1)

Yeni çerçeve: n = 3. Koşul n == 0 || n == 1 yanlış → taban durum değil.

faktoriyel(4)4 * ?faktoriyel(3)3 * ?faktoriyel(2)faktoriyel(1)

3 * faktoriyel(2) hesaplanmalı. Çarpım bekler; önce faktoriyel(2) çağrılır.

faktoriyel(4)4 * ?faktoriyel(3)3 * ?faktoriyel(2)n = 2faktoriyel(1)

Yeni çerçeve: n = 2. Koşul n == 0 || n == 1 yanlış → taban durum değil.

faktoriyel(4)4 * ?faktoriyel(3)3 * ?faktoriyel(2)2 * ?faktoriyel(1)

2 * faktoriyel(1) hesaplanmalı. Çarpım bekler; önce faktoriyel(1) çağrılır.

faktoriyel(4)4 * ?faktoriyel(3)3 * ?faktoriyel(2)2 * ?faktoriyel(1)return 1

n = 1: taban durum. Yeni çağrı yapılmaz, 1 döndürülür. Artık çerçeveler geri dönmeye başlar.

faktoriyel(4)4 * ?faktoriyel(3)3 * ?faktoriyel(2)2 * 1 = 2faktoriyel(1)= 1

faktoriyel(1) çerçevesi yığından çıktı, 1 getirdi: 2 * 1 = 2 döner.

faktoriyel(4)4 * ?faktoriyel(3)3 * 2 = 6faktoriyel(2)= 2faktoriyel(1)= 1

faktoriyel(2) çerçevesi yığından çıktı, 2 getirdi: 3 * 2 = 6 döner.

faktoriyel(4)4 * 6 = 24faktoriyel(3)= 6faktoriyel(2)= 2faktoriyel(1)= 1

faktoriyel(3) çerçevesi yığından çıktı, 6 getirdi: 4 * 6 = 24 döner.

faktoriyel(4)= 24faktoriyel(3)= 6faktoriyel(2)= 2faktoriyel(1)= 1

24 main'e döndü ve yazdırıldı. 4 çağrı yapıldı; en derin anda yığında 5 çerçeve vardı.

static int faktoriyel(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * faktoriyel(n - 1);
    }
}

public static void main(String[] args) {
    System.out.println(faktoriyel(4));
}

Faktöriyel ve Toplam

Toplam Bulma

int toplam(int n) {
    if (n == 1) {
        return 1;
    } else {
        return n + toplam(n - 1);
    }
}

1'den n'ye kadar sayıların toplamı: toplam(n) = n + toplam(n − 1), toplam(1) = 1.

toplam(5) = 5 + toplam(4)
          = 5 + (4 + (3 + (2 + toplam(1))))
          = 5 + (4 + (3 + (2 + 1)))
          = 15

Faktöriyelle aynı kalıp; yalnızca × yerine + ve taban durum farklı. toplam(100) = 5050.

Taban durum yalnızca n == 1. toplam(0) ya da negatif bir sayı çağrılırsa n hiç 1 olmaz: 0, −1, −2, … sonsuz özyineleme → StackOverflowError.

Güvenli sürümde taban durum if (n <= 0) return 0; olur; tüm geçersiz girdiler de yakalanır.

Seriler

Fibonacci Serisi

  • 0 ile başlar, ardından 1 gelir: F(0) = 0, F(1) = 1.
  • Genel formül: F(n) = F(n − 1) + F(n − 2): her terim önceki iki terimin toplamı.
  • Metot kendini iki kez çağırır: çağrılar bir ağaç oluşturur (ağaç özyinelemesi).
int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}
n0123456789101112
F(n)01123581321345589144

Tavşan çoğalması problemiyle tanınır (Leonardo Fibonacci, 1202). Ardışık terimlerin oranı altın orana (≈ 1,618) yaklaşır: 144 / 89 ≈ 1,618.

Seriler · Adım adım kod izleme

fibonacci(4): Çağrı Ağacı ve Yığın

fib(4)?fib(3)fib(2)fib(1)fib(0)fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(4): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(3) çağrılır.

fib(4)?fib(3)?fib(2)fib(1)fib(0)fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(3): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(2) çağrılır.

fib(4)?fib(3)?fib(2)?fib(1)fib(0)fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(2): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(1) çağrılır.

fib(4)?fib(3)?fib(2)?fib(1)= 1fib(0)fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(1): n <= 1 → taban durum, 1 döner.

fib(4)?fib(3)?fib(2)1 + ?fib(1)= 1fib(0)fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

Sol terim 1 geldi. Şimdi sağ terim: fibonacci(0) çağrılır.

fib(4)?fib(3)?fib(2)1 + ?fib(1)= 1fib(0)= 0fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(0): n <= 1 → taban durum, 0 döner.

fib(4)?fib(3)?fib(2)= 1fib(1)= 1fib(0)= 0fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

1 + 0 = 1 döner.

fib(4)?fib(3)1 + ?fib(2)= 1fib(1)= 1fib(0)= 0fib(1)fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

Sol terim 1 geldi. Şimdi sağ terim: fibonacci(1) çağrılır.

fib(4)?fib(3)1 + ?fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(1): n <= 1 → taban durum, 1 döner.

fib(4)?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

1 + 1 = 2 döner.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

Sol terim 2 geldi. Şimdi sağ terim: fibonacci(2) çağrılır.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)?fib(1)fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(2): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(1) çağrılır.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)?fib(1)= 1fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(1): n <= 1 → taban durum, 1 döner.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)1 + ?fib(1)= 1fib(0)şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

Sol terim 1 geldi. Şimdi sağ terim: fibonacci(0) çağrılır.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)1 + ?fib(1)= 1fib(0)= 0şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

fibonacci(0): n <= 1 → taban durum, 0 döner.

fib(4)2 + ?fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)= 1fib(1)= 1fib(0)= 0şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

1 + 0 = 1 döner.

fib(4)= 3fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)= 1fib(1)= 1fib(0)= 0şimdi çalışanyığında, bekliyordeğer döndürdühenüz çağrılmadı

2 + 1 = 3 döner. Toplam 9 çağrı; fibonacci(2) iki kez hesaplandı.

static int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

Seriler

Lucas Serisi

  • 2 ile başlar, ardından 1 gelir: L(0) = 2, L(1) = 1.
  • Genel formül Fibonacci ile aynı: L(n) = L(n − 1) + L(n − 2).
  • Yalnızca başlangıç değerleri farklıdır → iki ayrı taban durum gerekir.
int lucas(int n) {
    if (n == 0) {
        return 2;
    } else if (n == 1) {
        return 1;
    } else {
        return lucas(n - 1) + lucas(n - 2);
    }
}
n012345
L(n)2134711
n678910
L(n)18294776123

İki seri birbirine bağlıdır: L(n) = F(n − 1) + F(n + 1). Örn. L(5) = F(4) + F(6) = 3 + 8 = 11.

Seriler

Catalan Serisi

  • 1 ile başlar: C(0) = 1, C(1) = 1.
  • Kapalı formül: C(n) = (2n)! / ((n + 1)! · n!).
  • Kodun kullandığı özyinelemeli formül: C(n) = Σ C(i) · C(n − 1 − i), i = 0 … n − 1.
  • Özyineleme bir döngünün içinde yapılır: her C(n), daha küçük tüm C değerlerinden kurulur.
long catalan(int n) {
    if (n <= 1) {
        return 1;
    }
    long sonuc = 0;
    for (int i = 0; i < n; i++) {
        sonuc += catalan(i) * catalan(n - i - 1);
    }
    return sonuc;
}

Değerler:

112514421324291430

Ne sayar? n çift parantezin doğru dizilişlerini. n = 3 → C(3) = 5:

((()))(()())(())()()(())()()()

Ayrıca n + 1 sayının çarpım sırasını belirleme ve n düğümlü ikili ağaç sayısı.

Seriler

Bell Serisi

  • 1 ile başlar: B(0) = 1, B(1) = 1.
  • Genel formül: B(n) = Σ C(n − 1, k) · B(k), k = 0 … n − 1; burada C(n, k) kombinasyondur.
int bell(int n) {
    if (n == 0) { return 1; }
    int sayi = 0;
    for (int k = 0; k < n; k++) {
        sayi += katsayi(n - 1, k) * bell(k);
    }
    return sayi;
}

int katsayi(int n, int k) {       // C(n, k)
    return faktoriyel(n) / (faktoriyel(k) * faktoriyel(n - k));
}

Değerler:

11251552203877

Ne sayar? n elemanlı bir kümeyi gruplara ayırma yollarını. {a, b, c} → B(3) = 5:

abca|bcb|acc|aba|b|c
katsayi, int döndüren faktoriyel'i kullanır; 13! taşar. Bu yüzden kod yalnızca n ≤ 13 için doğrudur.

Özyinelemeli Metot Yazma

İki Temel Kural

Kural 1: Taban durum (Base Case)

Sonsuz özyinelemeye girmemek için en az bir taban duruma ihtiyaç vardır. Örneğin n sayısı 0 veya 1 olduğunda metot kendini çağırmadan sonlanmalıdır.

Kural 2: İlerleme sağlama

Her özyinelemeli çağrı taban duruma doğru ilerlemelidir: parametre her çağrıda azaltılır (ya da artırılır), dizgi kısalır, aralık daralır.

faktoriyel(4)faktoriyel(3)faktoriyel(2)faktoriyel(1)her çağrıda n − 1: ilerlemetaban durum: çağrı yok
  • Sonsuz özyineleme (taban durum yok ya da hiç ulaşılamıyor) ve yetersiz ilerleme (parametre değişmiyor) en sık iki hatadır.
  • Her özyinelemeli çağrı problemin daha küçük bir alt kümesine odaklanmalıdır.
  • Taban durum her zaman özyinelemeli çağrıdan önce sınanır.
  • Tüm olası girdiler hesaba katılır: negatif sayı, 0, boş dizgi taban duruma ulaşıyor mu?

Özyinelemeli Metot Yazma

Sonsuz Özyineleme

public class Toplam {
    static int toplam(int n) {
        if (n == 1) {
            return 1;
        } else {
            return n + toplam(n - 1);   // satır 6
        }
    }
    public static void main(String[] args) {
        System.out.println(toplam(0));
    }
}
Exception in thread "main" java.lang.StackOverflowError
	at Toplam.toplam(Toplam.java:6)
	at Toplam.toplam(Toplam.java:6)
	at Toplam.toplam(Toplam.java:6)
	...

toplam(0): n = 0, −1, −2, … hiçbir zaman 1 olmaz. Her çağrı yeni bir çerçeve açar; yaklaşık on binlerce çağrıdan sonra yığın dolar ve JVM programı durdurur.

Derleyici bu hatayı yakalamaz; çalışma zamanında ortaya çıkar. Yığın izindeki (stack trace) aynı satırın tekrar tekrar görünmesi sonsuz özyinelemenin işaretidir.

Çözüm: taban durumu genişletmek (n <= 1), girdiyi denetlemek ya da derinliği çok büyük olan problemlerde döngüye geçmek.

Özyinelemeli Metot Yazma · Adım adım kod izleme

toplam(0): Yığın Nasıl Dolar?

yığın sınırımainmain

main, toplam(0)'ı çağırır. Taban durum yalnızca n == 1.

yığın sınırımaintoplam(0)n = 0

n = 0: 0 == 1 yanlış → 0 + toplam(-1) hesaplanmalı; yeni çağrı.

yığın sınırımaintoplam(0)toplam(-1)n = -1

n = -1: yine 1 değil. n her çağrıda uzaklaşıyor: taban duruma hiç ulaşılmayacak.

yığın sınırımaintoplam(0)toplam(-1)toplam(-2)n = -2

Her çağrı yığına yeni bir çerçeve ekler; hiçbiri dönemez, çünkü hepsi bir sonrakinin sonucunu bekler.

yığın sınırı1001 çerçeven = -999

Bin çağrı sonra yığında 1001 çerçeve var; hâlâ hata yok.

yığın sınırı≈ 10 200 çerçevetaşma

Yığın için ayrılan bellek doldu: JVM StackOverflowError fırlatır. Bu makinede ölçülen derinlik ≈ 10 200 çağrıdır; sayı JVM ayarına göre değişir.

public class Toplam {
    static int toplam(int n) {
        if (n == 1) {
            return 1;
        } else {
            return n + toplam(n - 1);   // satır 6
        }
    }
    public static void main(String[] args) {
        System.out.println(toplam(0));
    }
}

Özyinelemeli Metot Yazma

Özyinelemeli Düşünme

  1. Küçültme: problem, kendisinin daha küçük bir kopyası cinsinden ifade edilir.
  2. Taban durum: cevabı doğrudan bilinen en küçük girdi bulunur.
  3. Güven: küçük kopyanın doğru çözüldüğü varsayılır (inanç sıçraması, leap of faith); çağrının içi zihinde açılmaz.
  4. Birleştirme: küçük cevaptan büyük cevap kurulur.

Örnek: bir sayının basamak sayısı.

  • 48729'un basamak sayısı = 1 + 4872'nin basamak sayısı.
  • Tek basamaklı sayı (n < 10) → 1.
int basamakSayisi(int n) {
    if (n < 10) {                       // taban durum
        return 1;
    }
    return 1 + basamakSayisi(n / 10);   // küçült + birleştir
}

basamakSayisi(48729) → 5 · basamakSayisi(7) → 1

Örnekler

Rakamlar Toplamı

int rakamlarToplami(int n) {
    if (n < 0) {
        return rakamlarToplami(-n);
    } else if (n < 10) {
        return n;
    } else {
        return rakamlarToplami(n / 10) + (n % 10);
    }
}
  • Negatif sayı önce pozitife çevrilir (satır 2–3).
  • Tek basamaklı sayı kendisidir: taban durum (satır 4–5).
  • Aksi halde: son basamak + kalan basamakların toplamı.
n = 48729n / 10 = 4872n % 10 = 9özyinelemeson basamak

n / 10 tam sayı bölmesi son basamağı atar; n % 10 son basamağı verir. Her çağrıda sayı bir basamak kısalır → ilerleme garantilidir.

Örnekler

rakamlarToplami(-48729) Açılımı

rakamlarToplami(-48729)
  = rakamlarToplami(48729)
  = rakamlarToplami(4872) + 9
  = (rakamlarToplami(487) + 2) + 9
  = ((rakamlarToplami(48) + 7) + 2) + 9
  = (((rakamlarToplami(4) + 8) + 7) + 2) + 9
  = (((4 + 8) + 7) + 2) + 9
  = ((12 + 7) + 2) + 9
  = (19 + 2) + 9
  = 21 + 9
  = 30

İlk çağrı işareti düzeltir; sonra her adımda son basamak (9, 2, 7, 8) bekleyen toplama olarak dışarıda kalır.

Taban durum rakamlarToplami(4) → 4. Dönüşte toplamlar içten dışa yapılır: 4 + 8 + 7 + 2 + 9 = 30.

Toplam 6 çağrı yapılır (1 işaret + 5 basamak).

Örnekler · Adım adım kod izleme

rakamlarToplami(-357): Kodun Adım Adım İzlenmesi

r(-357)r(357)r(35)r(3)

main, rakamlarToplami(-357)'yi çağırır (şekilde kısaca r).

r(-357)n < 0r(357)r(35)r(3)

n = -357: negatif → ilk dal. İşaret düzeltilip r(357) çağrılır; bu çerçeve yalnızca sonucu aynen döndürecek.

r(-357)→ r(357)r(357)r(35) + 7r(35)r(3)

n = 357: 10'dan büyük. 357 / 10 = 35 ile çağrı yapılır; son basamak 7 bekleyen toplama olarak kalır.

r(-357)→ r(357)r(357)r(35) + 7r(35)r(3) + 5r(3)

n = 35: r(3) çağrılır, 5 bekler.

r(-357)→ r(357)r(357)r(35) + 7r(35)r(3) + 5r(3)return 3

n = 3 < 10: taban durum, 3 döner. Artık çerçeveler geri dönmeye başlar.

r(-357)→ r(357)r(357)r(35) + 7r(35)3 + 5 = 8r(3)= 3

r(35) 3'ü aldı: 3 + 5 = 8 döner.

r(-357)→ r(357)r(357)8 + 7 = 15r(35)= 8r(3)= 3

r(357) 8'i aldı: 8 + 7 = 15 döner.

r(-357)= 15r(357)= 15r(35)= 8r(3)= 3

r(-357) gelen 15'i değiştirmeden döndürür.

r(-357)= 15r(357)= 15r(35)= 8r(3)= 3

15 yazdırılır: 3 + 5 + 7 = 15. Dört çağrı yapıldı (1 işaret + 3 basamak).

static int rakamlarToplami(int n) {
    if (n < 0) {
        return rakamlarToplami(-n);
    } else if (n < 10) {
        return n;
    } else {
        return rakamlarToplami(n / 10) + (n % 10);
    }
}

public static void main(String[] args) {
    System.out.println(rakamlarToplami(-357));
}

Örnekler

Palindrom Kontrolü

static boolean palindromMu(String s) {
    if (s.length() <= 1) {
        return true;
    } else if (s.charAt(0) == s.charAt(s.length() - 1)) {
        return palindromMu(s.substring(1, s.length() - 1));
    }
    return false;
}

palindromMu("kayak") → true · palindromMu("abba") → true · palindromMu("java") → false

kayakdıştan içe: k = k, a = a; ortada y kalır
  • Palindrom: tersten okunuşu aynı olan dizgi (kayak, ey edip adanada pide ye).
  • s.charAt(i): i. karakter (0'dan başlar).
  • s.substring(1, s.length() - 1): ilk ve son harfi atılmış dizgi.
  • İki taban durum: uzunluk ≤ 1 → true; uçlar farklı → false.
  • Büyük-küçük harfe duyarlıdır: "Kayak" → false.

Örnekler · Adım adım kod izleme

palindromMu("racecar"): Kodun Adım Adım İzlenmesi

r0a1c2e3c4a5r6s = "racecar" (uzunluk 7)

s = "racecar": uzunluk 7 > 1. İlk harf 'r' ile son harf 'r' karşılaştırılır → eşit.

r0a1c2e3c4a5r6s = "racecar" (uzunluk 7)

Uçlar eşleşti; iç kısım s.substring(1, 6) = "aceca" için yeni çağrı yapılır.

r0a1c2e3c4a5r6s = "aceca" (uzunluk 5)

s = "aceca": uzunluk 5 > 1. İlk harf 'a' ile son harf 'a' karşılaştırılır → eşit.

r0a1c2e3c4a5r6s = "aceca" (uzunluk 5)

Uçlar eşleşti; iç kısım s.substring(1, 4) = "cec" için yeni çağrı yapılır.

r0a1c2e3c4a5r6s = "cec" (uzunluk 3)

s = "cec": uzunluk 3 > 1. İlk harf 'c' ile son harf 'c' karşılaştırılır → eşit.

r0a1c2e3c4a5r6s = "cec" (uzunluk 3)

Uçlar eşleşti; iç kısım s.substring(1, 2) = "e" için yeni çağrı yapılır.

r0a1c2e3c4a5r6s = "e" (uzunluk 1)

Uzunluk 1 ≤ 1 → taban durum: tek harfli dizgi palindromdur, true döner.

r0a1c2e3c4a5r6s = "cec" (uzunluk 3)

palindromMu("cec") çerçevesi true aldı ve aynen true döndürür.

r0a1c2e3c4a5r6s = "aceca" (uzunluk 5)

palindromMu("aceca") çerçevesi true aldı ve aynen true döndürür.

r0a1c2e3c4a5r6s = "racecar" (uzunluk 7)

palindromMu("racecar") çerçevesi true aldı ve aynen true döndürür. Sonuç: "racecar" bir palindromdur.

static boolean palindromMu(String s) {
    if (s.length() <= 1) {
        return true;
    } else if (s.charAt(0) == s.charAt(s.length() - 1)) {
        return palindromMu(s.substring(1, s.length() - 1));
    }
    return false;
}

Örnekler

En Büyük Ortak Bölen

static int gcd(int x, int y) {
    if (y == 0) {
        return x;
    }
    return gcd(y, x % y);
}
  • Öklid algoritması: EBOB(x, y) = EBOB(y, x mod y).
  • Taban durum: y = 0 ise EBOB = x.
  • Kalan her adımda küçülür → mutlaka 0'a ulaşır.
  • gcd(93, 444): ilk adım gcd(444, 93) olur; sıra önemsizdir.
gcd(444, 93)
  = gcd(93, 72)     // 444 % 93 = 72
  = gcd(72, 21)     // 93 % 72 = 21
  = gcd(21, 9)      // 72 % 21 = 9
  = gcd(9, 3)       // 21 % 9 = 3
  = gcd(3, 0)       // 9 % 3 = 0
  = 3

gcd (greatest common divisor) = EBOB. gcd(48, 18) → 6.

Örnekler · Adım adım kod izleme

gcd(444, 93): Kodun Adım Adım İzlenmesi

çağrıxyx % y
gcd(444, 93)4449372

y = 93 ≠ 0. Kalan 444 % 93 = 72; yeni çağrı gcd(93, 72).

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221

y = 72 ≠ 0. Kalan 93 % 72 = 21; yeni çağrı gcd(72, 21).

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221
gcd(72, 21)72219

y = 21 ≠ 0. Kalan 72 % 21 = 9; yeni çağrı gcd(21, 9).

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221
gcd(72, 21)72219
gcd(21, 9)2193

y = 9 ≠ 0. Kalan 21 % 9 = 3; yeni çağrı gcd(9, 3).

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221
gcd(72, 21)72219
gcd(21, 9)2193
gcd(9, 3)930

y = 3 ≠ 0. Kalan 9 % 3 = 0; yeni çağrı gcd(3, 0).

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221
gcd(72, 21)72219
gcd(21, 9)2193
gcd(9, 3)930
gcd(3, 0)30—

y == 0 → taban durum: x = 3 döner. EBOB(444, 93) = 3.

çağrıxyx % y
gcd(444, 93)4449372
gcd(93, 72)937221
gcd(72, 21)72219
gcd(21, 9)2193
gcd(9, 3)930
gcd(3, 0)30—

Son çağrıdaki return gcd(…) ifadesinden sonra yapılacak iş yoktur: 3 değeri her çerçeveden değişmeden geri taşınır (5 dönüş). main, sonuç olarak 3 alır.

static int gcd(int x, int y) {
    if (y == 0) {
        return x;
    }
    return gcd(y, x % y);
}

Örnekler

Hanoi Kuleleri

Üç çubuk (A, B, C) ve A'da büyükten küçüğe dizili n disk var. Hepsi C'ye taşınacak:

  • Her hamlede yalnızca bir disk, en üstteki taşınır.
  • Büyük disk asla küçük diskin üstüne konmaz.

Özyinelemeli fikir: üstteki n − 1 disk yardımcıya, en büyük disk hedefe, sonra n − 1 disk yardımcıdan hedefe taşınır.

Hamle sayısı 2ⁿ − 1: 3 disk → 7, 10 disk → 1023, 64 disk → yaklaşık 1,8 × 10¹⁹.

public class HanoiKuleleri {
    static void coz(int n, char kaynak,
                    char hedef, char yardimci) {
        if (n == 1) {   // tek disk: doğrudan taşı
            System.out.println(n + ": " + kaynak + "->" + hedef);
        } else {
            coz(n - 1, kaynak, yardimci, hedef);
            System.out.println(n + ": " + kaynak + "->" + hedef);
            coz(n - 1, yardimci, hedef, kaynak);
        }
    }
    public static void main(String[] args) {
        coz(3, 'A', 'C', 'B');
    }
}

Döngüyle yazması zor olan bu problem, özyinelemeyle birkaç satırdır.

Örnekler · Adım adım kod izleme

Hanoi Kuleleri: coz(3, 'A', 'C', 'B')

ABC321

Başlangıç: 3 disk A'da. coz(3, 'A', 'C', 'B'): üç disk, B yardımcı olarak kullanılıp C'ye taşınacak.

ABC321

En derindeki coz(1, A, C, B): taban durum, 1. disk doğrudan A'dan C'ye.

ABC321

coz(2, A, B, C): üstündeki 1 disk kalktı; 2. disk A'dan B'ye.

ABC321

2. diskin üstüne 1. disk: coz(1, C, B, A). Üstteki 2 disk artık B'de.

ABC213

coz(3, …) ortadaki satıra geldi: en büyük disk (3) A'dan C'ye.

ABC123

Şimdi B'deki 2 disk C'ye taşınacak: coz(2, B, C, A) → önce 1. disk B'den A'ya.

ABC132

2. disk B'den C'ye, 3. diskin üstüne.

ABC321

Son hamle: 1. disk A'dan C'ye. 2³ − 1 = 7 hamle; bulmaca çözüldü.

static void coz(int n, char kaynak,
                char hedef, char yardimci) {
    if (n == 1) {
        System.out.println(n + ": " + kaynak + "->" + hedef);
    } else {
        coz(n - 1, kaynak, yardimci, hedef);
        System.out.println(n + ": " + kaynak + "->" + hedef);
        coz(n - 1, yardimci, hedef, kaynak);
    }
}

Verimlilik

Verimlilik (Efficiency)

  • Özyinelemeli metotlar bazen yavaş çalışabilir: her çağrının bir maliyeti (çerçeve açma, parametre kopyalama) vardır.
  • Özyinelemeli çağrılar yapıldıkça sistem, bekleyen kısmi sonuçları çağrı yığınında biriktirir.
  • Yığın aşırı dolarsa program StackOverflowError (yığın taşması) ile sonlanır.
  • Sorunlu durumlarda çözüm, döngülerle çalışan ilerlemeli (iterative) çözümlerdir.
  • İlerlemeli çözümler kısmi sonuçları biriktirmez: tek bir çerçeve ve birkaç değişken yeter.
Bellekfaktoriyel(n) özyinelemeli: n çerçeve, O(n) bellek. Döngüyle: O(1).
Derinlik sınırıVarsayılan yığınla birkaç on bin çağrı derinliğinden sonra StackOverflowError alınır.
TekrarAğaç özyinelemesi (Fibonacci) aynı değerleri defalarca hesaplayabilir.

Verimlilik

Tekrarlanan Hesaplama

fib(5)fib(4)fib(3)fib(2)fib(1)fib(0)fib(1)fib(2)fib(1)fib(0)fib(3)fib(2)fib(1)fib(0)fib(1)

fibonacci(5): 15 çağrı. Kırmızı düğümler daha önce hesaplanmış değerlerin yeniden hesaplanmasıdır.

nçağrı sayısıF(n)
1017755
2021 8916 765
302 692 537832 040
40331 160 281102 334 155

n 1 artınca çağrı sayısı ≈ 1,6 katına çıkar: üstel büyüme. fibonacci(40) yarım saniye sürer; fibonacci(50) dakikalar alır.

Verimlilik

Faktöriyel: Döngü ile

Özyinelemeli

int faktoriyel(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * faktoriyel(n - 1);
    }
}

İlerlemeli (döngü)

int faktoriyel(int n) {
    int carpim = 1;
    for (int i = 1; i <= n; i++) {
        carpim *= i;
    }
    return carpim;
}
faktoriyel(5)faktoriyel(4)faktoriyel(3)faktoriyel(2)faktoriyel(1)özyinelemeli: 5 çerçevecarpim: 1→1→2→6→24→120döngü: tek çerçeve
Aynı sonuç (faktoriyel(5) → 120). Döngülü sürüm tek çerçevede, sabit bellekle çalışır; çarpım carpim değişkeninde biriktirilir.

Verimlilik

Rakamlar Toplamı ve Palindrom: Döngü ile

Rakamlar toplamı

int rakamlarToplami(int n) {
    int toplam = 0;
    int kalan = Math.abs(n);
    while (kalan > 0) {
        toplam += kalan % 10;
        kalan /= 10;
    }
    return toplam;
}

rakamlarToplami(-48729) → 30: özyinelemeli sürümle aynı. Rakamların toplamı negatif olamaz; işaret yalnızca Math.abs ile atılır.

Palindrom

boolean palindromMu(String s) {
    s = s.toLowerCase();
    int uzunluk = s.length();
    for (int i = 0; i < uzunluk / 2; i++) {
        if (s.charAt(i) != s.charAt(uzunluk - 1 - i)) {
            return false;
        }
    }
    return true;
}

i baştan, uzunluk − 1 − i sondan ilerler. toLowerCase() sayesinde "Kayak" → true.

Verimlilik · Adım adım kod izleme

Döngü ile Palindrom: Adım Adım

palindromMu("Kepek")

main, palindromMu("Kepek")'i çağırır.

K0e1p2e3k4

Parametre s = "Kepek": ilk harf büyük.

k0e1p2e3k4

toLowerCase() yeni bir dizgi döndürür: s = "kepek". Artık K ile k farkı yok.

k0e1p2e3k4

uzunluk = 5; döngü i < 5 / 2 = 2 olduğu sürece döner.

k0e1p2e3k4iuzunluk − 1 − i

i = 0: baştan 0., sondan 5 − 1 − 0 = 4. indis karşılaştırılacak.

k0e1p2e3k4iuzunluk − 1 − i'k' == 'k'

'k' == 'k': eşit, return false çalışmaz.

k0e1p2e3k4iuzunluk − 1 − i

i = 1: 1. ve 3. indis.

k0e1p2e3k4iuzunluk − 1 − i'e' == 'e'

'e' == 'e': eşit.

k0e1p2e3k4

i = 2: 2 < 2 yanlış, döngü biter. Ortadaki 'p' kendisiyle eşleşir, bakmaya gerek yok.

k0e1p2e3k4

Hiç farklı çift bulunmadı: true döner.

palindromMu("Kepek")

true yazdırılır. Tek çerçeve kullanıldı; özyinelemeli sürümdeki gibi her adımda yeni dizgi de üretilmedi.

static boolean palindromMu(String s) {
    s = s.toLowerCase();
    int uzunluk = s.length();
    for (int i = 0; i < uzunluk / 2; i++) {
        if (s.charAt(i) != s.charAt(uzunluk - 1 - i)) {
            return false;
        }
    }
    return true;
}

public static void main(String[] args) {
    System.out.println(palindromMu("Kepek"));
}

Verimlilik

Özyineleme mi Döngü mü?

ÖzyinelemeDöngü (ilerlemeli)
DurmaTaban durumDöngü koşulu
İlerlemeDaha küçük argümanla çağrıSayaç / değişken güncelleme
BellekHer çağrı için bir çerçeve: O(derinlik)Genellikle O(1)
HızÇağrı maliyeti; ağaç özyinelemesinde tekrarGenellikle daha hızlı
HataTaban durum yoksa StackOverflowErrorKoşul bozuksa sonsuz döngü (program donar)
OkunabilirlikAğaç, böl ve fethet, Hanoi, seriler: çok doğalBasit sayma ve biriktirme işleri için doğal
Her özyinelemeli çözüm döngüyle (gerekirse kendi yığınını tutarak) yazılabilir; tersi de doğrudur. Seçim; açıklık, derinlik ve performansa göre yapılır.

Kuyruk Özyinelemesi

Kuyruk Özyinelemesi (Tail Recursion)

  • Metodun son işlemi özyinelemeli çağrının kendisidir; dönüşte yapılacak iş kalmaz.
  • Sonuç, ek bir biriktirici (accumulator) parametrede taşınır.
  • Bu biçim, derleyicinin kuyruk çağrısı optimizasyonu (TCO) yapmasını mümkün kılar: çağrı bir döngü gibi işlenir, çerçeveler yığılmaz → bellek verimliliği.
  • Java (JVM) bu optimizasyonu yapmaz: kuyruk özyinelemeli Java kodu yine O(n) yığın kullanır. Scheme, Scala (@tailrec), Kotlin (tailrec) gibi dillerde yapılır.

Kuyruk özyinelemeli değil

return n * faktoriyel(n - 1);
// çağrı döndükten SONRA çarpma var

Kuyruk özyinelemeli

return faktoriyel(n - 1, n * birikim);
// çarpma çağrıdan ÖNCE; son iş çağrı
n * faktoriyel(n - 1)faktoriyel(n - 1, n * b)4 * ?(4, 1)3 * ?(3, 4)2 * ?(2, 12)1(1, 24)dönüşte çarpım beklerbekleyen iş yok

Kuyruk Özyinelemesi

Kuyruk Özyinelemeli Örnekler

int rakamlarToplami(int sayi, int toplam) {
    if (sayi == 0) {
        return toplam;
    } else {
        return rakamlarToplami(sayi / 10,
                               toplam + sayi % 10);
    }
}

Rakamlar toplamı: son basamak, çağrıdan önce toplam'a eklenir. rakamlarToplami(48729, 0) → 30 (n ≥ 0 için).

int faktoriyel(int n, int birikim) {
    if (n == 0) {
        return birikim;
    } else {
        return faktoriyel(n - 1, n * birikim);
    }
}

Faktöriyel: çarpım, çağrıdan önce birikim'e katılır. faktoriyel(5, 1) → 120.

Biriktiricinin başlangıç değeri işlemin etkisiz elemanıdır: toplama için 0, çarpma için 1. Kullanıcıyı bundan kurtarmak için sarmalayıcı metot yazılır: int faktoriyel(int n) { return faktoriyel(n, 1); }

Kuyruk Özyinelemesi · Adım adım kod izleme

faktoriyel(4, 1): Kuyruk Özyinelemesinin İzlenmesi

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)faktoriyel(2, 12)faktoriyel(1, 24)faktoriyel(0, 24)

Çarpım çağrıdan önce yapılır: 4 * 1 = 4 yeni birikim olur; çağrı faktoriyel(3, 4).

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)faktoriyel(1, 24)faktoriyel(0, 24)

Çarpım çağrıdan önce yapılır: 3 * 4 = 12 yeni birikim olur; çağrı faktoriyel(2, 12).

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)son iş: çağrıfaktoriyel(1, 24)faktoriyel(0, 24)

Çarpım çağrıdan önce yapılır: 2 * 12 = 24 yeni birikim olur; çağrı faktoriyel(1, 24).

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)son iş: çağrıfaktoriyel(1, 24)son iş: çağrıfaktoriyel(0, 24)

Çarpım çağrıdan önce yapılır: 1 * 24 = 24 yeni birikim olur; çağrı faktoriyel(0, 24).

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)son iş: çağrıfaktoriyel(1, 24)son iş: çağrıfaktoriyel(0, 24)return 24

n == 0: taban durum. Sonuç zaten birikim'de hazır: 24 döner.

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)son iş: çağrıfaktoriyel(1, 24)= 24faktoriyel(0, 24)= 24

Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)son iş: çağrıfaktoriyel(2, 12)= 24faktoriyel(1, 24)= 24faktoriyel(0, 24)= 24

Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.

faktoriyel(4, 1)son iş: çağrıfaktoriyel(3, 4)= 24faktoriyel(2, 12)= 24faktoriyel(1, 24)= 24faktoriyel(0, 24)= 24

Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.

faktoriyel(4, 1)= 24faktoriyel(3, 4)= 24faktoriyel(2, 12)= 24faktoriyel(1, 24)= 24faktoriyel(0, 24)= 24

Sonuç 24. Java kuyruk çağrısını optimize etmez: 5 çerçeve yine de yığına girdi. TCO yapan dillerde tek çerçeve yeterdi.

static int faktoriyel(int n, int birikim) {
    if (n == 0) {
        return birikim;
    } else {
        return faktoriyel(n - 1, n * birikim);
    }
}

Bellekleme

Bellekleme (Memoization)

  • Tekrarlanan hesaplamaların sonuçlarını saklayıp yeniden kullanma yöntemidir (durum bilgisi taşıma).
  • Sonuçlar bir önbelleğe (Map) alınır; aynı argüman tekrar gelince hesaplamadan döndürülür.
  • Her n için hesap bir kez yapılır: üstel çağrı sayısı doğrusala iner.
  • Map<Integer, Long>: anahtar n, değer F(n).

fibonacci(90) → 2880067194370816120 (anında).

import java.util.HashMap;
import java.util.Map;

static Map<Integer, Long> hafiza = new HashMap<>();

static long fibonacci(int n) {
    if (hafiza.containsKey(n)) {
        return hafiza.get(n);    // daha önce hesaplandı
    }
    if (n == 0 || n == 1) {
        return n;
    }
    long sonuc = fibonacci(n - 1) + fibonacci(n - 2);
    hafiza.put(n, sonuc);        // sonucu sakla
    return sonuc;
}

Bellekleme · Adım adım kod izleme

Belleklemeli fibonacci(4): Adım Adım

fib(4)çağrı yaparhafiza2 → ?3 → ?4 → ?

fibonacci(4) çağrıldı. Bellek boş: 4 orada yok, taban durum da değil → önce fibonacci(3) çağrılır.

fib(4)fib(3)çağrı yaparhafiza2 → ?3 → ?4 → ?

fibonacci(3): bellekte yok → fibonacci(2) çağrılır.

fib(4)fib(3)fib(2)çağrı yaparhafiza2 → ?3 → ?4 → ?

fibonacci(2): bellekte yok → fibonacci(1) çağrılır.

fib(4)fib(3)fib(2)fib(1)temel: 1hafiza2 → ?3 → ?4 → ?

n = 1: taban durum, 1 döner.

fib(4)fib(3)fib(2)fib(0)temel: 0hafiza2 → ?3 → ?4 → ?

n = 0: taban durum, 0 döner.

fib(4)fib(3)fib(2)= 1hafiza2 → 13 → ?4 → ?

fibonacci(2) = 1 + 0 = 1. Sonuç belleğe yazılır: {2=1}.

fib(4)fib(3)fib(1)temel: 1hafiza2 → 13 → ?4 → ?

fibonacci(3)'ün ikinci çağrısı fibonacci(1): taban durum, 1.

fib(4)fib(3)= 2hafiza2 → 13 → 24 → ?

fibonacci(3) = 1 + 1 = 2, belleğe yazılır.

fib(4)fib(2)bellekten: 1hafiza2 → 13 → 24 → ?

İkinci fibonacci(2) çağrısı: bellekte var, hesaplanmadan 1 döner (belleklemesiz sürüm burada 3 çağrı daha yapardı).

fib(4)= 3hafiza2 → 13 → 24 → 3

fibonacci(4) = 2 + 1 = 3, belleğe yazılır. Toplam 7 çağrı (belleklemesiz: 9).

static Map<Integer, Long> hafiza = new HashMap<>();
static long fibonacci(int n) {
    if (hafiza.containsKey(n)) {
        return hafiza.get(n);
    }
    if (n == 0 || n == 1) {
        return n;
    }
    long sonuc = fibonacci(n - 1) + fibonacci(n - 2);
    hafiza.put(n, sonuc);
    return sonuc;
}

Bellekleme

Belleklemeli Fibonacci: Çağrı Ağacı

fib(5)= 5fib(4)= 3fib(3)= 2fib(2)= 1fib(1)= 1fib(0)= 0fib(1)= 1fib(2)= 1fib(3)= 2hesaplandıbellekten okundu

Belleklemeli fibonacci(5): sol kol bir kez hesaplanır; sağdaki fib(2) ve fib(3) bellekten gelir. 15 yerine 9 çağrı.

nbelleklemesiz çağrıbelleklemeli çağrıbelleklemesiz süre (ölçülen)belleklemeli süre
2021 89139≈ 0,5 ms< 0,1 ms
40331 160 28179≈ 0,5 s< 1 ms

Belleklemeli sürümde çağrı sayısı 2n − 1'dir. Süreler makineye göre değişir; oran değişmez.

Bellekleme

Belleklemeli Faktöriyel

static Map<Integer, Long> hafiza = new HashMap<>();

static long faktoriyel(int n) {
    if (hafiza.containsKey(n)) {
        return hafiza.get(n);
    }
    if (n == 0 || n == 1) {
        return 1;
    }
    long sonuc = n * faktoriyel(n - 1);
    hafiza.put(n, sonuc);
    return sonuc;
}
  • Aynı kalıp: önce belleğe bak, yoksa hesapla, sakla, döndür.
  • long ile 20!'e kadar doğru: faktoriyel(20) = 2432902008176640000.
  • Tek bir faktoriyel(n) çağrısında tekrar yoktur; bellek bu çağrıyı hızlandırmaz.
  • Kazanç, program birçok kez faktöriyel istediğinde ortaya çıkar: faktoriyel(10)'dan sonra faktoriyel(12) yalnızca 2 yeni çarpma yapar.
  • Bedel: bellek. Bellekleme, hız için bellek harcamaktır.

Özet

Özet

KavramÖzü
ÖzyinelemeMetodun kendini daha küçük bir girdiyle çağırması
Taban durumDoğrudan çözülen en küçük durum; özyinelemeyi durdurur
İlerlemeHer çağrı taban duruma yaklaşmalı (n − 1, n / 10, substring, x % y)
Çağrı yığınıHer çağrıya bir çerçeve; dönüşte LIFO sırasıyla kapanır. Çok derin → StackOverflowError
Ağaç özyinelemesiBirden çok çağrı (Fibonacci, Hanoi); çağrı sayısı üstel büyüyebilir
İlerlemeli çözümDöngü + biriktirici değişken; sabit bellek, genellikle daha hızlı
Kuyruk özyinelemesiSon iş çağrıdır; sonuç biriktirici parametrede. Java TCO yapmaz
BelleklemeHesaplanan sonuçları Map'te saklayıp yeniden kullanmak

Özet

Sık Yapılan Hatalar

Taban durum yok / ulaşılamıyor

toplam(0) gibi: Exception in thread "main" java.lang.StackOverflowError.

İlerleme yok

return n * faktoriyel(n); parametre küçülmez; yine StackOverflowError.

Eksik return

if … else if ile biten metot: error: missing return statement. Çözüm: son dal else olur.

Çağrının sonucunu kullanmamak

faktoriyel(n - 1); tek başına yazılırsa değer kaybolur; return n * faktoriyel(n - 1); olmalı.

Taşma

int faktöriyel 13!'te, Bell katsayısı 13'ten sonra yanlışlaşır; hata mesajı yoktur. Çözüm: long.

Gereksiz ağaç özyinelemesi

Belleklemesiz fibonacci(50) dakikalar sürer. Çözüm: döngü ya da bellekleme.

Özet

Örnek Problemler (1)

static void yaz(int n) {
    if (n == 0) return;
    System.out.print(n + " ");
    yaz(n - 1);
}
static void yaz2(int n) {
    if (n == 0) return;
    yaz2(n - 1);
    System.out.print(n + " ");
}
static int gizem(int a, int b) {
    if (b == 0) return 0;
    return a + gizem(a, b - 1);
}
  1. yaz(3) ne yazdırır?
  2. yaz2(3) ne yazdırır? Fark nereden gelir?
  3. gizem(4, 3) kaçtır? Metot ne hesaplar?
  4. gizem(4, -1) çağrısında ne olur?
Cevapları göster
  1. 3 2 1: yazdırma çağrıdan önce yapılır (inişte).
  2. 1 2 3: yazdırma çağrıdan sonra, dönüşte yapılır; en derindeki çerçeve önce yazar.
  3. 4 + 4 + 4 + 0 = 12: a × b'yi toplama ile hesaplar.
  4. b = −1, −2, … hiç 0 olmaz → StackOverflowError.

Özet

Örnek Problemler (2)

static String ters(String s) {
    if (s.isEmpty()) return "";
    return ters(s.substring(1)) + s.charAt(0);
}
  1. ters("java") ne döndürür?
  2. ters kuyruk özyinelemeli midir? Ya gcd?
  3. fibonacci(5) kaç kez çağrılır (ilk çağrı dahil)?
  4. gcd(48, 18) çağrılarını yazın.
Cevapları göster
  1. "avaj": ilk harf, kalanın tersinin sonuna eklenir: ters("ava") + 'j'.
  2. ters değil: çağrı döndükten sonra birleştirme (+) yapılır. gcd evet: son iş çağrıdır.
  3. 15 çağrı (çağrı ağacındaki düğüm sayısı).
  4. gcd(48, 18) → gcd(18, 12) → gcd(12, 6) → gcd(6, 0) → 6.

Özet

Alıştırmalar

  1. long us(int taban, int us): tabanın üssünü özyinelemeyle hesaplayın. us(2, 10) = ?
  2. void ikili(int n): n'nin ikili (binary) gösterimini yazdırın. ikili(13) → ?
  3. int diziToplam(int[] a, int i): dizinin i. indisten sonuna kadar toplamı. {3, 1, 4, 1, 5} → ?
  4. Fibonacci'yi döngüyle ve kuyruk özyinelemesiyle (fibK(n, a, b)) yazın.
  5. lucas(5) kaç çağrı yapar? Bellekleme ile kaça iner?
  6. Hanoi kulelerinde 4 disk için kaç hamle gerekir? İlk hamle hangisidir?
Cevaplar / ipuçları
  1. if (us == 0) return 1; return taban * us(taban, us - 1); → 1024.
  2. if (n > 1) ikili(n / 2); System.out.print(n % 2); → 1101 (yazdırma dönüşte: önce yüksek basamaklar).
  3. if (i == a.length) return 0; return a[i] + diziToplam(a, i + 1); → 14.
  4. Döngü: a = 0, b = 1; n kez (a, b) ← (b, a + b). Kuyruk: n == 0 ? a : fibK(n - 1, b, a + b) → fibK(10, 0, 1) = 55.
  5. 15 çağrı (Fibonacci ile aynı ağaç biçimi); belleklemeli sürümde 9 (2n − 1).
  6. 2⁴ − 1 = 15 hamle. n çift olduğu için ilk hamle 1. diski yardımcı çubuğa (A → B) taşır.

Özet

Kaynaklar

  • Oracle, The Java Tutorials: Defining Methods; Returning a Value from a Method.
  • J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition (15.12 Method Invocation Expressions).
  • P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 18: Recursion).
  • A. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly (Bölüm 8: Recursive Methods).
  • C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson.
  • R. Sedgewick ve K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley (2.3 Recursion).

Son

Bölüm 3.5: Özyineleme
Sonraki bölüm: Bölüm 4.1: Nesneler ve Sınıflar · Terim sözlüğü
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.