Özyineleme (recursion)
Taban durum ve özyinelemeli adım, çağrı yığını izleri, bağlı listede özyinelemeli metotlar ve geri izlemeye (backtracking) giriş. Sınavda “what is printed”, çağrı sayısı ve özyinelemeli metot yazma soruları gelir.
Özyineleme (recursion), bir metodun kendini daha küçük bir girdiyle çağırmasıdır. İlk başta büyü gibi görünür ama arkasındaki mantık basit: problemi bir parça + aynı problemin küçük hali olarak yaz, en küçük hali doğrudan çöz. Bağlı liste zaten özyinelemeli bir yapıdır (bir düğüm + geri kalan liste), o yüzden liste metotları özyinelemeyle çok kısa yazılır. Sınav soruları: kodun ne bastığı, kaç çağrı yapıldığı ve kısa bir özyinelemeli metot yazmak. Bir sonraki hafta bu metotların karmaşıklığını hesaplayacağız.
Altın kurallar:
- Taban durum (base case) olmadan özyineleme yazma. Önce onu yaz.
- Her çağrı tabana yaklaşmalı. n − 1, n / 2, p.next.
- Sonuca güven.
size(p.next)doğru çalışıyor kabul et, sadece kendi adımını ekle. - Print’in yeri sırayı belirler. Çağrıdan önce mi sonra mı?
Taban durum ve özyinelemeli adım
static int factorial(int n) {
if (n <= 1) return 1; // taban durum: doğrudan cevap
return n * factorial(n - 1); // özyinelemeli adım: küçült ve çağır
}
Matematikteki tanımın aynısı: ve . factorial(5) 120, factorial(0) 1 döner.
Çağrı yığını (call stack)
Her metot çağrısı, kendi parametrelerini ve yerel değişkenlerini tutan bir yığın çerçevesi (stack frame) açar. Çağrı bitince çerçeve pop edilir ve dönüş değeri çağırana iletilir. 5. haftadaki yığının ta kendisi.
factorial(4) izinin program çıktısı (girinti = derinlik):
factorial(4) called
factorial(3) called
factorial(2) called
factorial(1) called
factorial(1) returns 1
factorial(2) returns 2
factorial(3) returns 6
factorial(4) returns 24
İniş sırasında hiçbir çarpma yapılmaz; hepsi bekler. Taban duruma ulaşınca dönüş başlar ve çarpmalar içten dışa yapılır: 1, 2 · 1, 3 · 2, 4 · 6.
çağrı yığını, en derin anda (tepe üstte):
| factorial(1) |
| factorial(2) | 2 * ? bekliyor
| factorial(3) | 3 * ? bekliyor
| factorial(4) | 4 * ? bekliyor
| main |
Taban durumu olmayan ya da tabana yaklaşmayan özyineleme bu yığını doldurur ve StackOverflowError alırsın. Varsayılan JVM ayarlarında bu birkaç bin ile birkaç on bin çağrı derinliğinde olur.
Print nerede?
static void down(int n) { if (n == 0) return; System.out.print(n + " "); down(n - 1); }
static void up(int n) { if (n == 0) return; up(n - 1); System.out.print(n + " "); }
3 2 1
1 2 3
down inerken basar, up dönerken basar. Çıktı sorusunda ilk sorman gereken: print özyinelemeli çağrıdan önce mi, sonra mı?
Birden fazla çağrı: ağaç özyinelemesi
Bir metot kendini iki kez çağırırsa çağrılar bir ağaç oluşturur ve sayıları hızla büyür.
static int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(5)=5 calls=15
fib(10)=55 calls=177
fib(20)=6765 calls=21891
fib(25)=75025 calls=242785
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2)
│ │ │ ├── fib(1)
│ │ │ └── fib(0)
│ │ └── fib(1)
│ └── fib(2)
│ ├── fib(1)
│ └── fib(0)
└── fib(3)
├── fib(2)
│ ├── fib(1)
│ └── fib(0)
└── fib(1)
fib(3) iki kez, fib(2) üç kez hesaplanıyor. Bu yüzden naif Fibonacci üsteldir; döngüyle ya da hesaplananları saklayarak (memoization) O(n) yapılır.
İki çağrının arasında print olursa ilginç bir desen çıkar:
static void g(int n) { if (n == 0) return; g(n - 1); System.out.print(n + " "); g(n - 1); }
1 2 1 3 1 2 1
Hız farkı: n − 1 mi, n / 2 mi?
static long pow(long b, int n) { // n+1 çağrı
if (n == 0) return 1;
return b * pow(b, n - 1);
}
static long fastPow(long b, int n) { // ~log n çağrı
if (n == 0) return 1;
long half = fastPow(b, n / 2);
return (n % 2 == 0) ? half * half : half * half * b;
}
1024 calls=11
1024 calls=5
, . Girdiyi yarıya indirmek, logaritmik derinlik demektir. 11. haftada bunu formülle göstereceğiz.
Bağlı listede özyineleme
Liste = boş (null) ya da bir düğüm + liste. Bu tanım doğrudan koda dönüşür:
static int size(Node p) { if (p == null) return 0; return 1 + size(p.next); }
static int sum(Node p) { if (p == null) return 0; return p.data + sum(p.next); }
static boolean contains(Node p, int x) {
if (p == null) return false;
if (p.data == x) return true;
return contains(p.next, x);
}
static void printReverse(Node p) {
if (p == null) return;
printReverse(p.next); // önce geri kalanı
System.out.print(p.data + " "); // sonra kendini
}
4 → 7 → 1 → 9 listesi için size, sum, contains(1), contains(5) ve printReverse:
4 21 true false
9 1 7 4
printReverse tek yönlü listeyi bozmadan tersten yazar; döngüyle bunu yapmak için ya listeyi ters çevirmen ya da bir yığın kullanman gerekirdi. Özyinelemede o yığın çağrı yığınının kendisi.
TUZAK · Taban durumu
p.next == nullyapmak.if (p.next == null) return 1;boş listede (p null) NullPointerException verir. Taban durumu en küçük girdi olan boş liste için yaz:p == null.
Geri izleme (backtracking) girişi
Bazı problemler “tüm olası seçimleri dene” ister: tüm alt kümeler, tüm permütasyonlar, satranç tahtasına vezir yerleştirme. Geri izleme şablonu:
çöz(durum):
eğer durum tamsa: kaydet / yazdır, dön
her seçenek için:
seçeneği uygula
çöz(yeni durum)
seçeneği GERİ AL
Tüm alt kümeler
static void subsets(int[] a, int i, List<Integer> cur) {
if (i == a.length) { System.out.print(cur + " "); return; }
cur.add(a[i]); // al
subsets(a, i + 1, cur);
cur.remove(cur.size() - 1); // geri al
subsets(a, i + 1, cur); // alma
}
[1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []
Permütasyonlar
static void perms(String prefix, String rest) {
if (rest.isEmpty()) { System.out.print(prefix + " "); return; }
for (int i = 0; i < rest.length(); i++)
perms(prefix + rest.charAt(i), rest.substring(0, i) + rest.substring(i + 1));
}
ABC ACB BAC BCA CAB CBA
N-Queens
n × n tahtaya n vezir, hiçbiri diğerini tehdit etmeyecek. Her satıra bir vezir koy; sütun ya da çapraz çakışıyorsa o sütunu atla, hiçbir sütun olmuyorsa bir önceki satıra dönüp onun vezirini kaydır. Programın saydığı çözüm sayıları:
4-queens: 2
5-queens: 10
6-queens: 4
7-queens: 40
8-queens: 92
Geri izleme kaba kuvvetten (brute force) iyidir çünkü çakışma görür görmez o dalı keser; 8 × 8’de milyon yerleşimin çok azını dener.
Sık yapılan hatalar
| Yanlış | Doğrusu |
|---|---|
| Taban durumu unutmak | İlk satır: if (taban) return ...; |
return yazmamak: factorial(n - 1); | Sonucu kullan: return n * factorial(n - 1); |
Listede taban p.next == null | p == null (boş liste) |
n küçülmüyor: f(n) içinde f(n) | f(n - 1) ya da f(n / 2) |
| fastPow’da yarıyı iki kez çağırmak | half değişkenine al |
| Print yerini önemsememek | Önce → iniş sırası, sonra → dönüş sırası |
| Backtracking’de seçimi geri almamak | cur.remove(...) olmadan sonraki dallar kirlenir |
| Özyineleme her zaman döngüden yavaştır | Liste ve ağaçta çoğu zaman daha sade, aynı O(n) |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
What does factorial(5) return?
static int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}Cevabı göster
5 · factorial(4) = 5 · 4 · factorial(3) = ... = 5 · 4 · 3 · 2 · 1 = 120. Taban durum n ≤ 1 olduğu için factorial(0) de 1 döner.
What do down(3) and up(3) print?
static void down(int n) {
if (n == 0) return;
System.out.print(n + " ");
down(n - 1);
}
static void up(int n) {
if (n == 0) return;
up(n - 1);
System.out.print(n + " ");
}Cevabı göster
down çağrıdan önce basar: 3'ü basar sonra 2'ye iner. up önce en dibe iner (up(0)), dönerken basar: 1, 2, 3. Print'in özyinelemeli çağrıya göre yeri sırayı belirler.
What do mystery(1234) and mystery(907) return?
static int mystery(int n) {
if (n == 0) return 0;
return n % 10 + mystery(n / 10);
}Cevabı göster
Son basamağı alır, kalanı özyinelemeye verir: basamak toplamı. 4 + 3 + 2 + 1 = 10. 7 + 0 + 9 = 16.
How many times is pow called (including the first call) to compute pow(2, 10)? How many for fastPow(2, 10)?
static long pow(long b, int n) {
if (n == 0) return 1;
return b * pow(b, n - 1);
}
static long fastPow(long b, int n) {
if (n == 0) return 1;
long half = fastPow(b, n / 2);
return (n % 2 == 0) ? half * half : half * half * b;
}Cevabı göster
pow n = 10, 9, ..., 0 için birer kez: 11 çağrı, O(n). fastPow n'yi her seferinde yarıya indirir: 10, 5, 2, 1, 0: 5 çağrı, O(log n). İkisi de 1024 döner. half'i bir değişkende tutmak önemli; fastPow(b, n/2) * fastPow(b, n/2) yazarsan çağrı sayısı yeniden O(n) olur.
With the naive recursive Fibonacci below, what is fib(5) and how many calls are made in total?
static int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}Cevabı göster
Çağrı ağacı: fib(5) → fib(4), fib(3); fib(4) → fib(3), fib(2); ... Aynı değerler tekrar tekrar hesaplanır. Çağrı sayısı C(n) = C(n−1) + C(n−2) + 1, C(5) = 15. fib(20) için 21 891, fib(25) için 242 785 çağrı: üstel büyüme.
What does g(3) print?
static void g(int n) {
if (n == 0) return;
g(n - 1);
System.out.print(n + " ");
g(n - 1);
}Cevabı göster
g(1) = 1. g(2) = g(1), 2, g(1) = 1 2 1. g(3) = g(2), 3, g(2) = 1 2 1 3 1 2 1. Her seviyede iki çağrı: toplam sayı basılır (Hanoi kuleleri sırası).
Write a recursive method static int size(Node p) that returns the number of nodes in the list starting at p. No loops.
Cevabı göster
Boş liste taban durumdur. Dolu liste = bir düğüm + geri kalan liste.
static int size(Node p) {
if (p == null) return 0;
return 1 + size(p.next);
}Aynı kalıpla toplam: return p.data + sum(p.next);. Liste 4 7 1 9 için size 4, sum 21.Write a recursive method static void printReverse(Node p) that prints a singly linked list from the last node to the first.
Cevabı göster
Önce listenin geri kalanını (ters) yazdır, sonra kendini. Çağrı yığını sırayı senin için ters çevirir.
static void printReverse(Node p) {
if (p == null) return;
printReverse(p.next);
System.out.print(p.data + " ");
}4 7 1 9 → 9 1 7 4. Liste değişmez. Bedeli O(n) yığın çerçevesi; çok uzun listede StackOverflowError riski var.What happens when bad(5) is called?
static int bad(int n) {
return n + bad(n - 1);
}Cevabı göster
Taban durum yok. Her çağrı yeni bir yığın çerçevesi (stack frame) açar ve hiçbiri dönmez. Çağrı yığını dolunca JVM StackOverflowError fırlatır. Her özyinelemeli metotta (1) taban durum olmalı ve (2) her çağrı tabana yaklaşmalı.
Write a recursive method static boolean isPal(String s, int lo, int hi) that checks whether s is a palindrome between indices lo and hi.
Cevabı göster
static boolean isPal(String s, int lo, int hi) {
if (lo >= hi) return true; // 0 veya 1 karakter kaldı
if (s.charAt(lo) != s.charAt(hi)) return false;
return isPal(s, lo + 1, hi - 1);
}isPal("level", 0, 4) → true, isPal("lever", 0, 4) → false: ilk adımda indeks 0 (l) ile indeks 4 (r) farklı. Çağrı isPal(s, 0, s.length() - 1) ile başlar.What does this backtracking method print for a = {1, 2, 3}?
static void subsets(int[] a, int i, List<Integer> cur) {
if (i == a.length) { System.out.print(cur + " "); return; }
cur.add(a[i]);
subsets(a, i + 1, cur);
cur.remove(cur.size() - 1);
subsets(a, i + 1, cur);
}Cevabı göster
Her eleman için iki seçenek: al, alma. Önce 'al' dalı tamamen keşfedilir. 1 al, 2 al, 3 al → [1,2,3]; 3'ü geri al → [1,2]; 2'yi geri al, 3 al → [1,3]; → [1]; sonra 1 alınmadan aynı şey: [2,3] [2] [3] []. Toplam alt küme. cur.remove satırı geri izlemenin (backtracking) kendisi.
In what order does this method print the permutations of "ABC"?
static void perms(String prefix, String rest) {
if (rest.isEmpty()) { System.out.print(prefix + " "); return; }
for (int i = 0; i < rest.length(); i++)
perms(prefix + rest.charAt(i),
rest.substring(0, i) + rest.substring(i + 1));
}Cevabı göster
Her seviyede kalan harflerden biri sırayla seçilip öne eklenir. A seçilince kalan BC: ABC, ACB. B seçilince kalan AC: BAC, BCA. C seçilince kalan AB: CAB, CBA. 3! = 6 permütasyon, sözlük sırasıyla.
Doğrulama: Bütün metotlar verify/SEN2211/W10.java içinde JDK ile çalıştırıldı (java W10.java). Çağrı yığını izi programın girinti yazdıran versiyonundan, çağrı sayıları (pow, fastPow, fib) metotların içindeki sayaçlardan, alt küme/permütasyon sırası ve N-Queens çözüm sayıları programın çıktısından alındı.