Bölüm 12: Özyineleme
Giriş
Öğrenme Çıktıları
Taban durum ve özyinelemeli durumu belirlemek.
Çağrı yığınını adım adım takip etmek.
Özyineleme ile döngüyü değerlendirmek.
Tekrarlı hesaplamayı bellekleme (memoization) ile azaltmak.
Temeller
Özyineleme Nedir?
- Bir metodun problemi daha küçük bir örneğine indirip kendisini çağırmasıdır.
- Her çağrı kendi parametre ve yerel değişkenleriyle ayrı bir yığın çerçevesi oluşturur.
- Problem her çağrıda taban duruma yaklaşmalıdır; yoksa çağrılar sonlanmaz.
Temeller
İki Zorunlu Parça
Sonucun doğrudan bilindiği, yeni çağrı yapılmayan durum (ör. n ≤ 1 → 1).
Problemi daha küçük örneğe dönüştüren çağrı (ör. n × f(n − 1)).
Örnekler
Özyinelemeli Faktöriyel
static long faktoriyel(int n) { if (n < 0) throw new IllegalArgumentException(); if (n <= 1) return 1; return n * faktoriyel(n - 1); }
n! = n × (n − 1)!, 0! = 1! = 1
n ≤ 1 → 1; her çağrıda n bir azalır.
Faktöriyel tanımsız → hata fırlatılır.
Çağrı Yığını · Kodu adım adım çalıştır
Kod İzleme: faktoriyel(4)
main, faktoriyel(4)'ü çağırır; sonucu bekler.
Yeni çerçeve: n = 4. 4 < 0 → false, 4 <= 1 → false.
4 * faktoriyel(3): çarpma için önce alt sonuç gerekli → bu çerçeve bekler, yeni çağrı yapılır.
Yeni çerçeve: n = 3. 3 < 0 → false, 3 <= 1 → false.
3 * faktoriyel(2): çarpma için önce alt sonuç gerekli → bu çerçeve bekler, yeni çağrı yapılır.
Yeni çerçeve: n = 2. 2 < 0 → false, 2 <= 1 → false.
2 * faktoriyel(1): çarpma için önce alt sonuç gerekli → bu çerçeve bekler, yeni çağrı yapılır.
Yeni çerçeve: n = 1. 1 < 0 → false, 1 <= 1 → true: taban durum, return 1.
faktoriyel(1) 1 döndürdü; çerçevesi silindi. Şimdi 2 × 1 = 2.
faktoriyel(2) 2 döndürdü; çerçevesi silindi. Şimdi 3 × 2 = 6.
faktoriyel(3) 6 döndürdü; çerçevesi silindi. Şimdi 4 × 6 = 24.
En üstteki çerçeve de kapandı; main 24 yazdırır. En fazla 5 çerçeve açıktı → ek bellek O(n).
static long faktoriyel(int n) { if (n < 0) throw new IllegalArgumentException(); if (n <= 1) return 1; return n * faktoriyel(n - 1); } System.out.println(faktoriyel(4));
Çağrı Yığını
faktoriyel(4): Açılım ve Dönüş
- Çağrılar taban duruma ulaşıncaya kadar yığında açılır; hiçbir çarpma henüz yapılmamıştır.
- f(1) = 1 döndükten sonra bekleyen çarpımlar ters sırada tamamlanır: 2, 6, 24.
Örnekler · Kodu adım adım çalıştır
1'den n'e Toplam: toplam(3)
Tanım: toplam(n) = n + toplam(n − 1), toplam(0) = 0. main, toplam(3)'ü çağırır.
3 <= 0 → false.
3 + toplam(2): toplama bekler, yeni çağrı.
2 <= 0 → false.
2 + toplam(1): toplama bekler, yeni çağrı.
1 <= 0 → false.
1 + toplam(0): toplama bekler, yeni çağrı.
0 <= 0 → true: taban durum, return 0.
toplam(0) = 0 döndü → 1 + 0 = 1.
toplam(1) = 1 döndü → 2 + 1 = 3.
toplam(2) = 3 döndü → 3 + 3 = 6.
6 = 3 + 2 + 1 + 0 yazdırılır.
static int toplam(int n) { if (n <= 0) return 0; return n + toplam(n - 1); } System.out.println(toplam(3));
Örnekler · Kodu adım adım çalıştır
Rakamlar Toplamı: rakamToplami(472)
main, rakamToplami(472)'yi çağırır.
Negatif sayılar için mutlak değer alınır: m = 472 (long'a çevirmek en küçük int'te taşmayı önler).
Asıl işi yapan yardımcı metot çağrılır.
472 < 10 → false: son rakam 2, kalan 47.
472 % 10 = 2 bekletilir; 472 / 10 = 47 ile yeni çağrı.
47 < 10 → false: son rakam 7, kalan 4.
47 % 10 = 7 bekletilir; 47 / 10 = 4 ile yeni çağrı.
4 < 10 → true: tek basamak, return 4.
Alt çağrı 4 döndü → 7 + 4 = 11.
Alt çağrı 11 döndü → 2 + 11 = 13.
Yardımcı metot 13 döndürdü; aynen döndürülür.
4 + 7 + 2 = 13 yazdırılır.
static int rakamToplami(int n) { long m = Math.abs((long) n); return rakamToplamiPozitif(m); } static int rakamToplamiPozitif(long n) { if (n < 10) return (int) n; return (int) (n % 10) + rakamToplamiPozitif(n / 10); } System.out.println(rakamToplami(472));
Örnekler · Kodu adım adım çalıştır
Palindrom: palindrom("kayak", 0, 4)
main, palindrom("kayak", 0, 4)'ü çağırır.
0 >= 4 → false.
'k' != 'k' → false: uçlar eşit, içe doğru devam.
İki uç atılır: palindrom(s, 1, 3) çağrılır.
1 >= 3 → false.
'a' != 'a' → false: uçlar eşit, içe doğru devam.
İki uç atılır: palindrom(s, 2, 2) çağrılır.
2 >= 2 → true: ortada tek karakter kaldı → return true.
Alt çağrı true döndürdü; bu çerçeve de aynı değeri döndürür.
Alt çağrı true döndürdü; bu çerçeve de aynı değeri döndürür.
Sonuç main'e ulaştı: true. "kayak" tersten de aynı okunur.
static boolean palindrom(String s, int sol, int sag) { if (sol >= sag) return true; if (s.charAt(sol) != s.charAt(sag)) return false; return palindrom(s, sol + 1, sag - 1); } System.out.println(palindrom("kayak", 0, 4));
Örnekler · Kodu adım adım çalıştır
Öklid ile EBOB: ebob(48, 18)
main, ebob(48, 18)'i çağırır.
18 == 0 → false.
48 % 18 = 12 → ebob(18, 12) çağrılır (sayılar küçülüyor).
12 == 0 → false.
18 % 12 = 6 → ebob(12, 6) çağrılır (sayılar küçülüyor).
6 == 0 → false.
12 % 6 = 0 → ebob(6, 0) çağrılır (sayılar küçülüyor).
0 == 0 → true: taban durum, return 6.
Alt çağrı 6 döndürdü; aynen döndürülür.
Alt çağrı 6 döndürdü; aynen döndürülür.
Alt çağrı 6 döndürdü; aynen döndürülür.
EBOB(48, 18) = 6: kalanlar küçülerek 0'a ulaştı; son sıfır olmayan kalan EBOB'dur.
static int ebob(int a, int b) { if (b == 0) return Math.abs(a); return ebob(b, a % b); } System.out.println(ebob(48, 18));
Örnekler · Kodu adım adım çalıştır
Hanoi Kuleleri: hanoi(3, 'A', 'C', 'B')
3 disk A'dan C'ye taşınacak; B yardımcı. Kural: büyük disk küçüğün üstüne konamaz.
hanoi(3, A, C, B): önce üstteki 2 disk A → B (yardımcıya) taşınmalı.
hanoi(2, A, B, C): önce üstteki 1 disk A → C (yardımcıya) taşınmalı.
Hamle 1: disk 1 A → C. (hanoi(0) çağrıları hemen döner)
Hamle 2: disk 2 A → B.
Hamle 3: disk 1 C → B.
Hamle 4: disk 3 A → C.
hanoi(2, B, C, A): önce üstteki 1 disk B → A (yardımcıya) taşınmalı.
Hamle 5: disk 1 B → A.
Hamle 6: disk 2 B → C.
Hamle 7: disk 1 A → C.
Bitti: 7 = 2³ − 1 hamle. n disk için 2ⁿ − 1 hamle gerekir (üstel).
static void hanoi(int n, char kaynak, char hedef, char ara) { if (n == 0) return; hanoi(n - 1, kaynak, ara, hedef); System.out.print(kaynak + "->" + hedef + " "); hanoi(n - 1, ara, hedef, kaynak); } hanoi(3, 'A', 'C', 'B');
Örnekler · Kodu adım adım çalıştır
Özyinelemeli Üs: us(2, 4)
2⁴ = 2 × 2³ tanımı: us(taban, n) = taban × us(taban, n − 1), us(taban, 0) = 1.
4 == 0 → false: 2 × us(2, 3) için yeni çağrı.
3 == 0 → false: 2 × us(2, 2) için yeni çağrı.
2 == 0 → false: 2 × us(2, 1) için yeni çağrı.
1 == 0 → false: 2 × us(2, 0) için yeni çağrı.
0 == 0 → true: taban durum, return 1.
us(2, 0) = 1 döndü → 2 × 1 = 2.
us(2, 1) = 2 döndü → 2 × 2 = 4.
us(2, 2) = 4 döndü → 2 × 4 = 8.
us(2, 3) = 8 döndü → 2 × 8 = 16.
16: n + 1 = 5 çağrı → O(n).
static long us(long taban, int n) { if (n == 0) return 1; return taban * us(taban, n - 1); } System.out.println(us(2, 4));
Örnekler · Kodu adım adım çalıştır
Hızlı Üs: hizliUs(2, 10)
Fikir: 2¹⁰ = (2⁵)², 2⁵ = (2²)² × 2. Üs her çağrıda yarıya iner.
us = 10 ≠ 0 → önce hizliUs(2, 5) (10 / 2 = 5).
us = 5 ≠ 0 → önce hizliUs(2, 2) (5 / 2 = 2).
us = 2 ≠ 0 → önce hizliUs(2, 1) (2 / 2 = 1).
us = 1 ≠ 0 → önce hizliUs(2, 0) (1 / 2 = 0).
us == 0: return 1.
yarim = 1; 1 tek → 1 × 1 × 2 = 2.
yarim = 2; 2 çift → 2 × 2 = 4.
yarim = 4; 5 tek → 4 × 4 × 2 = 32.
yarim = 32; 10 çift → 32 × 32 = 1024.
1024, yalnızca 5 çağrıyla (düz yöntem 11 çağrı yapardı) → O(log n).
static long hizliUs(long taban, int us) { if (us == 0) return 1; long yarim = hizliUs(taban, us / 2); if (us % 2 == 0) return yarim * yarim; return yarim * yarim * taban; } System.out.println(hizliUs(2, 10));
Örnekler · Kodu adım adım çalıştır
Özyinelemeli İkili Arama: ara(a, 38, 0, 7)
Döngü yerine özyineleme: her çağrı aralığın yarısıyla kendini çağırır.
0 <= 7; mid = 3, a[mid] = 12.
12 < 38 → sağ yarı: ara(a, 38, 4, 7).
4 <= 7; mid = 5, a[mid] = 23.
23 < 38 → sağ yarı: ara(a, 38, 6, 7).
6 <= 7; mid = 6, a[mid] = 38.
38 == 38 → return 6 (taban durum).
Sonuç çerçevelerden aynen geri geçer: 6.
static int ara(int[] a, int x, int lo, int hi) { if (lo > hi) return -1; int mid = (lo + hi) / 2; if (a[mid] == x) return mid; if (a[mid] < x) return ara(a, x, mid + 1, hi); return ara(a, x, lo, mid - 1); } // a = {2, 5, 8, 12, 16, 23, 38, 56} System.out.println(ara(a, 38, 0, 7));
Örnekler
Böl ve Fethet: Birleştirme Sıralaması
static void sirala(int[] a, int lo, int hi) { if (lo >= hi) return; int mid = (lo + hi) / 2; sirala(a, lo, mid); sirala(a, mid + 1, hi); int[] t = new int[hi - lo + 1]; int i = lo, j = mid + 1, k = 0; while (i <= mid && j <= hi) t[k++] = a[i] <= a[j] ? a[i++] : a[j++]; while (i <= mid) t[k++] = a[i++]; while (j <= hi) t[k++] = a[j++]; System.arraycopy(t, 0, a, lo, t.length); }
Her düzeyde toplam n öğe birleştirilir, düzey sayısı log₂ n: zaman O(n log n), ek bellek O(n).
Örnekler · Kodu adım adım çalıştır
Dizgiyi Özyinelemeli Ters Çevirme
ters(s) = ters(ilk karakter hariç kalan) + ilk karakter.
ters("bc") çağrılır; 'a' sona eklenmek üzere bekler.
ters("c") çağrılır; 'b' sona eklenmek üzere bekler.
Uzunluk 1 ≤ 1 → taban durum, "c" döner.
"c" + 'b' = "cb".
"cb" + 'a' = "cba".
cba yazdırılır. Her çağrı substring ile yeni bir alt dizgi üretir.
static String ters(String s) { if (s.length() <= 1) return s; return ters(s.substring(1)) + s.charAt(0); } System.out.println(ters("abc"));
Örnekler · Kodu adım adım çalıştır
Kod İzleme: Alt Küme Üretimi
main, boş bir liste ile altKume(a, 0, sec)'yi çağırır. Her öğe için iki seçim var: dahil et / hariç tut.
i = 0: dizi bitmedi (0 ≠ 2). Önce 1 dahil dalı.
sec.add(1): liste artık [1].
Yeni çerçeve i = 1: 2 için karar; önce dahil et.
sec.add(2): liste [1, 2].
i = 2 = a.length: taban durum. Alt küme yazdırılır: [1, 2].
Geri dönüş: sec.remove(...) 2'yi çıkarır (geri izleme) → [1].
2'yi hariç tut dalı yaprağa ulaşır: [1].
altKume(i=1) bitti; remove 1'i çıkarır → [].
1'i hariç tut dalı: yeni çerçeve i = 1.
sec.add(2): liste [2].
Yaprak: [2] yazdırılır.
remove 2'yi çıkarır → [].
Son yaprak: boş küme [].
Tüm çerçeveler kapandı. 2 öğe → 2² = 4 alt küme; çıktı sırası: [1, 2], [1], [2], [].
static void altKume(int[] a, int i, List<Integer> sec) { if (i == a.length) { System.out.println(sec); return; } sec.add(a[i]); altKume(a, i + 1, sec); sec.remove(sec.size() - 1); altKume(a, i + 1, sec); } altKume(new int[]{1, 2}, 0, new ArrayList<>());
Çağrı Yığını
Çağrı Yığını
- Yeni çağrı yapıldığında mevcut metodun dönüş noktası ve yerel değişkenleri çerçevesinde saklanır.
- Taban durumdan sonra çerçeveler ters sırada (son açılan ilk) kapanır.
- Taban durum yoksa (sonsuz özyineleme) ya da çok derin özyinelemede yığın dolar ve yığın taşması olur:
StackOverflowError.
Çağrı Yığını · Kodu adım adım çalıştır
Kod İzleme: StackOverflowError
main, sonsuz(0)'ı çağırır.
İlk çerçeve. Metotta taban durum yok: hiçbir koşul çağrıyı durdurmuyor, hemen sonsuz(1) çağrılır.
İkinci çerçeve; önceki çerçeve hâlâ yığında.
Üçüncü çerçeve. Hiçbir çağrı dönmediği için hiçbir çerçeve silinmez.
Bu böyle sürer; yığın belleği sınırlıdır (varsayılan boyutta genellikle on binlerce çağrı).
Yığın doldu: JVM StackOverflowError fırlatır, program biter; hata metni stderr'e yazılır.
static void sonsuz(int n) { sonsuz(n + 1); } sonsuz(0);
Verimlilik
Özyineleme mi Döngü mü?
- Kendine benzeyen problemlerde (ağaçlar, böl-parçala) özyineleme tanıma yakın ve açıktır.
- Basit doğrusal tekrarlarda döngü tek çerçevede çalışır, daha az yığın belleği kullanır.
- Seçim yalnızca kod kısalığına değil; doğruluk, okunabilirlik ve kaynak kullanımına dayanmalıdır.
Verimlilik
Faktöriyel Karşılaştırması
| Özellik | Özyinelemeli | Döngülü |
|---|---|---|
| Zaman karmaşıklığı | O(n) | O(n) |
| Ek yığın | O(n) | O(1) |
| Akış | Tanıma yakın | Açık sayaç |
| Derinlik riski | Var | Yok |
Verimlilik · Kodu adım adım çalıştır
Karşılaştırma: Döngülü faktoriyel(4)
main, faktoriyelDongu(4)'ü çağırır.
Tek çerçeve açılır; sonuc = 1.
2 <= 4 → true
sonuc *= 2 → 1 × 2 = 2. Yeni çerçeve açılmadı.
i++ → 3 <= 4 → true
sonuc *= 3 → 2 × 3 = 6. Yeni çerçeve açılmadı.
i++ → 4 <= 4 → true
sonuc *= 4 → 6 × 4 = 24. Yeni çerçeve açılmadı.
i++ → 5 <= 4 → false: döngü biter.
return 24.
24 yazdırılır. Yığın hiç 2 çerçeveyi aşmadı → ek yığın O(1).
static long faktoriyelDongu(int n) { long sonuc = 1; for (int i = 2; i <= n; i++) sonuc *= i; return sonuc; } System.out.println(faktoriyelDongu(4));
Verimlilik
Fibonacci Sorunu: fib(5) Özyineleme Ağacı
- fib(5) için 15 çağrı yapılır: f(3) 2 kez, f(2) 3 kez, f(1) 5 kez hesaplanır (kesikli turuncu = tekrar).
- fib(n − 1) ve fib(n − 2) alt ağaçları büyük ölçüde örtüşür; çağrı sayısı n ile yaklaşık üstel büyür.
Verimlilik · Kodu adım adım çalıştır
Kod İzleme: fib(4)
main, fib(4)'ü çağırır.
fib(4) çağrıldı: 4 <= 1 → false.
fib(3) çağrıldı: 3 <= 1 → false.
fib(2) çağrıldı: 2 <= 1 → false.
fib(1) çağrıldı: 1 <= 1 → true, return 1.
fib(0) çağrıldı: 0 <= 1 → true, return 0.
İki alt sonuç hazır: 1 + 0 = 1, return.
fib(1) çağrıldı: 1 <= 1 → true, return 1.
İki alt sonuç hazır: 1 + 1 = 2, return.
fib(2) çağrıldı: 2 <= 1 → false. Tekrar: f(2) yeniden hesaplanıyor!
fib(1) çağrıldı: 1 <= 1 → true, return 1.
fib(0) çağrıldı: 0 <= 1 → true, return 0.
İki alt sonuç hazır: 1 + 0 = 1, return.
İki alt sonuç hazır: 2 + 1 = 3, return.
3 yazdırılır. Küçük bir n için bile 9 çağrı yapıldı.
static long fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } System.out.println(fib(4));
İyileştirme
Bellekleme (Memoization)
- Hesaplanan alt problem sonuçları bir dizide (önbellek) saklanır.
- Aynı girdi tekrar gelince özyineleme ağacı yeniden açılmaz, kayıtlı değer döner.
- fib(5) için 15 çağrı yerine 9 çağrı; büyük n'de fark çok büyür.
- Zaman kazanımı karşılığında O(n) ek bellek kullanılır.
İyileştirme · Kodu adım adım çalıştır
Kod İzleme: Belleklemeli fib(5)
6 hücreli bellek dizisi; 0 = henüz hesaplanmadı.
fib(5): bellek[5] == 0 → henüz yok, hesaplanacak.
fib(4): bellek[4] == 0 → henüz yok, hesaplanacak.
fib(3): bellek[3] == 0 → henüz yok, hesaplanacak.
fib(2): bellek[2] == 0 → henüz yok, hesaplanacak.
fib(1): 1 <= 1 → true, return 1.
fib(0): 0 <= 1 → true, return 0.
bellek[2] = 1 + 0 = 1 kaydedilir ve döndürülür.
fib(1): 1 <= 1 → true, return 1.
bellek[3] = 1 + 1 = 2 kaydedilir ve döndürülür.
fib(2): bellek[2] = 1 != 0 → hesaplamadan return 1.
bellek[4] = 2 + 1 = 3 kaydedilir ve döndürülür.
fib(3): bellek[3] = 2 != 0 → hesaplamadan return 2.
bellek[5] = 3 + 2 = 5 kaydedilir ve döndürülür.
5 yazdırılır. Toplam 9 çağrı (belleksiz 15) → zaman O(n).
static long fib(int n, long[] bellek) { if (n <= 1) return n; if (bellek[n] != 0) return bellek[n]; bellek[n] = fib(n - 1, bellek) + fib(n - 2, bellek); return bellek[n]; } long[] bellek = new long[6]; System.out.println(fib(5, bellek));
İyileştirme
Kuyruk Özyinelemesi
- Özyinelemeli çağrı metodun son işlemidir; dönüşte yapılacak iş kalmaz.
- Ara sonuç bir parametrede (
birikim) taşınır. - Bazı diller bu çağrıları yığını büyütmeden optimize eder.
- Java dili/JVM kuyruk çağrısı optimizasyonunu garanti etmez: çerçeveler yine birikir.
static long fakt(int n, long birikim) { if (n <= 1) return birikim; return fakt(n - 1, n * birikim); } // fakt(4, 1) → fakt(3, 4) // → fakt(2, 12) → fakt(1, 24) → 24
İyileştirme · Kodu adım adım çalıştır
Kod İzleme: fakt(4, 1)
main, fakt(4, 1)'i çağırır: ikinci parametre birikim başlangıçta 1.
Çerçeve açıldı. 4 <= 1 → false. Son işlem fakt(3, 4 * 1) yani fakt(3, 4) çağrısı; dönüşte yapılacak çarpma yok.
Çarpma çağrıdan önce yapıldı: sıradaki çağrı fakt(2, 12). Eski çerçevenin bekleyecek işi yok; ama Java onu silmez.
Sıradaki çağrı fakt(1, 24). Yığın yine her çağrıda bir çerçeve büyüyor.
1 <= 1 → true: taban durum, return birikim → 24. Kuyruk özyinelemesi olsa da yığında 5 çerçeve (main + 4) birikti: Java'da kuyruk çağrısı optimizasyonu yok.
24 çerçevelerden geçerek main'e döner; çarpma yapılmadı, yalnızca sonuç taşındı. Bellek yine O(n).
static long fakt(int n, long birikim) { if (n <= 1) return birikim; return fakt(n - 1, n * birikim); } System.out.println(fakt(4, 1)); // 24
Doğrulama
Taşma ve Uç Durumlar
faktoriyel(20) → 2432902008176640000 (doğru).faktoriyel(21) → -4249290049419214848
Hata fırlatılmaz; değer sessizce sarar. long en çok 9223372036854775807 tutar.
us(2, -1): taban durum n == 0 hiç sağlanmaz; n −1, −2, … diye iner → StackOverflowError. Önlem: n < 0 ise IllegalArgumentException fırlatılır.
Fibonacci: özyinelemeli ≈ O(2ⁿ), döngülü O(n)
| n | fib(n) özyinelemeli: çağrı sayısı | fib(n) döngülü: tur sayısı |
|---|---|---|
| 10 | 177 | 10 |
| 20 | 21891 | 20 |
| 30 | 2692537 | 30 |
Sayılar, her çağrıda sayaç artırılarak elde edildi; süre ölçümü değildir.
Doğrulama
Hata Ayıklama
- Taban duruma her girdi için ulaşıldığı gösterilir.
- Problemin her çağrıda hangi ölçüye göre küçüldüğü belirlenir.
- Küçük bir girdi için çağrı ve dönüş sırası tabloya dökülür (sağda
toplam(2)). - Özyineleme derinliği ve tekrarlanan alt problemler değerlendirilir.
| Derinlik | Olay | Değer |
|---|---|---|
| 0 | toplam(2) çağrıldı | |
| 1 | toplam(1) çağrıldı | |
| 2 | toplam(0) çağrıldı | |
| 2 | toplam(0) döndü | 0 |
| 1 | toplam(1) döndü | 1 |
| 0 | toplam(2) döndü | 3 |
Özet
Bölüm Özeti
Taban durum + probleme küçülten özyinelemeli durum.
Her çağrı bir çerçeve açar; taban durumdan sonra çerçeveler ters sırada kapanır.
Doğrusal özyineleme O(n) yığın kullanır; döngü çoğu zaman O(1) ile aynı işi yapar.
Tekrarlanan alt problem sonuçlarını saklamak fib'i üstelden O(n)'e indirir.
Özet
Kontrol Soruları
- Taban durum olmazsa ne olur?
- Çağrı yığını neden ters sırada çözülür?
- Özyinelemeli faktöriyel ne kadar ek yığın kullanır?
- Doğrudan Fibonacci neden yavaştır?
- Java'da kuyruk özyinelemesi neden yığın güvenliği sağlamaz?
Cevaplar
- Çağrılar sonlanmaz; yığın dolar ve
StackOverflowErroroluşur. - Her çağrı, kendisinden sonra açılan çağrının sonucunu bekler; son açılan çerçeve ilk tamamlanır.
- n çerçeve → O(n).
- Aynı alt problemleri (f(2), f(3)…) tekrar tekrar hesaplar; çağrı sayısı üstel büyür.
- JVM kuyruk çağrılarını optimize etmeyi garanti etmez; her çağrı yine yeni çerçeve açar.
Özet
Alıştırmalar
- Bir dizinin öğeleri toplamını özyinelemeli yazın:
toplam(a, i)= a[i] + toplam(a, i + 1). - Bir sayının basamak sayısını özyinelemeli bulun (
basamak(472)= 3). - Onluk sayıyı özyinelemeyle ikiliye çeviren
ikili(n)metodunu yazın (13 → "1101"). - Hanoi'de n = 4 için kaç hamle gerekir? Programla doğrulayın.
kombinasyon(n, k) = kombinasyon(n − 1, k − 1) + kombinasyon(n − 1, k)için taban durumları yazın; belleklemeyle hızlandırın.- Bir dizgideki bir karakterin kaç kez geçtiğini özyinelemeli sayın.
Özet
Kaynaklar
- Roberts, Thinking Recursively with Java, Wiley, 2006.
- Sedgewick ve Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016: 2.3 Recursion.
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022.
- Liang, Introduction to Java Programming and Data Structures, 13. baskı, Pearson, 2024.
- Oracle, Java SE 21 API: StackOverflowError.
- Downey ve Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (ücretsiz çevrim içi sürüm).