Özyineleme (recursion)
Kendini çağıran metotlar: base case, recursive step, çağrı yığınını izlemek ve faktöriyel, Fibonacci, üs alma, palindrom, ikili arama. Çıktı izleme sorularının ikinci büyük kaynağı.
Özyineleme (recursion), bir metodun problemi çözmek için kendini daha küçük bir girdiyle çağırmasıdır. Sınavda iki biçimde gelir: “What is printed?” (çağrı yığınını izleyip çıktı sırasını bulmak) ve “write a recursive method” (döngü kullanmadan). İkisinin de anahtarı aynı: her çağrının kendi değişkenleri olan ayrı bir kopya gibi çalıştığını görmek.
Altın kurallar:
- Base case olmadan özyineleme olmaz. En küçük durumda metot kendini çağırmadan döner.
- Her çağrı base case’e yaklaşmalı.
n - 1,n / 10,substring(1)gibi. - Her çağrının kendi değişkenleri var. Bir çağrıdaki n, diğerinin n’ini etkilemez.
- Çağrıdan sonra gelen satırlar ters sırada çalışır. En son çağrılan ilk biter.
Base case ve recursive step
Faktöriyel tanımı zaten özyinelemelidir: , ve .
static long factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive step
}
System.out.println(factorial(5) + " " + factorial(0) + " " + factorial(20));
120 1 2432902008176640000
long kullandık çünkü faktöriyel çok hızlı büyür; int ile 13! taşar. 20! long’un sığabildiği en büyük faktöriyeldir.
Çağrı yığını (call stack)
Her metot çağrısı yığına (stack) bir çerçeve (stack frame) ekler: parametreler, yerel değişkenler ve “dönünce nereden devam edeceğim” bilgisi. Metot dönünce çerçevesi yığından çıkar. Özyinelemede aynı metodun birçok çerçevesi aynı anda yığında durur.
Faktöriyele girinti ekleyip her çağrıyı ve dönüşü basarsak:
factorial(4)
factorial(3)
factorial(2)
factorial(1)
return 1
return 2
return 6
return 24
yığın (en üst = şu an çalışan)
┌────────────┐
│ f(1) → 1 │ base case, döner
┌────┴────────────┤
│ f(2) → 2 * 1 │
┌────┴─────────────────┤
│ f(3) → 3 * 2 │
┌────┴──────────────────────┤
│ f(4) → 4 * 6 = 24 │
└───────────────────────────┘
Çıktı sırası: önce mi, sonra mı?
İzleme sorularının çoğu bu tek fikre dayanır: özyinelemeli çağrıdan önce yazılan satırlar iniş sırasında, sonra yazılan satırlar dönüş sırasında (ters sırayla) çalışır.
static void printBefore(int n) {
if (n == 0) return;
System.out.print(n + " "); // önce bas
printBefore(n - 1);
}
static void printAfter(int n) {
if (n == 0) return;
printAfter(n - 1);
System.out.print(n + " "); // dönüşte bas
}
static void both(int n) {
if (n == 0) return;
System.out.print("<" + n);
both(n - 1);
System.out.print(n + ">");
}
printBefore(3); System.out.println();
printAfter(3); System.out.println();
both(3); System.out.println();
3 2 1
1 2 3
<3<2<11>2>3>
İki çağrı yapan metotlarda bir ağaç çiz. tree(n) önce tree(n-1), sonra n’yi basar, sonra yine tree(n-1):
tree(3)
├─ tree(2)
│ ├─ tree(1) → "1"
│ ├─ "2"
│ └─ tree(1) → "1"
├─ "3"
└─ tree(2) → "121"
sonuç: 1213121
Klasik örnekler
Fibonacci
, , .
static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
for (int i = 0; i <= 10; i++) System.out.print(fib(i) + " ");
0 1 1 2 3 5 8 13 21 34 55
Kısa ve okunaklı, ama çok yavaş: aynı değerleri tekrar tekrar hesaplar. Bir sayaçla toplam çağrı sayısını ölçtük:
| Hesap | Toplam fib çağrısı |
|---|---|
fib(5) | 15 |
fib(10) | 177 |
fib(30) | 2692537 |
Çağrı sayısı yaklaşık büyür. Aynı işi döngü adımda yapar. Sınavda “neden yavaş” sorusunun cevabı: tekrar eden alt problemler.
Üs alma
static double power(double base, int exp) {
if (exp == 0) return 1;
if (exp < 0) return 1 / power(base, -exp);
return base * power(base, exp - 1); // exp kadar çağrı
}
static long fastPower(long b, int e) {
if (e == 0) return 1;
long half = fastPower(b, e / 2); // problemi yarıya böl
return (e % 2 == 0) ? half * half : half * half * b;
}
System.out.println(power(2, 10) + " " + power(3, 0) + " " + power(2, -2));
System.out.println(fastPower(2, 30) + " calls=" + pcalls);
1024.0 1.0 0.25
1073741824 calls=6
Düz sürüm için 31 çağrı yapardı; yarıya bölen sürüm 6 çağrıda bitirdi (30, 15, 7, 3, 1, 0). Bu fikri ikili aramada da var.
Palindrom
static boolean isPalindrome(String s) {
if (s.length() <= 1) return true;
if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
return isPalindrome(s.substring(1, s.length() - 1));
}
System.out.println(isPalindrome("racecar") + " " + isPalindrome("abca") + " "
+ isPalindrome("") + " " + isPalindrome("noon"));
true false true true
İkili arama (binary search)
Sıralı dizide aranan değeri ortadaki elemanla karşılaştır; küçükse sol yarıda, büyükse sağ yarıda aynı işi tekrarla. Her çağrı arama aralığını yarıya indirir. (13. haftada döngülü sürümüyle karşılaştıracağız.)
static int binarySearch(int[] a, int key, int low, int high) {
if (low > high) return -1; // aralık boş: yok
int mid = (low + high) / 2;
System.out.println(" low=" + low + " high=" + high + " mid=" + mid + " a[mid]=" + a[mid]);
if (a[mid] == key) return mid;
if (key < a[mid]) return binarySearch(a, key, low, mid - 1);
return binarySearch(a, key, mid + 1, high);
}
int[] a = {3, 8, 15, 21, 30, 42, 57, 66, 80};
System.out.println("found at " + binarySearch(a, 57, 0, a.length - 1));
System.out.println("found at " + binarySearch(a, 10, 0, a.length - 1));
low=0 high=8 mid=4 a[mid]=30
low=5 high=8 mid=6 a[mid]=57
found at 6
low=0 high=8 mid=4 a[mid]=30
low=0 high=3 mid=1 a[mid]=8
low=2 high=3 mid=2 a[mid]=15
found at -1
İz satırları “found at” satırından önce basılıyor: println’ın argümanındaki birleştirme, binarySearch tamamen bitince yapılabilir.
Diğer kalıplar
static int sumDigits(int n) { // 9045 → 9+0+4+5
if (n < 10) return n;
return n % 10 + sumDigits(n / 10);
}
static int sumArray(int[] a, int i) { // dizi + indeks parametresi
if (i == a.length) return 0;
return a[i] + sumArray(a, i + 1);
}
static int countX(String s) { // String'i harf harf küçült
if (s.isEmpty()) return 0;
return (s.charAt(0) == 'x' ? 1 : 0) + countX(s.substring(1));
}
System.out.println(sumDigits(9045));
System.out.println(sumArray(new int[]{4, 7, 1, 8}, 0));
System.out.println(countX("xaxbxx"));
18
20
4
Özyineleme mi, döngü mü?
| Özyineleme | Döngü (iteration) | |
|---|---|---|
| Okunaklılık | Doğası özyinelemeli problemlerde (ağaç, böl-yönet) çok temiz | Basit tekrarlarda daha açık |
| Bellek | Her çağrı bir yığın çerçevesi | Sabit |
| Hız | Çağrı maliyeti var; naif fib gibi tekrar eden hesaplarda felaket | Genelde daha hızlı |
| Risk | Base case hatası → StackOverflowError | Koşul hatası → sonsuz döngü |
| Dönüşüm | Her özyineleme döngüye çevrilebilir | Her döngü özyinelemeye çevrilebilir |
TUZAK · base case’i atlamak.
m2(n)metodun + m2(n - 2)döndürüyorsa ve base casen == 0ise, tek sayılar 0’ı hiç görmez (7, 5, 3, 1, −1, …) ve yığın taşar.n <= 0yazmak bu yüzden daha güvenli. Base case yazarken “her girdi buraya ulaşıyor mu?” diye sor.
Base case’siz bir metot (return n + bad(n - 1);) derlenir ama çalışınca StackOverflowError fırlatır; denemede tam olarak bu oldu. Her yolun return etmediği bir metot ise hiç derlenmez: missing return statement.
Sık yapılan hatalar
| Yanlış düşünce | Doğrusu |
|---|---|
| Base case olmasa da bir yerde durur | StackOverflowError |
| Çağrıdan sonraki satırlar da ilk sırada çalışır | Dönüşte, ters sırada |
Bütün çağrılar aynı n’i paylaşır | Her çağrının kendi n’i var |
fib(5) 5 çağrı yapar | 15 çağrı; ağaç üstel büyür |
factorial int ile her n için çalışır | 13! int’e sığmaz; long kullan |
return n + sum(n-1) yerine sum(n-1) + n; | Değer döndürülmeli; tek başına ifade not a statement |
| Her yol return etmese de olur | missing return statement |
| İkili arama sırasız dizide de çalışır | Dizi sıralı olmalı |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
What is printed?
static void printBefore(int n) {
if (n == 0) return;
System.out.print(n + " ");
printBefore(n - 1);
}
static void printAfter(int n) {
if (n == 0) return;
printAfter(n - 1);
System.out.print(n + " ");
}
// in main:
printBefore(3); System.out.println();
printAfter(3);Cevabı göster
printBefore önce basar sonra iner: 3, 2, 1. printAfter önce en dibe iner (3 → 2 → 1 → 0), basma işi geri dönerken yapılır: en son çağrılan (1) ilk basar. Özyinelemeli çağrıdan SONRA gelen satırlar ters sırada çalışır.
What is printed by both(3);?
static void both(int n) {
if (n == 0) return;
System.out.print("<" + n);
both(n - 1);
System.out.print(n + ">");
}Cevabı göster
İnerken "<n" basılır: <3 <2 <1. both(0) hemen döner. Dönerken "n>" basılır, en içten dışa: 1> 2> 3>. Birleşince <3<2<11>2>3> (ortadaki "11", "<1" ve "1>" yan yana).
What does factorial(4) return, and how deep does the call stack get?
static long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}Cevabı göster
factorial(4) → 4 · factorial(3) → 3 · factorial(2) → 2 · factorial(1) = 1. En dipte 4 çerçeve (frame) aynı anda yığında. Dönüşte: 1, 2 · 1 = 2, 3 · 2 = 6, 4 · 6 = 24.
How many times is fib called in total when computing fib(5)?
static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}Cevabı göster
Her çağrı (n ≥ 2 ise) iki çağrı daha yapar. Sayı C(n) = 1 + C(n−1) + C(n−2), C(0) = C(1) = 1: C(2) = 3, C(3) = 5, C(4) = 9, C(5) = 15. Sonuç fib(5) = 5. Denemede fib(10) için 177, fib(30) için 2.692.537 çağrı sayıldı: üstel büyüme.
What is printed?
static int mystery(int a, int b) {
if (b == 0) return a;
return mystery(b, a % b);
}
// in main:
System.out.println(mystery(48, 18) + " " + mystery(17, 5));Cevabı göster
Bu Öklid algoritması: en büyük ortak bölen (GCD). (48, 18) → (18, 12) → (12, 6) → (6, 0) → 6. (17, 5) → (5, 2) → (2, 1) → (1, 0) → 1.
What is printed?
static String reverse(String s) {
if (s.isEmpty()) return s;
return reverse(s.substring(1)) + s.charAt(0);
}
// in main:
System.out.println(reverse("stack"));Cevabı göster
Her adım ilk harfi ayırıp geri kalanı ters çevirir, ilk harfi sona ekler: reverse("stack") = reverse("tack") + 's' = (reverse("ack") + 't') + 's' = ... = "kcats".
What is printed?
static int m2(int n) {
if (n <= 0) return 0;
return n + m2(n - 2);
}
// in main:
System.out.println(m2(7) + " " + m2(6));Cevabı göster
m2(7) = 7 + 5 + 3 + 1 + m2(−1) = 16. m2(6) = 6 + 4 + 2 + m2(0) = 12. Base case n <= 0 yerine n == 0 yazılsaydı tek sayılar 0'ı atlayıp sonsuza iner ve StackOverflowError olurdu.
Find the compile error.
static int sum(int n) {
if (n == 0) return 0;
else if (n > 0) return n + sum(n - 1);
}Cevabı göster
Derleyici her yolun bir değer döndürdüğünü ister. n < 0 olduğunda hiçbir return'e girilmiyor. javac: missing return statement. Çözüm: son else if'i else yap ya da sona bir return ekle.
What happens when bad(5) is called?
static int bad(int n) {
return n + bad(n - 1);
}Cevabı göster
Base case yok: metot kendini sonsuza kadar çağırır, her çağrı yığına yeni bir çerçeve ekler, yığın dolar ve JVM StackOverflowError fırlatır. Derleme hatası değildir. Error, Exception'ın değil Throwable'ın alt sınıfıdır.
A recursive binary search looks for 57 in {3, 8, 15, 21, 30, 42, 57, 66, 80} with mid = (low + high) / 2. Give the (low, high, mid) of each call.
Cevabı göster
İlk çağrı low = 0, high = 8, mid = 4, a[4] = 30. 57 > 30, sağ yarıya git: low = 5. mid = (5 + 8) / 2 = 6, a[6] = 57: bulundu. 2 çağrı. 10 aransaydı: (0,8,4) → (0,3,1) → (2,3,2) → low = 2 > high = 1: −1, toplam 4 çağrı.
What is printed by tree(3);?
static void tree(int n) {
if (n == 0) return;
tree(n - 1);
System.out.print(n);
tree(n - 1);
}Cevabı göster
tree(1) = "1". tree(2) = tree(1) + "2" + tree(1) = "121". tree(3) = tree(2) + "3" + tree(2) = "1213121". Her seviye iki çağrı yaptığı için uzunluk 2 üzeri n eksi 1: 7 karakter.
Write a recursive method boolean isPalindrome(String s) (no loops). isPalindrome("racecar") is true, isPalindrome("abca") is false, and the empty string is a palindrome.
Cevabı göster
İki base case var: boş ya da tek karakter (true) ve uçlar farklı (false). Recursive step problemi her seferinde iki karakter küçültür:
public static boolean isPalindrome(String s) {
if (s.length() <= 1) return true; // base case
if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
return isPalindrome(s.substring(1, s.length() - 1)); // ortası
}Denemede "racecar" true, "abca" false, "" true, "noon" true.Doğrulama: Tüm kod örnekleri, izler ve çağrı sayıları JDK 26 ile çalıştırıldı (java verify/SEN1002/W12.java); derleme hataları javac ile denendi (verify/SEN1002/err/E12a-b.java).