Bölüm 3.5: Özyineleme
Giriş
Öğrenme Çıktıları
Özyinelemeyi (recursion), taban durumu ve özyinelemeli adımı tanımlamak; bir problemi kendisinin küçük bir kopyasına indirgemek.
Ç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.
Faktöriyel, toplam, seri, rakamlar toplamı, palindrom, EBOB ve Hanoi kuleleri için özyinelemeli Java metotları yazmak.
Sonsuz özyinelemeyi, StackOverflowError'ı ve eksik return hatalarını tanımak ve önlemek.
Özyinelemeli ve ilerlemeli (iterative) çözümleri karşılaştırmak; gerektiğinde döngüye çevirmek.
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.
Özyineleme Kavramı
Taban Durum ve Özyinelemeli Adım
1! = 1.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)
Ö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
returnettiğ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
StackOverflowErrorile biter.
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 } }
n! = n × (n − 1)!, 0! = 1! = 1
0! = 1! = 1; burada özyineleme durur.n!, (n − 1)!'in n ile çarpımıdır.n 1 azalır → n ≥ 0 için taban duruma mutlaka ulaşılır.int 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
faktoriyel(1) doğrudan 1 döndürür. Kod n == 1'de durduğu için faktoriyel(0) hiç çağrılmaz.Faktöriyel ve Toplam · Adım adım kod izleme
faktoriyel(4): Kodun Adım Adım İzlenmesi
main, faktoriyel(4) çağrısını yapar; sonucu yazdırmak için beklemeye başlar.
Yeni çerçeve: n = 4. Koşul n == 0 || n == 1 yanlış → taban durum değil.
4 * faktoriyel(3) hesaplanmalı. Çarpım bekler; önce faktoriyel(3) çağrılır.
Yeni çerçeve: n = 3. Koşul n == 0 || n == 1 yanlış → taban durum değil.
3 * faktoriyel(2) hesaplanmalı. Çarpım bekler; önce faktoriyel(2) çağrılır.
Yeni çerçeve: n = 2. Koşul n == 0 || n == 1 yanlış → taban durum değil.
2 * faktoriyel(1) hesaplanmalı. Çarpım bekler; önce faktoriyel(1) çağrılır.
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(1) çerçevesi yığından çıktı, 1 getirdi: 2 * 1 = 2 döner.
faktoriyel(2) çerçevesi yığından çıktı, 2 getirdi: 3 * 2 = 6 döner.
faktoriyel(3) çerçevesi yığından çıktı, 6 getirdi: 4 * 6 = 24 döner.
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)))
= 15Faktöriyelle aynı kalıp; yalnızca × yerine + ve taban durum farklı. toplam(100) = 5050.
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); } }
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| F(n) | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
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
fibonacci(4): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(3) çağrılır.
fibonacci(3): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(2) çağrılır.
fibonacci(2): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(1) çağrılır.
fibonacci(1): n <= 1 → taban durum, 1 döner.
Sol terim 1 geldi. Şimdi sağ terim: fibonacci(0) çağrılır.
fibonacci(0): n <= 1 → taban durum, 0 döner.
1 + 0 = 1 döner.
Sol terim 1 geldi. Şimdi sağ terim: fibonacci(1) çağrılır.
fibonacci(1): n <= 1 → taban durum, 1 döner.
1 + 1 = 2 döner.
Sol terim 2 geldi. Şimdi sağ terim: fibonacci(2) çağrılır.
fibonacci(2): n > 1. Toplamın önce sol terimi hesaplanır: fibonacci(1) çağrılır.
fibonacci(1): n <= 1 → taban durum, 1 döner.
Sol terim 1 geldi. Şimdi sağ terim: fibonacci(0) çağrılır.
fibonacci(0): n <= 1 → taban durum, 0 döner.
1 + 0 = 1 döner.
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); } }
| n | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| L(n) | 2 | 1 | 3 | 4 | 7 | 11 |
| n | 6 | 7 | 8 | 9 | 10 | |
| L(n) | 18 | 29 | 47 | 76 | 123 |
İ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:
112514421324291430Ne 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:
11251552203877Ne sayar? n elemanlı bir kümeyi gruplara ayırma yollarını. {a, b, c} → B(3) = 5:
abca|bcb|acc|aba|b|ckatsayi, 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
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.
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.
- 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?
main, toplam(0)'ı çağırır. Taban durum yalnızca n == 1.
n = 0: 0 == 1 yanlış → 0 + toplam(-1) hesaplanmalı; yeni çağrı.
n = -1: yine 1 değil. n her çağrıda uzaklaşıyor: taban duruma hiç ulaşılmayacak.
Her çağrı yığına yeni bir çerçeve ekler; hiçbiri dönemez, çünkü hepsi bir sonrakinin sonucunu bekler.
Bin çağrı sonra yığında 1001 çerçeve var; hâlâ hata yok.
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
- Küçültme: problem, kendisinin daha küçük bir kopyası cinsinden ifade edilir.
- Taban durum: cevabı doğrudan bilinen en küçük girdi bulunur.
- 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.
- 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 / 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
main, rakamlarToplami(-357)'yi çağırır (şekilde kısaca r).
n = -357: negatif → ilk dal. İşaret düzeltilip r(357) çağrılır; bu çerçeve yalnızca sonucu aynen döndürecek.
n = 357: 10'dan büyük. 357 / 10 = 35 ile çağrı yapılır; son basamak 7 bekleyen toplama olarak kalır.
n = 35: r(3) çağrılır, 5 bekler.
n = 3 < 10: taban durum, 3 döner. Artık çerçeveler geri dönmeye başlar.
r(35) 3'ü aldı: 3 + 5 = 8 döner.
r(357) 8'i aldı: 8 + 7 = 15 döner.
r(-357) gelen 15'i değiştirmeden döndürür.
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
- 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
s = "racecar": uzunluk 7 > 1. İlk harf 'r' ile son harf 'r' karşılaştırılır → eşit.
Uçlar eşleşti; iç kısım s.substring(1, 6) = "aceca" için yeni çağrı yapılır.
s = "aceca": uzunluk 5 > 1. İlk harf 'a' ile son harf 'a' karşılaştırılır → eşit.
Uçlar eşleşti; iç kısım s.substring(1, 4) = "cec" için yeni çağrı yapılır.
s = "cec": uzunluk 3 > 1. İlk harf 'c' ile son harf 'c' karşılaştırılır → eşit.
Uçlar eşleşti; iç kısım s.substring(1, 2) = "e" için yeni çağrı yapılır.
Uzunluk 1 ≤ 1 → taban durum: tek harfli dizgi palindromdur, true döner.
palindromMu("cec") çerçevesi true aldı ve aynen true döndürür.
palindromMu("aceca") çerçevesi true aldı ve aynen true döndürür.
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ımgcd(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ı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
y = 93 ≠ 0. Kalan 444 % 93 = 72; yeni çağrı gcd(93, 72).
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
y = 72 ≠ 0. Kalan 93 % 72 = 21; yeni çağrı gcd(72, 21).
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
gcd(72, 21) | 72 | 21 | 9 |
y = 21 ≠ 0. Kalan 72 % 21 = 9; yeni çağrı gcd(21, 9).
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
gcd(72, 21) | 72 | 21 | 9 |
gcd(21, 9) | 21 | 9 | 3 |
y = 9 ≠ 0. Kalan 21 % 9 = 3; yeni çağrı gcd(9, 3).
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
gcd(72, 21) | 72 | 21 | 9 |
gcd(21, 9) | 21 | 9 | 3 |
gcd(9, 3) | 9 | 3 | 0 |
y = 3 ≠ 0. Kalan 9 % 3 = 0; yeni çağrı gcd(3, 0).
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
gcd(72, 21) | 72 | 21 | 9 |
gcd(21, 9) | 21 | 9 | 3 |
gcd(9, 3) | 9 | 3 | 0 |
gcd(3, 0) | 3 | 0 | — |
y == 0 → taban durum: x = 3 döner. EBOB(444, 93) = 3.
| çağrı | x | y | x % y |
|---|---|---|---|
gcd(444, 93) | 444 | 93 | 72 |
gcd(93, 72) | 93 | 72 | 21 |
gcd(72, 21) | 72 | 21 | 9 |
gcd(21, 9) | 21 | 9 | 3 |
gcd(9, 3) | 9 | 3 | 0 |
gcd(3, 0) | 3 | 0 | — |
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')
Başlangıç: 3 disk A'da. coz(3, 'A', 'C', 'B'): üç disk, B yardımcı olarak kullanılıp C'ye taşınacak.
En derindeki coz(1, A, C, B): taban durum, 1. disk doğrudan A'dan C'ye.
coz(2, A, B, C): üstündeki 1 disk kalktı; 2. disk A'dan B'ye.
2. diskin üstüne 1. disk: coz(1, C, B, A). Üstteki 2 disk artık B'de.
coz(3, …) ortadaki satıra geldi: en büyük disk (3) A'dan C'ye.
Şimdi B'deki 2 disk C'ye taşınacak: coz(2, B, C, A) → önce 1. disk B'den A'ya.
2. disk B'den C'ye, 3. diskin üstüne.
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.
faktoriyel(n) özyinelemeli: n çerçeve, O(n) bellek. Döngüyle: O(1).StackOverflowError alınır.Verimlilik
Tekrarlanan Hesaplama
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) |
|---|---|---|
| 10 | 177 | 55 |
| 20 | 21 891 | 6 765 |
| 30 | 2 692 537 | 832 040 |
| 40 | 331 160 281 | 102 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) → 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
main, palindromMu("Kepek")'i çağırır.
Parametre s = "Kepek": ilk harf büyük.
toLowerCase() yeni bir dizgi döndürür: s = "kepek". Artık K ile k farkı yok.
uzunluk = 5; döngü i < 5 / 2 = 2 olduğu sürece döner.
i = 0: baştan 0., sondan 5 − 1 − 0 = 4. indis karşılaştırılacak.
'k' == 'k': eşit, return false çalışmaz.
i = 1: 1. ve 3. indis.
'e' == 'e': eşit.
i = 2: 2 < 2 yanlış, döngü biter. Ortadaki 'p' kendisiyle eşleşir, bakmaya gerek yok.
Hiç farklı çift bulunmadı: true döner.
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ü?
| Özyineleme | Döngü (ilerlemeli) | |
|---|---|---|
| Durma | Taban durum | Döngü koşulu |
| İlerleme | Daha küçük argümanla çağrı | Sayaç / değişken güncelleme |
| Bellek | Her çağrı için bir çerçeve: O(derinlik) | Genellikle O(1) |
| Hız | Çağrı maliyeti; ağaç özyinelemesinde tekrar | Genellikle daha hızlı |
| Hata | Taban durum yoksa StackOverflowError | Koşul bozuksa sonsuz döngü (program donar) |
| Okunabilirlik | Ağaç, böl ve fethet, Hanoi, seriler: çok doğal | Basit sayma ve biriktirme işleri için doğal |
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ı
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.
int faktoriyel(int n) { return faktoriyel(n, 1); }Kuyruk Özyinelemesi · Adım adım kod izleme
faktoriyel(4, 1): Kuyruk Özyinelemesinin İzlenmesi
Çarpım çağrıdan önce yapılır: 4 * 1 = 4 yeni birikim olur; çağrı faktoriyel(3, 4).
Çarpım çağrıdan önce yapılır: 3 * 4 = 12 yeni birikim olur; çağrı faktoriyel(2, 12).
Çarpım çağrıdan önce yapılır: 2 * 12 = 24 yeni birikim olur; çağrı faktoriyel(1, 24).
Çarpım çağrıdan önce yapılır: 1 * 24 = 24 yeni birikim olur; çağrı faktoriyel(0, 24).
n == 0: taban durum. Sonuç zaten birikim'de hazır: 24 döner.
Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.
Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.
Dönüşte hiçbir hesap yapılmaz; 24 aynen yukarı aktarılır.
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
fibonacci(4) çağrıldı. Bellek boş: 4 orada yok, taban durum da değil → önce fibonacci(3) çağrılır.
fibonacci(3): bellekte yok → fibonacci(2) çağrılır.
fibonacci(2): bellekte yok → fibonacci(1) çağrılır.
n = 1: taban durum, 1 döner.
n = 0: taban durum, 0 döner.
fibonacci(2) = 1 + 0 = 1. Sonuç belleğe yazılır: {2=1}.
fibonacci(3)'ün ikinci çağrısı fibonacci(1): taban durum, 1.
fibonacci(3) = 1 + 1 = 2, belleğe yazılır.
İkinci fibonacci(2) çağrısı: bellekte var, hesaplanmadan 1 döner (belleklemesiz sürüm burada 3 çağrı daha yapardı).
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ı
Belleklemeli fibonacci(5): sol kol bir kez hesaplanır; sağdaki fib(2) ve fib(3) bellekten gelir. 15 yerine 9 çağrı.
| n | belleklemesiz çağrı | belleklemeli çağrı | belleklemesiz süre (ölçülen) | belleklemeli süre |
|---|---|---|---|---|
| 20 | 21 891 | 39 | ≈ 0,5 ms | < 0,1 ms |
| 40 | 331 160 281 | 79 | ≈ 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.
longile 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 sonrafaktoriyel(12)yalnızca 2 yeni çarpma yapar. - Bedel: bellek. Bellekleme, hız için bellek harcamaktır.
Özet
Özet
| Kavram | Özü |
|---|---|
| Özyineleme | Metodun kendini daha küçük bir girdiyle çağırması |
| Taban durum | Doğrudan çözülen en küçük durum; özyinelemeyi durdurur |
| İlerleme | Her ç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ç özyinelemesi | Birden çok çağrı (Fibonacci, Hanoi); çağrı sayısı üstel büyüyebilir |
| İlerlemeli çözüm | Döngü + biriktirici değişken; sabit bellek, genellikle daha hızlı |
| Kuyruk özyinelemesi | Son iş çağrıdır; sonuç biriktirici parametrede. Java TCO yapmaz |
| Bellekleme | Hesaplanan sonuçları Map'te saklayıp yeniden kullanmak |
Özet
Sık Yapılan Hatalar
toplam(0) gibi: Exception in thread "main" java.lang.StackOverflowError.
return n * faktoriyel(n); parametre küçülmez; yine StackOverflowError.
returnif … else if ile biten metot: error: missing return statement. Çözüm: son dal else olur.
faktoriyel(n - 1); tek başına yazılırsa değer kaybolur; return n * faktoriyel(n - 1); olmalı.
int faktöriyel 13!'te, Bell katsayısı 13'ten sonra yanlışlaşır; hata mesajı yoktur. Çözüm: long.
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); }
yaz(3)ne yazdırır?yaz2(3)ne yazdırır? Fark nereden gelir?gizem(4, 3)kaçtır? Metot ne hesaplar?gizem(4, -1)çağrısında ne olur?
Cevapları göster
- 3 2 1: yazdırma çağrıdan önce yapılır (inişte).
- 1 2 3: yazdırma çağrıdan sonra, dönüşte yapılır; en derindeki çerçeve önce yazar.
- 4 + 4 + 4 + 0 = 12:
a × b'yi toplama ile hesaplar. - 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); }
ters("java")ne döndürür?terskuyruk özyinelemeli midir? Yagcd?fibonacci(5)kaç kez çağrılır (ilk çağrı dahil)?gcd(48, 18)çağrılarını yazın.
Cevapları göster
- "avaj": ilk harf, kalanın tersinin sonuna eklenir: ters("ava") + 'j'.
tersdeğil: çağrı döndükten sonra birleştirme (+) yapılır.gcdevet: son iş çağrıdır.- 15 çağrı (çağrı ağacındaki düğüm sayısı).
- gcd(48, 18) → gcd(18, 12) → gcd(12, 6) → gcd(6, 0) → 6.
Özet
Alıştırmalar
long us(int taban, int us): tabanın üssünü özyinelemeyle hesaplayın.us(2, 10)= ?void ikili(int n): n'nin ikili (binary) gösterimini yazdırın.ikili(13)→ ?int diziToplam(int[] a, int i): dizinin i. indisten sonuna kadar toplamı.{3, 1, 4, 1, 5}→ ?- Fibonacci'yi döngüyle ve kuyruk özyinelemesiyle (
fibK(n, a, b)) yazın. lucas(5)kaç çağrı yapar? Bellekleme ile kaça iner?- Hanoi kulelerinde 4 disk için kaç hamle gerekir? İlk hamle hangisidir?
Cevaplar / ipuçları
if (us == 0) return 1; return taban * us(taban, us - 1);→ 1024.if (n > 1) ikili(n / 2); System.out.print(n % 2);→ 1101 (yazdırma dönüşte: önce yüksek basamaklar).if (i == a.length) return 0; return a[i] + diziToplam(a, i + 1);→ 14.- 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. - 15 çağrı (Fibonacci ile aynı ağaç biçimi); belleklemeli sürümde 9 (2n − 1).
- 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).