Bölüm 4: Özyineleme
Giriş
Öğrenme Çıktıları
Bir problemi taban durum ve özyinelemeli adımla tanımlayıp Java'da yazmak.
Özyinelemeli bir metodu çerçeve çerçeve izleyip çıktısını ve dönüş değerini bulmak.
Doğrusal, ikili ve çoklu özyinelemeyi ve geri izlemeyi örnekleriyle ayırt etmek.
Yineleme bağıntısını yazıp çağrı ağacıyla zaman ve alan maliyetini bulmak; gerektiğinde döngüye çevirmek.
Özyineleme Nedir
Günlük Hayatta Özyineleme
Uzun bir kuyrukta kaçıncı sırada olduğunuzu öğrenmek istiyorsunuz ama yerinizden ayrılıp baştan sayamıyorsunuz.
Özyineleme Nedir
Özyineleme (Recursion)
- Özyineleme: bir metodun kendini daha küçük bir girdiyle çağırması.
- Taban durum (base case): özyinelemesiz, doğrudan çözülür. Olmazsa çağrılar bitmez →
StackOverflowError. - Özyinelemeli adım: problemi küçültür ve taban duruma yaklaştırır; küçük problemin cevabından büyüğünkini kurar.
- Her çağrı çağrı yığınında yeni bir çerçeve açar (kendi n'si).
Özyineleme Nedir
Özyinelemeli Düşünmek
- Taban durum: en küçük girdi hangisi ve cevabı ne? Burada özyineleme durur.
- Küçültme: problem, aynı türden daha küçük bir probleme nasıl indirgenir? (n − 1, n / 2, i + 1 …)
- Birleştirme: küçük problemin cevabından büyük problemin cevabı nasıl kurulur?
Küçük problemin doğru çözüldüğü kabul edilir ve yalnızca tek bir adım düşünülür. Taban durum doğruysa ve her adım doğruysa tüm zincir doğrudur (tümevarımdaki mantık).
void geriSay(int n) { if (n == 0) { // taban durum System.out.println("Bitti"); return; } System.out.println(n); geriSay(n - 1); // özyinelemeli adım } geriSay(3); // main içinde
geriSay(n): n'den 1'e kadar yaz, sonra “Bitti”.
Özyineleme Nedir · Kodu adım adım çalıştır
Kod İzleme: geriSay(3)
main, geriSay(3)'ü çağırır. Yığına yeni bir çerçeve eklenecek.
n = 3 ≠ 0: taban durum değil. 3 yazılır, sonra geriSay(2) çağrılır; bu çerçeve bekler.
n = 2 ≠ 0: taban durum değil. 2 yazılır, sonra geriSay(1) çağrılır; bu çerçeve bekler.
n = 1 ≠ 0: taban durum değil. 1 yazılır, sonra geriSay(0) çağrılır; bu çerçeve bekler.
n = 0: taban durum. “Bitti” yazılır. Yığın en derinde: main + 4 çerçeve.
return: geriSay(0) biter, çerçevesi yığından çıkar.
geriSay(1), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.
geriSay(2), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.
geriSay(3), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.
Tüm çerçeveler döndü. 4 çağrı yapıldı (n = 3, 2, 1, 0); çıktı: 3 2 1 Bitti.
void geriSay(int n) { if (n == 0) { // taban durum System.out.println("Bitti"); return; } System.out.println(n); geriSay(n - 1); // özyinelemeli adım } geriSay(3); // main içinde
Özyineleme Nedir
İş Çağrıdan Önce mi, Sonra mı?
void geriSay2(int n) { if (n == 0) return; System.out.print(n + " "); geriSay2(n - 1); // sonra çağrı }
geriSay2(3) çıktısı: 3 2 1
Yazdırma çağrılar inerken yapılır.
void ileriSay(int n) { if (n == 0) return; ileriSay(n - 1); // önce çağrı System.out.print(n + " "); }
ileriSay(3) çıktısı: 1 2 3
Yazdırma çağrılar dönerken yapılır.
ileriSay(3), 3'ü yazmadan önceileriSay(2)'nin bitmesini bekler; o da 2'yi yazmadan önceileriSay(1)'i bekler.- İlk biten çağrı en son başlayandır (n = 0). Bu yüzden çağrıdan sonra yapılan işler ters sırayla yapılır: 1, 2, 3.
Çağrı Yığını
Çağrı Yığını ve Çerçeveler
- Her metot çağrısı için bellekte bir çerçeve (frame) açılır: parametreler, yerel değişkenler ve dönüşte devam edilecek yer.
- Çerçeveler çağrı yığınında tutulur: çağrı yapılınca üste eklenir (push), metot bitince üstten çıkar (pop). Son başlayan ilk biter: LIFO.
- Yalnızca en üstteki çerçeve çalışır; alttakiler, üstlerindeki çağrının sonucunu bekler.
- Özyinelemede aynı metodun birden çok çerçevesi aynı anda yığında bulunur; her birinin n'si ayrıdır.
Çağrı Yığını · Kodu adım adım çalıştır
Kod İzleme: faktoriyel(4) ve çağrı yığını
main, faktoriyel(4)'ü çağırır.
Yeni çerçeve: n = 4. 4 ≤ 1 mi? Hayır.
4 · faktoriyel(3): çarpım için önce faktoriyel(3) bitmeli; bu çerçeve bekler.
Yeni çerçeve: n = 3. 3 ≤ 1 mi? Hayır.
3 · faktoriyel(2): çarpım için önce faktoriyel(2) bitmeli; bu çerçeve bekler.
Yeni çerçeve: n = 2. 2 ≤ 1 mi? Hayır.
2 · faktoriyel(1): çarpım için önce faktoriyel(1) bitmeli; bu çerçeve bekler.
Yeni çerçeve: n = 1. 1 ≤ 1 mi? Evet.
Taban durum: return 1. Artık çağrı yok; değerler geri dönmeye başlar.
faktoriyel(1) = 1 döndü → 2 · 1 = 2, return 2.
faktoriyel(2) = 2 döndü → 3 · 2 = 6, return 6.
faktoriyel(3) = 6 döndü → 4 · 6 = 24, return 24.
Ekrana 24. 4 çağrı, yığın derinliği en fazla 4 + main → zaman ve bellek O(n).
int faktoriyel(int n) { if (n <= 1) return 1; // taban durum return n * faktoriyel(n - 1); } // main içinde: System.out.println(faktoriyel(4));
Çağrı Yığını
Yığın Taşması: StackOverflowError
long topla(int n) { // 1 + 2 + … + n if (n == 0) return 0; return n + topla(n - 1); } // main içinde: System.out.println(topla(10_000)); // 50005000 System.out.println(topla(1_000_000)); // hata
Exception in thread "main"
java.lang.StackOverflowError
at Ornek.topla(Ornek.java:3)
at Ornek.topla(Ornek.java:3)
…- Çağrı yığını sınırlı bir bellek alanıdır. Boyutu JVM'e ve işletim sistemine bağlıdır;
-Xssseçeneğiyle değiştirilebilir. - Her çerçeve yer kaplar. Varsayılan ayarlarla derinlik genellikle birkaç bin ile birkaç on bin çağrıyla sınırlıdır.
topla(10_000)sığar;topla(1_000_000)taban duruma ulaşmadan yığını doldurur.- Aynı toplam bir döngüyle ya da n(n + 1)/2 formülüyle sorunsuz hesaplanır.
Çağrı Yığını
Taban Durumu Unutmak
int f(int n) { return n * f(n - 1); }n = 3, 2, 1, 0, −1, −2 … çağrılar hiç durmaz.
if (n == 1) return 1; return n * f(n - 1);
f(0) → −1 → −2 …: n hiçbir zaman 1 olmaz. n <= 1 bu girdiyi de yakalar.if (n <= 1) return 1; return n * f(n);Aynı girdiyle tekrar çağrı: taban durum var ama ona hiç yaklaşılmıyor.
StackOverflowError oluşur. Her olası girdi (0 ve negatif değerler dahil) sonlu sayıda adımda bir taban duruma ulaşmalıdır.Doğrusal Özyineleme
Doğrusal Özyineleme
- Her çağrı en fazla bir özyinelemeli çağrı yapar; çağrılar dallanmayan bir zincir oluşturur.
- n boyutlu problem, n − 1 (ya da n / 2) boyutlu tek bir probleme indirgenir.
- Örnekler: faktoriyel, geriSay, dizinin toplamı, diziyi tersine çevirme, üs alma, bağlı listenin uzunluğu.
- Dizinin toplamı: ilk n öğenin toplamı = ilk n − 1 öğenin toplamı + a[n − 1]. Taban durum: n = 0 (boş parça) → 0.
int toplam(int[] a, int n) { // ilk n öğenin toplamı if (n == 0) return 0; // taban durum return toplam(a, n - 1) + a[n - 1]; }
Doğrusal Özyineleme · Kodu adım adım çalıştır
Kod İzleme: toplam(a, 4)
main, toplam(a, 4)'ü çağırır: dizinin ilk 4 öğesinin toplamı.
n = 4 ≠ 0 → önce toplam(a, 3) hesaplanmalı; a[3] = 2 toplamaya eklenmeyi bekler.
n = 3 ≠ 0 → önce toplam(a, 2) hesaplanmalı; a[2] = 6 toplamaya eklenmeyi bekler.
n = 2 ≠ 0 → önce toplam(a, 1) hesaplanmalı; a[1] = 3 toplamaya eklenmeyi bekler.
n = 1 ≠ 0 → önce toplam(a, 0) hesaplanmalı; a[0] = 4 toplamaya eklenmeyi bekler.
n = 0: taban durum → 0 döner. Yığın en derinde: main + 5 çerçeve.
toplam(a, 0) 0 döndürdü → 0 + a[0] = 0 + 4 = 4 döner.
toplam(a, 1) 4 döndürdü → 4 + a[1] = 4 + 3 = 7 döner.
toplam(a, 2) 7 döndürdü → 7 + a[2] = 7 + 6 = 13 döner.
toplam(a, 3) 13 döndürdü → 13 + a[3] = 13 + 2 = 15 döner.
Sonuç 15. 5 çağrı (n = 4, 3, 2, 1, 0): zaman O(n), en çok 5 çerçeve (+ main): bellek O(n).
int toplam(int[] a, int n) { // ilk n öğenin toplamı if (n == 0) return 0; // taban durum return toplam(a, n - 1) + a[n - 1]; } // main içinde: int[] a = {4, 3, 6, 2}; System.out.println(toplam(a, 4));
Doğrusal Özyineleme
Diziyi Tersine Çevirme
- Fikir: dizinin iki ucunu değiştir, aradaki parçayı aynı yöntemle tersine çevir.
- Parça
a[i..j]indisleriyle verilir; ilk çağrıters(a, 0, n − 1). - Taban durum:
i >= j, yani parçada 0 ya da 1 öğe kaldı; yapılacak iş yok. - Her çağrı parçayı 2 öğe kısaltır: n öğe için ⌊n/2⌋ değiştirme, ⌊n/2⌋ + 1 çağrı → O(n).
- Dizi yerinde değişir; metot bir değer döndürmez (
void).
Doğrusal Özyineleme · Kodu adım adım çalıştır
Kod İzleme: ters(a, 0, 5)
main, ters(a, 0, 5)'i çağırır.
i = 0 < j = 5 → a[0] ile a[5] değişir: dizi 6 7 4 9 1 2. Sonra ters(a, 1, 4) çağrılır.
i = 1 < j = 4 → a[1] ile a[4] değişir: dizi 6 1 4 9 7 2. Sonra ters(a, 2, 3) çağrılır.
i = 2 < j = 3 → a[2] ile a[3] değişir: dizi 6 1 9 4 7 2. Sonra ters(a, 3, 2) çağrılır.
i = 3 ≥ j = 2: taban durum, ortada çevrilecek öğe kalmadı. Yığında main + 4 çerçeve.
Çerçeveler sırayla döner; dönüşte yapılacak iş yok. Sonuç: 6 1 9 4 7 2. 3 değiştirme, 4 çağrı.
void ters(int[] a, int i, int j) { if (i >= j) return; // taban: 0 ya da 1 öğe int t = a[i]; a[i] = a[j]; a[j] = t; // uçlar ters(a, i + 1, j - 1); // içteki parça } // main içinde: int[] a = {2, 7, 4, 9, 1, 6}; ters(a, 0, a.length - 1);
Doğrusal Özyineleme
Üs Alma: Yavaş ve Hızlı
Yavaş: xⁿ = x · xⁿ⁻¹, x⁰ = 1
long us(long x, int n) { if (n == 0) return 1; return x * us(x, n - 1); }
Hızlı: üssü her adımda yarıya indir
xⁿ = x · (x(n−1)/2)² (n tek)
2¹⁰ = (2⁵)², 2⁵ = 2 · (2²)²,
2² = (2¹)², 2¹ = 2 · (2⁰)²
| n | us: çağrı | hizliUs: çağrı |
|---|---|---|
| 10 | 11 | 5 |
| 100 | 101 | 8 |
| 1 000 | 1 001 | 11 |
| 10⁶ | 1 000 001 | 21 |
Yavaş sürüm n + 1 çağrı yapar: O(n). Hızlı sürümde n her çağrıda yarıya iner: ⌊log₂ n⌋ + 2 çağrı, O(log n).
Doğrusal Özyineleme · Kodu adım adım çalıştır
Kod İzleme: hizliUs(2, 10)
main, hizliUs(2, 10)'u çağırır: 2¹⁰.
n = 10 ≠ 0 → önce y = hizliUs(2, 5) hesaplanır (10 / 2 = 5).
n = 5 ≠ 0 → önce y = hizliUs(2, 2) hesaplanır (5 / 2 = 2).
n = 2 ≠ 0 → önce y = hizliUs(2, 1) hesaplanır (2 / 2 = 1).
n = 1 ≠ 0 → önce y = hizliUs(2, 0) hesaplanır (1 / 2 = 0).
n = 0: taban durum → 1 döner (2⁰ = 1).
y = 1 geldi. n = 1 tek → 2 · y · y = 2 · 1 · 1 = 2 döner.
y = 2 geldi. n = 2 çift → y · y = 2 · 2 = 4 döner.
y = 4 geldi. n = 5 tek → 2 · y · y = 2 · 4 · 4 = 32 döner.
y = 32 geldi. n = 10 çift → y · y = 32 · 32 = 1024 döner.
Sonuç 1024. Yalnızca 5 çağrı; yavaş us(2, 10) 11 çağrı yapardı.
long hizliUs(long x, int n) { if (n == 0) return 1; // taban durum long y = hizliUs(x, n / 2); // x^(n/2) if (n % 2 == 0) return y * y; // n çift return x * y * y; // n tek } // main içinde: System.out.println(hizliUs(2, 10));
İkili Özyineleme
Fibonacci Sayıları
int fib(int n) { if (n <= 1) return n; // taban durum return fib(n - 1) + fib(n - 2); }
F(n) = F(n − 1) + F(n − 2) (n ≥ 2)
- İkili özyineleme: bir çağrı iki özyinelemeli çağrı yapar.
- Çağrılar artık zincir değil, bir çağrı ağacı oluşturur: her düğüm bir çağrı, çocukları onun yaptığı çağrılar.
- Tanım koda doğrudan dökülür; iki taban durum vardır: n = 0 ve n = 1.
- Ağaçta aynı değer birden çok kez hesaplanır; maliyeti bu belirler.
İkili Özyineleme · Kodu adım adım çalıştır
Kod İzleme: fib(4) çağrı ağacı
main, fib(4)'ü çağırır.
fib(4): n > 1 → önce fib(3) çağrılır; fib(2) sırada bekler.
fib(3): n > 1 → önce fib(2) çağrılır; fib(1) sırada bekler.
fib(2): n > 1 → önce fib(1) çağrılır; fib(0) sırada bekler.
fib(1): taban durum → 1 döner.
İkinci terim: fib(0): taban durum → 0 döner.
fib(2) = fib(1) + fib(0) = 1 + 0 = 1 döner.
İkinci terim: fib(1): taban durum → 1 döner.
fib(3) = fib(2) + fib(1) = 1 + 1 = 2 döner.
fib(4) ikinci terim için fib(2)'yi çağırır: fib(2) ikinci kez hesaplanacak. Önce fib(1).
fib(1): taban durum → 1 döner.
İkinci terim: fib(0): taban durum → 0 döner.
fib(2) = fib(1) + fib(0) = 1 + 0 = 1 döner.
fib(4) = fib(3) + fib(2) = 2 + 1 = 3 döner.
fib(4) = 3. 9 çağrı: fib(2) iki kez, fib(1) üç kez, fib(0) iki kez hesaplandı.
int fib(int n) { if (n <= 1) return n; // taban durum return fib(n - 1) + fib(n - 2); } // main içinde: System.out.println(fib(4));
İkili Özyineleme
Özyinelemenin Maliyeti
int fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); }
- faktoriyel: T(n) = T(n − 1) + c → n çağrı → O(n) zaman, O(n) yığın belleği.
- fib: her çağrı iki çağrı yapar; aynı alt problemler tekrar tekrar çözülür.
- fib(4) → 9 çağrı; turuncu f(2) iki kez hesaplanıyor. Çağrı sayısı üstel büyür (≈ 1,6ⁿ).
- Çözüm: sonuçları sakla veya döngüyle hesapla → O(n).
İkili Özyineleme
Tekrarlanan Hesaptan Kaçınmak
| n | fib(n) | fib çağrı sayısı |
|---|---|---|
| 5 | 5 | 15 |
| 10 | 55 | 177 |
| 20 | 6 765 | 21 891 |
| 30 | 832 040 | 2 692 537 |
| 40 | 102 334 155 | 331 160 281 |
Çağrı sayısı = 2 · F(n + 1) − 1: n her 1 arttığında yaklaşık 1,6 katına çıkar.
long[] bellek = new long[91]; // 0: hesaplanmadı long fibBellek(int n) { if (n <= 1) return n; if (bellek[n] != 0) return bellek[n]; bellek[n] = fibBellek(n - 1) + fibBellek(n - 2); return bellek[n]; }
long fibDongu(int n) { long a = 0, b = 1; // F(0), F(1) for (int i = 0; i < n; i++) { long t = a + b; a = b; b = t; } return a; }
fibBellek(40) 79 çağrı yapar. Döngülü sürüm O(n) zaman ve O(1) bellekle çalışır.İkili Özyineleme
Özyinelemeli İkili Arama
- Sıralı dizide x aranır: ortadaki öğeye bakılır; x küçükse sol yarıda, büyükse sağ yarıda aynı arama yapılır.
- İki taban durum: aralık boş (
lo > hi) → −1; ortadaki öğe x → indisi. - Kodda iki özyinelemeli çağrı yazılıdır ama her çağrı bunlardan yalnızca birini yapar. Fibonacci gibi ağaç oluşmaz; çağrılar bir zincirdir.
- Aralık her çağrıda yarıya iner: T(n) = T(n/2) + c → O(log n) zaman ve O(log n) yığın belleği.
- n = 10 için en çok 5, n = 10⁶ için en çok 21 çağrı.
İkili Özyineleme · Kodu adım adım çalıştır
Kod İzleme: ikiliAra(a, 23, 0, 9)
main, 10 öğeli sıralı dizide 23'ü arar: ikiliAra(a, 23, 0, 9).
lo = 0 ≤ hi = 9: aralık boş değil. mid = (0 + 9) / 2 = 4, a[4] = 16.
23 > 16 → sağ yarı: ikiliAra(a, 23, 5, 9). Sol yarıya hiç bakılmaz.
lo = 5 ≤ hi = 9: aralık boş değil. mid = (5 + 9) / 2 = 7, a[7] = 56.
23 < 56 → sol yarı: ikiliAra(a, 23, 5, 6). Sağ yarıya hiç bakılmaz.
lo = 5 ≤ hi = 6: aralık boş değil. mid = (5 + 6) / 2 = 5, a[5] = 23.
a[5] = 23 = x: taban durum (bulundu) → 5 döner.
ikiliAra(a, 23, 5, 9) aldığı 5'i return ile olduğu gibi iletir.
ikiliAra(a, 23, 0, 9) de 5'i iletir. Dönüşte yeni hesap yapılmaz.
Sonuç 5. 3 çağrı; aralık 10 → 5 → 2 öğeye indi.
int ikiliAra(int[] a, int x, int lo, int hi) { if (lo > hi) return -1; // taban: boş aralık int mid = (lo + hi) / 2; if (a[mid] == x) return mid; // taban: bulundu if (x < a[mid]) return ikiliAra(a, x, lo, mid - 1); // sol yarı return ikiliAra(a, x, mid + 1, hi); // sağ yarı } System.out.println(ikiliAra(a, 23, 0, 9));
Hanoi ve Geri İzleme
Hanoi Kuleleri (Towers of Hanoi)
Bilgisayar bilimi ve matematikte klasik bir problem: bir disk yığını, kurallara uyularak kaynak çubuktan hedef çubuğa taşınır.
- Bir hamlede yalnızca bir disk taşınır (bir çubuğun en üstündeki).
- Bir disk yalnızca daha büyük bir diskin üstüne ya da boş çubuğa konabilir.
- Amaç: yardımcı çubuğu kullanarak tüm diskleri kaynaktan (A) hedefe (C) taşımak.
Hanoi ve Geri İzleme
Hanoi Kuleleri: 3 Diskle 7 Hamle
Başlangıç: 3 disk A çubuğunda (büyük altta). Hedef: hepsini C'ye taşımak.
Hamle 1: disk 1, A → C.
Hamle 2: disk 2, A → B.
Hamle 3: disk 1, C → B.
Hamle 4: disk 3, A → C.
Hamle 5: disk 1, B → A.
Hamle 6: disk 2, B → C.
Hamle 7: disk 1, A → C. Bitti: 2³ − 1 = 7 hamle.
Hanoi ve Geri İzleme · Kodu adım adım çalıştır
Kod İzleme: Özyinelemeli hanoi(3, A, B, C)
main, hanoi(3, 'A', 'B', 'C') çağırır. Ağaçtaki her kutu bir çağrı: n:kaynak→hedef.
main: yeni çerçeve açıldı. n = 3 ≠ 1 → önce üstteki 2 disk A çubuğundan B çubuğuna taşınmalı.
Satır 6: hanoi(2,A,C,B): yeni çerçeve açıldı. n = 2 ≠ 1 → önce üstteki 1 disk A çubuğundan C çubuğuna taşınmalı.
Satır 6: hanoi(1,A,B,C): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, A → C.
return: çerçeve yığından atılır, kontrol çağırana döner.
Sol alt çağrı bitti; A çubuğunda yalnızca disk 2 kaldı → Disk 2: A → B.
Satır 8: hanoi(1,C,A,B): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, C → B.
return: çerçeve yığından atılır, kontrol çağırana döner.
hanoi(2,A,C,B) biter; çerçevesi atılır.
Sol alt çağrı bitti; A çubuğunda yalnızca disk 3 kaldı → Disk 3: A → C.
Satır 8: hanoi(2,B,A,C): yeni çerçeve açıldı. n = 2 ≠ 1 → önce üstteki 1 disk B çubuğundan A çubuğuna taşınmalı.
Satır 6: hanoi(1,B,C,A): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, B → A.
return: çerçeve yığından atılır, kontrol çağırana döner.
Sol alt çağrı bitti; B çubuğunda yalnızca disk 2 kaldı → Disk 2: B → C.
Satır 8: hanoi(1,A,B,C): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, A → C.
return: çerçeve yığından atılır, kontrol çağırana döner.
hanoi(2,B,A,C) biter; çerçevesi atılır.
hanoi(3,A,B,C) biter; çerçevesi atılır. Ağaçtaki 7 çağrı = 7 hamle = 2³ − 1.
main'e dönüldü: tüm diskler C'de. Çağrı yığınının en fazla derinliği n = 3 (+ main) → bellek O(n), zaman O(2ⁿ).
void hanoi(int n, char k, char y, char h) { if (n == 1) { System.out.println("1: " + k + "→" + h); return; } hanoi(n - 1, k, h, y); System.out.println(n + ": " + k + "→" + h); hanoi(n - 1, y, k, h); } // main içinde: hanoi(3, 'A', 'B', 'C');
Hanoi ve Geri İzleme
Geri İzleme (Backtracking)
- Çözüm adım adım kurulur; her adımda olası seçeneklerden biri denenir.
- Seçimle bir sonraki adıma geçilir: özyinelemeli çağrı.
- Çağrı döndüğünde seçim geri alınır ve sıradaki seçenek denenir. Böylece her dal aynı başlangıç durumunu görür.
- Bir seçim çözüme götüremeyecekse o dal bırakılır, daha derine inilmez.
- Örnekler: alt kümeleri ya da permütasyonları listeleme, labirentte yol bulma, sudoku, n vezir.
Hanoi ve Geri İzleme
Alt Kümeleri Listeleme
- Her öğe için iki karar: al ya da alma. Sol dal öğeyi alır (+1, +2, +3), sağ dal almaz.
- i. seviyede a[i] için karar verilir; yaprakta (i = n) tüm kararlar bitmiştir → s bir alt kümedir.
- n öğe → 2ⁿ yaprak = 2ⁿ alt küme; çağrı sayısı 2ⁿ⁺¹ − 1 (n = 3: 8 alt küme, 15 çağrı).
- Çıktı sırası:
[1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []
Hanoi ve Geri İzleme · Kodu adım adım çalıştır
Kod İzleme: altKumeler ile {1, 2, 3}
main, a = {1, 2, 3} ve boş s ile altKumeler(a, 0, s)'yi çağırır.
i = 0: a[0] = 1 alınır (s = [1]), sol dala inilir.
i = 1: a[1] = 2 alınır (s = [1, 2]), sol dala inilir.
i = 2: a[2] = 3 alınır (s = [1, 2, 3]), sol dala inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 2, 3] yazılır.
Sol daldan dönüldü. 3 geri alınır (s = [1, 2]); aynı i ile sağ dala, 3'siz alt kümelere inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 2] yazılır.
Sol daldan dönüldü. 2 geri alınır (s = [1]); aynı i ile sağ dala, 2'siz alt kümelere inilir.
i = 2: a[2] = 3 alınır (s = [1, 3]), sol dala inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 3] yazılır.
Sol daldan dönüldü. 3 geri alınır (s = [1]); aynı i ile sağ dala, 3'siz alt kümelere inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [1] yazılır.
Sol daldan dönüldü. 1 geri alınır (s = []); aynı i ile sağ dala, 1'siz alt kümelere inilir.
i = 1: a[1] = 2 alınır (s = [2]), sol dala inilir.
i = 2: a[2] = 3 alınır (s = [2, 3]), sol dala inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [2, 3] yazılır.
Sol daldan dönüldü. 3 geri alınır (s = [2]); aynı i ile sağ dala, 3'siz alt kümelere inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [2] yazılır.
Sol daldan dönüldü. 2 geri alınır (s = []); aynı i ile sağ dala, 2'siz alt kümelere inilir.
i = 2: a[2] = 3 alınır (s = [3]), sol dala inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [3] yazılır.
Sol daldan dönüldü. 3 geri alınır (s = []); aynı i ile sağ dala, 3'siz alt kümelere inilir.
i = 3 = a.length: taban durum. Tüm kararlar verildi → [] yazılır.
Bitti: 8 alt küme, 15 çağrı. Yığında aynı anda en çok main + 4 çerçeve vardı.
void altKumeler(int[] a, int i, List<Integer> s) { if (i == a.length) { // taban durum System.out.print(s + " "); return; } s.add(a[i]); // a[i]'yi al altKumeler(a, i + 1, s); s.remove(s.size() - 1); // geri al altKumeler(a, i + 1, s); // a[i]'siz dene } // main içinde: altKumeler(new int[]{1, 2, 3}, 0, new ArrayList<>());
Hanoi ve Geri İzleme
Permütasyonları Listeleme
void perm(char[] h, boolean[] kul, StringBuilder s) { if (s.length() == h.length) { // taban durum System.out.print(s + " "); return; } for (int k = 0; k < h.length; k++) { if (kul[k]) continue; // harf kullanıldı kul[k] = true; s.append(h[k]); // seç perm(h, kul, s); kul[k] = false; // geri al s.deleteCharAt(s.length() - 1); } }
- Çıktı:
ABC ACB BAC BCA CAB CBA - Her seviyede kalan harflerden biri seçilir: 3 · 2 · 1 = 3! = 6 yaprak.
- Çağrı sayısı 1 + 3 + 6 + 6 = 16.
kul[] ve s tüm çağrılarca paylaşılır; bu yüzden her denemeden sonra seçim geri alınır.Yineleme Bağıntıları
Yineleme Bağıntısı (Recurrence)
- T(n): n boyutlu girdide metodun yaptığı toplam iş. Özyinelemeli metodun maliyeti, aynı metodun daha küçük girdilerdeki maliyetiyle yazılır: yineleme bağıntısı.
- Yazmak için üç soru: taban durumun maliyeti nedir (genellikle sabit c)? Kaç özyinelemeli çağrı yapılıyor ve girdileri ne kadar küçük? Çağrılar dışındaki iş ne kadar?
| Metot | Çağrılar | Yineleme bağıntısı | Çözüm |
|---|---|---|---|
| faktoriyel, toplam, geriSay | bir çağrı, n − 1 | T(n) = T(n − 1) + c | O(n) |
| ters | bir çağrı, n − 2 | T(n) = T(n − 2) + c | O(n) |
| ikiliAra, hizliUs | bir çağrı, n / 2 | T(n) = T(n/2) + c | O(log n) |
| hanoi | iki çağrı, n − 1 | T(n) = 2T(n − 1) + c | O(2ⁿ) |
| fib | iki çağrı, n − 1 ve n − 2 | T(n) = T(n − 1) + T(n − 2) + c | üstel |
Yineleme Bağıntıları
T(n) = T(n − 1) + c
T(n) = T(n − 1) + c
= T(n − 2) + 2c
= T(n − 3) + 3c
…
= T(0) + n · c
= c + n · c → O(n)- Bağıntı açılarak çözülür: her açılışta bir c daha eklenir, n azalır. n adımda taban duruma inilir.
- Çağrı ağacı dallanmayan bir zincirdir: n + 1 çağrı. Zincirin tamamı aynı anda yığındadır → bellek de O(n).
- Örnekler: faktoriyel, toplam, geriSay, yavaş üs alma, bağlı listenin uzunluğu.
tersiçin T(n) = T(n − 2) + c: yarı sayıda çağrı, yine O(n).
Yineleme Bağıntıları
T(n) = T(n/2) + c
T(n) = T(n/2) + c
= T(n/4) + 2c
= T(n/8) + 3c
…
= T(n/2ᵏ) + k · cn/2ᵏ = 1 olduğunda durulur: k = log₂ n.
T(n) = T(1) + c · log₂ n → O(log n)
| n | 16 | 1 024 | 1 000 000 |
|---|---|---|---|
| yarılama zinciri (çağrı) | 5 | 11 | 20 |
Örnekler: ikili arama, hızlı üs alma. Girdi bin kat büyüyünce çağrı sayısı yalnızca yaklaşık 10 artar; zincir derinliği de log n olduğundan bellek O(log n).
Yineleme Bağıntıları
T(n) = 2T(n − 1) + c
- Her çağrı n − 1 boyutlu iki çağrı yapar: çağrı ağacının her seviyesinde çağrı sayısı ikiye katlanır.
- Seviye k'de 2ᵏ çağrı; seviyeler 0 … n − 1:
1 + 2 + 4 + … + 2ⁿ⁻¹ = 2ⁿ − 1 çağrı → O(2ⁿ). - Hanoi: tam olarak 2ⁿ − 1 hamle. n = 20 için 1 048 575; n = 64 için yaklaşık 1,8 · 10¹⁹.
- fib: T(n − 2) ≤ T(n − 1) olduğundan T(n) ≤ 2T(n − 1) + c → üst sınır O(2ⁿ); gerçek büyüme yaklaşık 1,6ⁿ.
Yineleme Bağıntıları
Alan Maliyeti: En Derin Çağrı Zinciri
- Bir çağrı, kardeşi başlamadan biter. Bu yüzden yığında aynı anda yalnızca kökten bir düğüme giden tek bir yol bulunur.
- Bellek = en derin çağrı zinciri × çerçeve boyutu. Toplam çağrı sayısı değil, derinlik belirler.
fib(4): 9 çağrı, ama aynı anda en çok 4 çerçeve (+ main).
| Çağrı | Toplam çağrı | En derin zincir | Zaman | Alan |
|---|---|---|---|---|
| faktoriyel(4) | 4 | 4 | O(n) | O(n) |
| fib(40) | 331 160 281 | 40 | üstel | O(n) |
| hanoi(3, …) | 7 | 3 | O(2ⁿ) | O(n) |
| altKumeler, n = 3 | 15 | 4 | O(n · 2ⁿ) | O(n) |
Kuyruk Özyinelemesi
Kuyruk Özyinelemesi (Tail Recursion)
- Özyinelemeli çağrı metodun son işiyse, yani çağrı döndükten sonra yapılacak bir iş kalmıyorsa, metot kuyruk özyinelemelidir.
- Ara sonuç bir biriktirici parametreyle (acc) aşağı taşınır; çarpma çağrıdan önce yapılır. İlk çağrı:
faktKuyruk(n, 1).
Kuyruk özyinelemesi değil
int faktoriyel(int n) { if (n <= 1) return 1; return n * faktoriyel(n - 1); // çarpma, çağrı dönünce yapılır }
Kuyruk özyinelemeli
int faktKuyruk(int n, int acc) { if (n <= 1) return acc; return faktKuyruk(n - 1, n * acc); // son iş: çağrı (çarpma önce) }
Kuyruk Özyinelemesi · Kodu adım adım çalıştır
Kod İzleme: faktKuyruk(4, 1)
main, faktKuyruk(4, 1)'i çağırır; acc başlangıçta 1.
n = 4 > 1 → çarpma önce: 4 · 1 = 4. Sonra faktKuyruk(3, 4) çağrılır.
n = 3 > 1 → çarpma önce: 3 · 4 = 12. Sonra faktKuyruk(2, 12) çağrılır.
n = 2 > 1 → çarpma önce: 2 · 12 = 24. Sonra faktKuyruk(1, 24) çağrılır.
n = 1: taban durum → acc = 24 döner. Sonuç burada hazır.
faktKuyruk(2, 12), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.
faktKuyruk(3, 4), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.
faktKuyruk(4, 1), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.
Sonuç 24. Java'da yine 4 çerçeve açıldı; dönüşte bu çerçevelere ihtiyaç yoktu.
int faktKuyruk(int n, int acc) { if (n <= 1) return acc; // taban durum return faktKuyruk(n - 1, n * acc); // son iş } // main içinde: System.out.println(faktKuyruk(4, 1));
Kuyruk Özyinelemesi
Kuyruk Özyinelemesini Döngüye Çevirme
int faktDongu(int n) { int acc = 1; while (n > 1) { // taban durum değilken acc = n * acc; // çağrının yeni n = n - 1; // parametreleri } return acc; // taban: return acc }
- Kuyruk çağrısı, parametreleri yeni değerlerle güncelleyip metodun başına dönmek demektir: parametreler değişken, taban durum döngü koşulu olur.
- Çevirme mekaniktir; döngülü ikili arama da
ikiliAra'nın böyle çevrilmiş hâlidir. - Döngü sabit bellekle çalışır: O(1), çerçeve açılmaz.
faktKuyruk için de her çağrıda yeni çerçeve açar; çok derin çağrıda StackOverflowError yine oluşur. Scheme gibi bazı diller bu eniyilemeyi garanti eder; Scala ve Kotlin derleyicileri kendini çağıran kuyruk özyinelemesini döngüye çevirebilir.Kuyruk Özyinelemesi
Özyineleme mi, Döngü mü?
| Ölçüt | Özyineleme | Döngü |
|---|---|---|
| Okunabilirlik | tanıma yakın, kısa | durum değişkenleri elle yönetilir |
| Bellek | O(derinlik) çağrı yığını | çoğunlukla O(1) |
| Hız | her çağrının ek maliyeti var | genellikle daha hızlı |
| Derinlik sınırı | StackOverflowError riski | yok |
Bağlı Listede Özyineleme
Bağlı Listede Özyineleme
- Bir bağlı liste ya boştur (
null) ya da bir düğüm ve onu izleyen daha kısa bir listedir. - Tanım özyinelemelidir: düğüm işlenir, kalan liste (
d.sonraki) aynı metoda verilir. - Taban durum:
d == null.
class Dugum { int veri; Dugum sonraki; } int uzunluk(Dugum d) { if (d == null) return 0; // boş liste return 1 + uzunluk(d.sonraki); // 1 + kalan }
void yazdir(Dugum d) { if (d == null) return; System.out.print(d.veri + " "); yazdir(d.sonraki); }
Bağlı Listede Özyineleme · Kodu adım adım çalıştır
Kod İzleme: uzunluk(bas)
main, uzunluk(bas)'ı çağırır; d ilk düğümü (7) gösterir.
d ≠ null → 1 + uzunluk(d.sonraki): kalan liste (3 ile başlayan) için çağrı; bu çerçeve bekler.
d ≠ null → 1 + uzunluk(d.sonraki): kalan liste (9 ile başlayan) için çağrı; bu çerçeve bekler.
d ≠ null → 1 + uzunluk(d.sonraki): d.sonraki = null, boş liste için çağrı.
d = null: taban durum, boş listenin uzunluğu 0 döner.
Kalan listeden 0 geldi → 1 + 0 = 1 döner.
Kalan listeden 1 geldi → 1 + 1 = 2 döner.
Kalan listeden 2 geldi → 1 + 2 = 3 döner.
Sonuç 3. 4 çağrı (3 düğüm + null): zaman O(n), yığın O(n).
int uzunluk(Dugum d) { if (d == null) return 0; // boş liste return 1 + uzunluk(d.sonraki); // 1 + kalan } // main içinde (bas → 7 → 3 → 9 → null): System.out.println(uzunluk(bas));
Bağlı Listede Özyineleme
Listeyi Tersten Yazdırma
void yazdir(Dugum d) { if (d == null) return; System.out.print(d.veri + " "); yazdir(d.sonraki); }
Çıktı: 7 3 9 (yazdırma çağrıdan önce)
void tersYazdir(Dugum d) { if (d == null) return; tersYazdir(d.sonraki); // önce kalan System.out.print(d.veri + " "); }
Çıktı: 9 3 7 (yazdırma çağrıdan sonra)
- Tek yönlü listede geriye bağlantı yoktur; geri dönüşü çağrı yığını sağlar: düğümler, yığın çözülürken sondan başa yazılır.
- Bedeli O(n) yığın belleğidir. Çok uzun listelerde döngü ve açık bir yığın daha güvenlidir.
Özet
Sık Yapılan Hatalar
StackOverflowError. n == 1 yerine n <= 1 daha güvenlidir.f(n) içinden yine f(n) ya da f(n + 1) çağrılırsa taban duruma yaklaşılmaz.ikiliAra'da sol yarı çağrısının önündeki return unutulursa sonuç atılır, akış sağ yarı çağrısına düşer: yanlış cevap, derleyici uyarmaz.s.remove(...) yapılmazsa bir dalın seçimleri sonraki dallara taşar; alt kümeler yanlış çıkar.fib gibi ikili özyinelemede aynı alt problem üstel sayıda çözülür. Sonuçları saklamak ya da döngü O(n) verir.uzunluk yığını taşırır. Java kuyruk çağrısını eniyilemez → döngü.Özet
Özet
| Örnek | Özyineleme türü | Zaman | Alan (yığın) |
|---|---|---|---|
| faktoriyel, toplam, ters | doğrusal | O(n) | O(n) |
| hizliUs, ikiliAra | doğrusal, yarılayan | O(log n) | O(log n) |
| fib | ikili | üstel (≈ 1,6ⁿ) | O(n) |
| hanoi | ikili | O(2ⁿ) | O(n) |
| altKumeler | geri izleme | O(n · 2ⁿ) | O(n) |
| perm | çoklu, geri izleme | O(n · n!) | O(n) |
| faktKuyruk | kuyruk özyinelemesi | O(n) | O(n) (Java'da) |
| uzunluk, tersYazdir | doğrusal (bağlı liste) | O(n) | O(n) |
Özet
Etkileşimli Simülatörler
Özet
Örnek Problemler
int f(int n) { if (n <= 0) return 0; return n % 10 + f(n / 10); }içinf(2026)kaçtır, kaç çağrı yapılır?ileriSay(4)ne yazar? Yığında (main dahil) en çok kaç çerçeve bulunur?hizliUs(3, 13)kaç çağrı yapar, sonuç nedir?fib(6)kaç çağrı yapar?- {1, 2, 3, 4} için
altKumelerkaç alt küme yazar, kaç çağrı yapar? - T(n) = T(n/2) + c ve T(1) = c ise n = 64 için zincirde kaç çağrı vardır?
Cevapları göster
- Basamaklar toplamı: 6 + 2 + 0 + 2 = 10; çağrılar 2026, 202, 20, 2, 0 → 5.
1 2 3 4; ileriSay(4 … 0) = 5 çerçeve + main = 6.- n: 13, 6, 3, 1, 0 → 5 çağrı; 3¹³ = 1 594 323.
- 2 · F(7) − 1 = 2 · 13 − 1 = 25 çağrı (fib(6) = 8).
- 2⁴ = 16 alt küme; 2⁵ − 1 = 31 çağrı.
- 64, 32, 16, 8, 4, 2, 1 → 7 = log₂ 64 + 1.
Özet
Alıştırmalar
faktoriyel(5)çalışırken çağrı yığınında (main dahil) en fazla kaç çerçeve olur?- 4 diskli Hanoi kaç hamle ister?
- Dizinin en büyük öğesini döndüren özyinelemeli
enBuyuk(a, n)için taban durum ve özyinelemeli adım nedir? palindrom(s, i, j)uçları karşılaştırıp içe ilerler: "kayak" için kaç çağrı yapılır?int g(int n) { if (n <= 1) return 1; return g(n - 1) + g(n - 1); }: g(5) değeri ve çağrı sayısı? Nasıl O(n) yapılır?faktKuyruk'un döngülü sürümünde her turda n ve acc nasıl değişir?
Cevaplar / ipuçları
- faktoriyel(5), (4), (3), (2), (1) + main = 6.
- 2⁴ − 1 = 15 hamle.
- Taban: n = 1 → a[0]. Adım: max(enBuyuk(a, n − 1), a[n − 1]). O(n).
- (0, 4) k = k → (1, 3) a = a → (2, 2) taban: 3 çağrı, true.
- g(5) = 2⁴ = 16; 2⁵ − 1 = 31 çağrı. Tek çağrı yeter:
return 2 * g(n - 1);→ O(n). acc = n * acc; n = n - 1;döngü n > 1 iken sürer; sonunda acc döner.
Özet
Kaynaklar
- Goodrich, Tamassia ve Goldwasser, Data Structures and Algorithms in Java, 6. baskı, 2014.
- Sedgewick ve Wayne, Computer Science: An Interdisciplinary Approach, 2016.
- Roberts, Thinking Recursively with Java, 2006.
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022.