SEN2211 · 10. hafta · Recursion

Ö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.

recursionbase caserecursive casecall stackstack frameStackOverflowErrortail recursiontree recursionrecursive list traversalbacktrackingsubsetspermutationsN-Queens

Ö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:

  1. Taban durum (base case) olmadan özyineleme yazma. Önce onu yaz.
  2. Her çağrı tabana yaklaşmalı. n − 1, n / 2, p.next.
  3. Sonuca güven. size(p.next) doğru çalışıyor kabul et, sadece kendi adımını ekle.
  4. 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ı: n!=n⋅(n−1)!n! = n \cdot (n-1)! ve 1!=11! = 1. 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.

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

b10=(b5)2b^{10} = (b^5)^2, b5=(b2)2⋅bb^5 = (b^2)^2 \cdot b. 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 == null yapmak. 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 88≈168^8 \approx 16 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 == nullp == 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ğırmakhalf değişkenine al
Print yerini önemsememekÖnce → iniş sırası, sonra → dönüş sırası
Backtracking’de seçimi geri almamakcur.remove(...) olmadan sonraki dallar kirlenir
Özyineleme her zaman döngüden yavaştırListe 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ç.

Soru 1kolay

What does factorial(5) return?

static int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
Cevabı göster
120

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.

Soru 2kolay

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: 3 2 1, up: 1 2 3

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.

Soru 3orta

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
10 and 16

Son basamağı alır, kalanı özyinelemeye verir: basamak toplamı. 4 + 3 + 2 + 1 = 10. 7 + 0 + 9 = 16.

Soru 4orta

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
11 and 5

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.

Soru 5orta

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
5, 15 calls

Ç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.

Soru 6zor

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
1 2 1 3 1 2 1

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 2n−12^n - 1 sayı basılır (Hanoi kuleleri sırası).

Soru 7kolay

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
if p == null return 0; else return 1 + size(p.next)

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.

Soru 8orta

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
Recurse on p.next first, then print p.data.

Ö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.

Soru 9kolay

What happens when bad(5) is called?

static int bad(int n) {
    return n + bad(n - 1);
}
Cevabı göster
StackOverflowError

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ı.

Soru 10orta

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
If lo >= hi return true; if chars differ return false; else recurse on lo+1, hi-1.

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.

Soru 11zor

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
[1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []

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 23=82^3 = 8 alt küme. cur.remove satırı geri izlemenin (backtracking) kendisi.

Soru 12zor

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
ABC ACB BAC BCA CAB CBA

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ı.

Deftere not kartı

Her özyineleme = TABAN DURUM (dur) + ÖZYİNELEMELİ ADIM (küçült ve çağır) Taban yok ya da tabana yaklaşmıyor → StackOverflowError Print çağrıdan ÖNCE → iniş sırası (3 2 1); SONRA → dönüş sırası (1 2 3) Her çağrı bir yığın çerçevesi: derinlik = eş zamanlı açık çağrı sayısı (sumTo(5) → 6 çerçeve) Listede kalıp: if (p == null) return taban; return f(p.data) + rec(p.next) printReverse: önce rec(p.next), sonra print → listeyi bozmadan ters yazar pow(b, n-1): n+1 çağrı O(n) fastPow(b, n/2): ~log n çağrı (half'i SAKLA) Naif fib: fib(5) 15 çağrı, fib(25) 242 785 çağrı → üstel g(n-1); print; g(n-1) → 2^n - 1 çıktı (1 2 1 3 1 2 1) BACKTRACKING: seç → özyinele → seçimi GERİ AL → sonraki seçenek Alt kümeler 2^n, permütasyonlar n!, 4-queens 2 çözüm, 8-queens 92