Bölüm 4: Özyineleme

Veri Yapıları
Taban durum ve özyinelemeli adımÇağrı yığınıDoğrusal özyineleme Hızlı üs almaFibonacci ve ikili aramaHanoi kuleleri Geri izlemeYineleme bağıntılarıKuyruk özyinelemesiBağlı listede özyineleme
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Özyinelemeli tasarım

Bir problemi taban durum ve özyinelemeli adımla tanımlayıp Java'da yazmak.

Çağrı yığını

Özyinelemeli bir metodu çerçeve çerçeve izleyip çıktısını ve dönüş değerini bulmak.

Özyineleme türleri

Doğrusal, ikili ve çoklu özyinelemeyi ve geri izlemeyi örnekleriyle ayırt etmek.

Maliyet

Yineleme bağıntısını yazıp çağrı ağacıyla zaman ve alan maliyetini bulmak; gerektiğinde döngüye çevirmek.

Özyineleme Nedir

Günlük Hayatta Özyineleme

Uzun bir kuyrukta kaçıncı sırada olduğunuzu öğrenmek istiyorsunuz ama yerinizden ayrılıp baştan sayamıyorsunuz.

54321“Sen kaçıncısın?”en önde: “1”cevap geri gelirken her kişi +1 eklerben
Aynı soru, küçük kuyrukHerkes önündekine aynı soruyu sorar; soru her seferinde bir kişi kısa bir kuyruk için sorulur.
Taban durumEn öndeki kişi kimseye sormadan cevap verir: “1”. Sorular burada durur.
Geri dönüşCevap geri gelirken her kişi kendi payını ekler: 1, 2, 3, 4, 5.
Özyinelemeli bir metot da böyle çalışır: kendini daha küçük bir girdiyle çağırır, en küçük girdide cevabı doğrudan verir.

Özyineleme Nedir

Özyineleme (Recursion)

n! = 1 (n ≤ 1) · n! = n · (n − 1)! (n > 1)
  • Özyineleme: bir metodun kendini daha küçük bir girdiyle çağırması.
  • Taban durum (base case): özyinelemesiz, doğrudan çözülür. Olmazsa çağrılar bitmez → StackOverflowError.
  • Özyinelemeli adım: problemi küçültür ve taban duruma yaklaştırır; küçük problemin cevabından büyüğünkini kurar.
  • Her çağrı çağrı yığınında yeni bir çerçeve açar (kendi n'si).
faktoriyel(4) = 4 · faktoriyel(3)= 24faktoriyel(3) = 3 · faktoriyel(2)= 6faktoriyel(2) = 2 · faktoriyel(1)= 2faktoriyel(1) = 1 (taban durum)= 1↓ çağrılar iner · ↑ değerler döner

Özyineleme Nedir

Özyinelemeli Düşünmek

  1. Taban durum: en küçük girdi hangisi ve cevabı ne? Burada özyineleme durur.
  2. Küçültme: problem, aynı türden daha küçük bir probleme nasıl indirgenir? (n − 1, n / 2, i + 1 …)
  3. Birleştirme: küçük problemin cevabından büyük problemin cevabı nasıl kurulur?

Küçük problemin doğru çözüldüğü kabul edilir ve yalnızca tek bir adım düşünülür. Taban durum doğruysa ve her adım doğruysa tüm zincir doğrudur (tümevarımdaki mantık).

void geriSay(int n) {
  if (n == 0) {     // taban durum
    System.out.println("Bitti");
    return;
  }
  System.out.println(n);
  geriSay(n - 1);   // özyinelemeli adım
}
geriSay(3);         // main içinde

geriSay(n): n'den 1'e kadar yaz, sonra “Bitti”.

Her özyinelemeli çağrı girdiyi taban duruma yaklaştırmalıdır; geriSay'de n her çağrıda 1 azalır ve 0'a ulaşır.

Özyineleme Nedir · Kodu adım adım çalıştır

Kod İzleme: geriSay(3)

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

main, geriSay(3)'ü çağırır. Yığına yeni bir çerçeve eklenecek.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

n = 3 ≠ 0: taban durum değil. 3 yazılır, sonra geriSay(2) çağrılır; bu çerçeve bekler.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

n = 2 ≠ 0: taban durum değil. 2 yazılır, sonra geriSay(1) çağrılır; bu çerçeve bekler.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

n = 1 ≠ 0: taban durum değil. 1 yazılır, sonra geriSay(0) çağrılır; bu çerçeve bekler.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

n = 0: taban durum. “Bitti” yazılır. Yığın en derinde: main + 4 çerçeve.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bitti→ çağrı · ← dönüş

return: geriSay(0) biter, çerçevesi yığından çıkar.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1geriSay(0)yaz Bittidöndü→ çağrı · ← dönüş

geriSay(1), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.

geriSay(3)yaz 3geriSay(2)yaz 2geriSay(1)yaz 1döndügeriSay(0)yaz Bittidöndü→ çağrı · ← dönüş

geriSay(2), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.

geriSay(3)yaz 3geriSay(2)yaz 2döndügeriSay(1)yaz 1döndügeriSay(0)yaz Bittidöndü→ çağrı · ← dönüş

geriSay(3), satır 7'deki çağrıdan döner. Yapılacak iş kalmadı → o da biter.

geriSay(3)yaz 3döndügeriSay(2)yaz 2döndügeriSay(1)yaz 1döndügeriSay(0)yaz Bittidöndü→ çağrı · ← dönüş

Tüm çerçeveler döndü. 4 çağrı yapıldı (n = 3, 2, 1, 0); çıktı: 3 2 1 Bitti.

void geriSay(int n) {
  if (n == 0) {     // taban durum
    System.out.println("Bitti");
    return;
  }
  System.out.println(n);
  geriSay(n - 1);   // özyinelemeli adım
}
geriSay(3);         // main içinde

Özyineleme Nedir

İş Çağrıdan Önce mi, Sonra mı?

void geriSay2(int n) {
  if (n == 0) return;
  System.out.print(n + " ");
  geriSay2(n - 1);     // sonra çağrı
}

geriSay2(3) çıktısı: 3 2 1
Yazdırma çağrılar inerken yapılır.

void ileriSay(int n) {
  if (n == 0) return;
  ileriSay(n - 1);     // önce çağrı
  System.out.print(n + " ");
}

ileriSay(3) çıktısı: 1 2 3
Yazdırma çağrılar dönerken yapılır.

  • ileriSay(3), 3'ü yazmadan önce ileriSay(2)'nin bitmesini bekler; o da 2'yi yazmadan önce ileriSay(1)'i bekler.
  • İlk biten çağrı en son başlayandır (n = 0). Bu yüzden çağrıdan sonra yapılan işler ters sırayla yapılır: 1, 2, 3.
Çağrıdan sonra kalan iş, çerçeve yığında beklerken saklanır. Bu iş yığın çözülürken yapılır.

Çağrı Yığını

Çağrı Yığını ve Çerçeveler

  • Her metot çağrısı için bellekte bir çerçeve (frame) açılır: parametreler, yerel değişkenler ve dönüşte devam edilecek yer.
  • Çerçeveler çağrı yığınında tutulur: çağrı yapılınca üste eklenir (push), metot bitince üstten çıkar (pop). Son başlayan ilk biter: LIFO.
  • Yalnızca en üstteki çerçeve çalışır; alttakiler, üstlerindeki çağrının sonucunu bekler.
  • Özyinelemede aynı metodun birden çok çerçevesi aynı anda yığında bulunur; her birinin n'si ayrıdır.
faktoriyel(1)n = 1return 1faktoriyel(2)n = 2bekliyor: 2 · ?faktoriyel(3)n = 3bekliyor: 3 · ?main()bekliyor: println(?)← üsther çerçevenin kendi n değişkeni var
Özyinelemeli bir metodun bellek maliyeti, yığında aynı anda bulunan çerçeve sayısıyla, yani en derin çağrı zinciriyle belirlenir.

Çağrı Yığını · Kodu adım adım çalıştır

Kod İzleme: faktoriyel(4) ve çağrı yığını

→ çağrı · ← dönen değer

main, faktoriyel(4)'ü çağırır.

faktoriyel(4)n = 4→ çağrı · ← dönen değer

Yeni çerçeve: n = 4. 4 ≤ 1 mi? Hayır.

faktoriyel(4)n = 4→ çağrı · ← dönen değer

4 · faktoriyel(3): çarpım için önce faktoriyel(3) bitmeli; bu çerçeve bekler.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)n = 3→ çağrı · ← dönen değer

Yeni çerçeve: n = 3. 3 ≤ 1 mi? Hayır.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)n = 3→ çağrı · ← dönen değer

3 · faktoriyel(2): çarpım için önce faktoriyel(2) bitmeli; bu çerçeve bekler.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)3 · ? (bekliyor)faktoriyel(2)n = 2→ çağrı · ← dönen değer

Yeni çerçeve: n = 2. 2 ≤ 1 mi? Hayır.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)3 · ? (bekliyor)faktoriyel(2)n = 2→ çağrı · ← dönen değer

2 · faktoriyel(1): çarpım için önce faktoriyel(1) bitmeli; bu çerçeve bekler.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)3 · ? (bekliyor)faktoriyel(2)2 · ? (bekliyor)faktoriyel(1)taban durum→ çağrı · ← dönen değer

Yeni çerçeve: n = 1. 1 ≤ 1 mi? Evet.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)3 · ? (bekliyor)faktoriyel(2)2 · 1faktoriyel(1)taban durum= 1→ çağrı · ← dönen değer

Taban durum: return 1. Artık çağrı yok; değerler geri dönmeye başlar.

faktoriyel(4)4 · ? (bekliyor)faktoriyel(3)3 · 2faktoriyel(2)2 · 1= 2faktoriyel(1)taban durum= 1→ çağrı · ← dönen değer

faktoriyel(1) = 1 döndü → 2 · 1 = 2, return 2.

faktoriyel(4)4 · 6faktoriyel(3)3 · 2= 6faktoriyel(2)2 · 1= 2faktoriyel(1)taban durum= 1→ çağrı · ← dönen değer

faktoriyel(2) = 2 döndü → 3 · 2 = 6, return 6.

faktoriyel(4)4 · 6= 24faktoriyel(3)3 · 2= 6faktoriyel(2)2 · 1= 2faktoriyel(1)taban durum= 1→ çağrı · ← dönen değer

faktoriyel(3) = 6 döndü → 4 · 6 = 24, return 24.

faktoriyel(4)4 · 6= 24faktoriyel(3)3 · 2= 6faktoriyel(2)2 · 1= 2faktoriyel(1)taban durum= 1→ çağrı · ← dönen değer

Ekrana 24. 4 çağrı, yığın derinliği en fazla 4 + main → zaman ve bellek O(n).

int faktoriyel(int n) {
  if (n <= 1)
    return 1;               // taban durum
  return n * faktoriyel(n - 1);
}

// main içinde:
System.out.println(faktoriyel(4));

Çağrı Yığını

Yığın Taşması: StackOverflowError

long topla(int n) {        // 1 + 2 + … + n
  if (n == 0) return 0;
  return n + topla(n - 1);
}
// main içinde:
System.out.println(topla(10_000));   // 50005000
System.out.println(topla(1_000_000)); // hata
Exception in thread "main"
  java.lang.StackOverflowError
    at Ornek.topla(Ornek.java:3)
    at Ornek.topla(Ornek.java:3)
    …
  • Çağrı yığını sınırlı bir bellek alanıdır. Boyutu JVM'e ve işletim sistemine bağlıdır; -Xss seçeneğiyle değiştirilebilir.
  • Her çerçeve yer kaplar. Varsayılan ayarlarla derinlik genellikle birkaç bin ile birkaç on bin çağrıyla sınırlıdır.
  • topla(10_000) sığar; topla(1_000_000) taban duruma ulaşmadan yığını doldurur.
  • Aynı toplam bir döngüyle ya da n(n + 1)/2 formülüyle sorunsuz hesaplanır.

Çağrı Yığını

Taban Durumu Unutmak

Taban durum yok
int f(int n) {
  return n * f(n - 1);
}
n = 3, 2, 1, 0, −1, −2 … çağrılar hiç durmaz.
Taban duruma ulaşılamıyor
if (n == 1) return 1;
return n * f(n - 1);
f(0) → −1 → −2 …: n hiçbir zaman 1 olmaz. n <= 1 bu girdiyi de yakalar.
Problem küçülmüyor
if (n <= 1) return 1;
return n * f(n);
Aynı girdiyle tekrar çağrı: taban durum var ama ona hiç yaklaşılmıyor.
Üçünün sonucu da aynıdır: yığın dolar ve StackOverflowError oluşur. Her olası girdi (0 ve negatif değerler dahil) sonlu sayıda adımda bir taban duruma ulaşmalıdır.

Doğrusal Özyineleme

Doğrusal Özyineleme

  • Her çağrı en fazla bir özyinelemeli çağrı yapar; çağrılar dallanmayan bir zincir oluşturur.
  • n boyutlu problem, n − 1 (ya da n / 2) boyutlu tek bir probleme indirgenir.
  • Örnekler: faktoriyel, geriSay, dizinin toplamı, diziyi tersine çevirme, üs alma, bağlı listenin uzunluğu.
  • Dizinin toplamı: ilk n öğenin toplamı = ilk n − 1 öğenin toplamı + a[n − 1]. Taban durum: n = 0 (boş parça) → 0.
40316223toplam(a, 3) = 13a[3] = 2toplam(a, 4) = 13 + 2 = 15
int toplam(int[] a, int n) {   // ilk n öğenin toplamı
  if (n == 0)
    return 0;                   // taban durum
  return toplam(a, n - 1) + a[n - 1];
}

Doğrusal Özyineleme · Kodu adım adım çalıştır

Kod İzleme: toplam(a, 4)

40316223toplam(a, 4) = ?dönen değerler: —

main, toplam(a, 4)'ü çağırır: dizinin ilk 4 öğesinin toplamı.

40316223a[3]toplam(a, 4) = toplam(a, 3) + a[3]dönen değerler: —

n = 4 ≠ 0 → önce toplam(a, 3) hesaplanmalı; a[3] = 2 toplamaya eklenmeyi bekler.

40316223a[2]toplam(a, 3) = toplam(a, 2) + a[2]dönen değerler: —

n = 3 ≠ 0 → önce toplam(a, 2) hesaplanmalı; a[2] = 6 toplamaya eklenmeyi bekler.

40316223a[1]toplam(a, 2) = toplam(a, 1) + a[1]dönen değerler: —

n = 2 ≠ 0 → önce toplam(a, 1) hesaplanmalı; a[1] = 3 toplamaya eklenmeyi bekler.

40316223a[0]toplam(a, 1) = toplam(a, 0) + a[0]dönen değerler: —

n = 1 ≠ 0 → önce toplam(a, 0) hesaplanmalı; a[0] = 4 toplamaya eklenmeyi bekler.

40316223toplam(a, 0) = 0 (boş parça)dönen değerler: 0

n = 0: taban durum → 0 döner. Yığın en derinde: main + 5 çerçeve.

40316223a[0]toplam(a, 1) = 0 + 4 = 4dönen değerler: 0 → 4

toplam(a, 0) 0 döndürdü → 0 + a[0] = 0 + 4 = 4 döner.

40316223a[1]toplam(a, 2) = 4 + 3 = 7dönen değerler: 0 → 4 → 7

toplam(a, 1) 4 döndürdü → 4 + a[1] = 4 + 3 = 7 döner.

40316223a[2]toplam(a, 3) = 7 + 6 = 13dönen değerler: 0 → 4 → 7 → 13

toplam(a, 2) 7 döndürdü → 7 + a[2] = 7 + 6 = 13 döner.

40316223a[3]toplam(a, 4) = 13 + 2 = 15dönen değerler: 0 → 4 → 7 → 13 → 15

toplam(a, 3) 13 döndürdü → 13 + a[3] = 13 + 2 = 15 döner.

40316223a[3]toplam(a, 4) = 13 + 2 = 15dönen değerler: 0 → 4 → 7 → 13 → 15

Sonuç 15. 5 çağrı (n = 4, 3, 2, 1, 0): zaman O(n), en çok 5 çerçeve (+ main): bellek O(n).

int toplam(int[] a, int n) {   // ilk n öğenin toplamı
  if (n == 0)
    return 0;                   // taban durum
  return toplam(a, n - 1) + a[n - 1];
}
// main içinde:
int[] a = {4, 3, 6, 2};
System.out.println(toplam(a, 4));

Doğrusal Özyineleme

Diziyi Tersine Çevirme

  • Fikir: dizinin iki ucunu değiştir, aradaki parçayı aynı yöntemle tersine çevir.
  • Parça a[i..j] indisleriyle verilir; ilk çağrı ters(a, 0, n − 1).
  • Taban durum: i >= j, yani parçada 0 ya da 1 öğe kaldı; yapılacak iş yok.
  • Her çağrı parçayı 2 öğe kısaltır: n öğe için ⌊n/2⌋ değiştirme, ⌊n/2⌋ + 1 çağrı → O(n).
  • Dizi yerinde değişir; metot bir değer döndürmez (void).
2071429314651.2.3.sonuç: 6 1 9 4 7 2

Doğrusal Özyineleme · Kodu adım adım çalıştır

Kod İzleme: ters(a, 0, 5)

207142931465a = {2, 7, 4, 9, 1, 6}

main, ters(a, 0, 5)'i çağırır.

607142931425ija[0] ↔ a[5] değişti

i = 0 < j = 5 → a[0] ile a[5] değişir: dizi 6 7 4 9 1 2. Sonra ters(a, 1, 4) çağrılır.

601142937425ija[1] ↔ a[4] değişti

i = 1 < j = 4 → a[1] ile a[4] değişir: dizi 6 1 4 9 7 2. Sonra ters(a, 2, 3) çağrılır.

601192437425ija[2] ↔ a[3] değişti

i = 2 < j = 3 → a[2] ile a[3] değişir: dizi 6 1 9 4 7 2. Sonra ters(a, 3, 2) çağrılır.

601192437425iji = 3 ≥ j = 2 → taban durum

i = 3 ≥ j = 2: taban durum, ortada çevrilecek öğe kalmadı. Yığında main + 4 çerçeve.

601192437425tersine çevrilmiş dizi

Çerçeveler sırayla döner; dönüşte yapılacak iş yok. Sonuç: 6 1 9 4 7 2. 3 değiştirme, 4 çağrı.

void ters(int[] a, int i, int j) {
  if (i >= j)
    return;                    // taban: 0 ya da 1 öğe
  int t = a[i]; a[i] = a[j]; a[j] = t;  // uçlar
  ters(a, i + 1, j - 1);       // içteki parça
}
// main içinde:
int[] a = {2, 7, 4, 9, 1, 6};
ters(a, 0, a.length - 1);

Doğrusal Özyineleme

Üs Alma: Yavaş ve Hızlı

Yavaş: xⁿ = x · xⁿ⁻¹, x⁰ = 1

long us(long x, int n) {
  if (n == 0) return 1;
  return x * us(x, n - 1);
}

Hızlı: üssü her adımda yarıya indir

xⁿ = (xn/2)²  (n çift)
xⁿ = x · (x(n−1)/2)²  (n tek)

2¹⁰ = (2⁵)²,  2⁵ = 2 · (2²)²,
2² = (2¹)²,  2¹ = 2 · (2⁰)²

nus: çağrıhizliUs: çağrı
10115
1001018
1 0001 00111
10⁶1 000 00121

Yavaş sürüm n + 1 çağrı yapar: O(n). Hızlı sürümde n her çağrıda yarıya iner: ⌊log₂ n⌋ + 2 çağrı, O(log n).

Doğrusal Özyineleme · Kodu adım adım çalıştır

Kod İzleme: hizliUs(2, 10)

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1→ çağrı · ← dönüş

main, hizliUs(2, 10)'u çağırır: 2¹⁰.

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1→ çağrı · ← dönüş

n = 10 ≠ 0 → önce y = hizliUs(2, 5) hesaplanır (10 / 2 = 5).

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1→ çağrı · ← dönüş

n = 5 ≠ 0 → önce y = hizliUs(2, 2) hesaplanır (5 / 2 = 2).

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1→ çağrı · ← dönüş

n = 2 ≠ 0 → önce y = hizliUs(2, 1) hesaplanır (2 / 2 = 1).

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1→ çağrı · ← dönüş

n = 1 ≠ 0 → önce y = hizliUs(2, 0) hesaplanır (1 / 2 = 0).

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · yhizliUs(2,0)1= 1→ çağrı · ← dönüş

n = 0: taban durum → 1 döner (2⁰ = 1).

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · yhizliUs(2,1)2 · y · y= 2hizliUs(2,0)1= 1→ çağrı · ← dönüş

y = 1 geldi. n = 1 tek → 2 · y · y = 2 · 1 · 1 = 2 döner.

hizliUs(2,10)y · yhizliUs(2,5)2 · y · yhizliUs(2,2)y · y= 4hizliUs(2,1)2 · y · y= 2hizliUs(2,0)1= 1→ çağrı · ← dönüş

y = 2 geldi. n = 2 çift → y · y = 2 · 2 = 4 döner.

hizliUs(2,10)y · yhizliUs(2,5)2 · y · y= 32hizliUs(2,2)y · y= 4hizliUs(2,1)2 · y · y= 2hizliUs(2,0)1= 1→ çağrı · ← dönüş

y = 4 geldi. n = 5 tek → 2 · y · y = 2 · 4 · 4 = 32 döner.

hizliUs(2,10)y · y= 1024hizliUs(2,5)2 · y · y= 32hizliUs(2,2)y · y= 4hizliUs(2,1)2 · y · y= 2hizliUs(2,0)1= 1→ çağrı · ← dönüş

y = 32 geldi. n = 10 çift → y · y = 32 · 32 = 1024 döner.

hizliUs(2,10)y · y= 1024hizliUs(2,5)2 · y · y= 32hizliUs(2,2)y · y= 4hizliUs(2,1)2 · y · y= 2hizliUs(2,0)1= 1→ çağrı · ← dönüş

Sonuç 1024. Yalnızca 5 çağrı; yavaş us(2, 10) 11 çağrı yapardı.

long hizliUs(long x, int n) {
  if (n == 0) return 1;           // taban durum
  long y = hizliUs(x, n / 2);     // x^(n/2)
  if (n % 2 == 0) return y * y;   // n çift
  return x * y * y;               // n tek
}
// main içinde:
System.out.println(hizliUs(2, 10));

İkili Özyineleme

Fibonacci Sayıları

0F(0)1F(1)1F(2)2F(3)3F(4)5F(5)8F(6)13F(7)21F(8)34F(9)
int fib(int n) {
  if (n <= 1) return n;    // taban durum
  return fib(n - 1) + fib(n - 2);
}
F(0) = 0, F(1) = 1
F(n) = F(n − 1) + F(n − 2)  (n ≥ 2)
  • İkili özyineleme: bir çağrı iki özyinelemeli çağrı yapar.
  • Çağrılar artık zincir değil, bir çağrı ağacı oluşturur: her düğüm bir çağrı, çocukları onun yaptığı çağrılar.
  • Tanım koda doğrudan dökülür; iki taban durum vardır: n = 0 ve n = 1.
  • Ağaçta aynı değer birden çok kez hesaplanır; maliyeti bu belirler.

İkili Özyineleme · Kodu adım adım çalıştır

Kod İzleme: fib(4) çağrı ağacı

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

main, fib(4)'ü çağırır.

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(4): n > 1 → önce fib(3) çağrılır; fib(2) sırada bekler.

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(3): n > 1 → önce fib(2) çağrılır; fib(1) sırada bekler.

f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(2): n > 1 → önce fib(1) çağrılır; fib(0) sırada bekler.

f(4)f(3)f(2)f(1)=1f(0)f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(1): taban durum → 1 döner.

f(4)f(3)f(2)f(1)=1f(0)=0f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

İkinci terim: fib(0): taban durum → 0 döner.

f(4)f(3)f(2)=1f(1)=1f(0)=0f(1)f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(2) = fib(1) + fib(0) = 1 + 0 = 1 döner.

f(4)f(3)f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

İkinci terim: fib(1): taban durum → 1 döner.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(3) = fib(2) + fib(1) = 1 + 1 = 2 döner.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(4) ikinci terim için fib(2)'yi çağırır: fib(2) ikinci kez hesaplanacak. Önce fib(1).

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)=1f(0)şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(1): taban durum → 1 döner.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)f(1)=1f(0)=0şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

İkinci terim: fib(0): taban durum → 0 döner.

f(4)f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(2) = fib(1) + fib(0) = 1 + 0 = 1 döner.

f(4)=3f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(4) = fib(3) + fib(2) = 2 + 1 = 3 döner.

f(4)=3f(3)=2f(2)=1f(1)=1f(0)=0f(1)=1f(2)=1f(1)=1f(0)=0şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

fib(4) = 3. 9 çağrı: fib(2) iki kez, fib(1) üç kez, fib(0) iki kez hesaplandı.

int fib(int n) {
  if (n <= 1) return n;    // taban durum
  return fib(n - 1) + fib(n - 2);
}
// main içinde:
System.out.println(fib(4));

İkili Özyineleme

Özyinelemenin Maliyeti

int fib(int n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
  • faktoriyel: T(n) = T(n − 1) + c → n çağrı → O(n) zaman, O(n) yığın belleği.
  • fib: her çağrı iki çağrı yapar; aynı alt problemler tekrar tekrar çözülür.
  • fib(4) → 9 çağrı; turuncu f(2) iki kez hesaplanıyor. Çağrı sayısı üstel büyür (≈ 1,6ⁿ).
  • Çözüm: sonuçları sakla veya döngüyle hesapla → O(n).
f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)

İkili Özyineleme

Tekrarlanan Hesaptan Kaçınmak

nfib(n)fib çağrı sayısı
5515
1055177
206 76521 891
30832 0402 692 537
40102 334 155331 160 281

Çağrı sayısı = 2 · F(n + 1) − 1: n her 1 arttığında yaklaşık 1,6 katına çıkar.

long[] bellek = new long[91];   // 0: hesaplanmadı
long fibBellek(int n) {
  if (n <= 1) return n;
  if (bellek[n] != 0) return bellek[n];
  bellek[n] = fibBellek(n - 1) + fibBellek(n - 2);
  return bellek[n];
}
long fibDongu(int n) {
  long a = 0, b = 1;              // F(0), F(1)
  for (int i = 0; i < n; i++) {
    long t = a + b;  a = b;  b = t;
  }
  return a;
}
Hesaplanan değerler bir dizide saklanırsa her değer bir kez hesaplanır: fibBellek(40) 79 çağrı yapar. Döngülü sürüm O(n) zaman ve O(1) bellekle çalışır.

İkili Özyineleme

Özyinelemeli İkili Arama

  • Sıralı dizide x aranır: ortadaki öğeye bakılır; x küçükse sol yarıda, büyükse sağ yarıda aynı arama yapılır.
  • İki taban durum: aralık boş (lo > hi) → −1; ortadaki öğe x → indisi.
  • Kodda iki özyinelemeli çağrı yazılıdır ama her çağrı bunlardan yalnızca birini yapar. Fibonacci gibi ağaç oluşmaz; çağrılar bir zincirdir.
  • Aralık her çağrıda yarıya iner: T(n) = T(n/2) + c → O(log n) zaman ve O(log n) yığın belleği.
  • n = 10 için en çok 5, n = 10⁶ için en çok 21 çağrı.
fib: iki çağrı → ağaçikiliAra: biri seçilir → zincir

İkili Özyineleme · Kodu adım adım çalıştır

Kod İzleme: ikiliAra(a, 23, 0, 9)

205182123164235386567728919x = 23

main, 10 öğeli sıralı dizide 23'ü arar: ikiliAra(a, 23, 0, 9).

205182123164235386567728919lomidhix = 23aralık: 10 öğe

lo = 0 ≤ hi = 9: aralık boş değil. mid = (0 + 9) / 2 = 4, a[4] = 16.

205182123164235386567728919lohix = 23aralık: 5 öğe

23 > 16 → sağ yarı: ikiliAra(a, 23, 5, 9). Sol yarıya hiç bakılmaz.

205182123164235386567728919lomidhix = 23aralık: 5 öğe

lo = 5 ≤ hi = 9: aralık boş değil. mid = (5 + 9) / 2 = 7, a[7] = 56.

205182123164235386567728919lohix = 23aralık: 2 öğe

23 < 56 → sol yarı: ikiliAra(a, 23, 5, 6). Sağ yarıya hiç bakılmaz.

205182123164235386567728919lo, midhix = 23aralık: 2 öğe

lo = 5 ≤ hi = 6: aralık boş değil. mid = (5 + 6) / 2 = 5, a[5] = 23.

205182123164235386567728919lo, midhix = 23aralık: 2 öğe

a[5] = 23 = x: taban durum (bulundu) → 5 döner.

205182123164235386567728919lo, midhix = 23aralık: 2 öğe

ikiliAra(a, 23, 5, 9) aldığı 5'i return ile olduğu gibi iletir.

205182123164235386567728919lo, midhix = 23aralık: 2 öğe

ikiliAra(a, 23, 0, 9) de 5'i iletir. Dönüşte yeni hesap yapılmaz.

205182123164235386567728919lo, midhix = 23aralık: 2 öğe

Sonuç 5. 3 çağrı; aralık 10 → 5 → 2 öğeye indi.

int ikiliAra(int[] a, int x, int lo, int hi) {
  if (lo > hi) return -1;       // taban: boş aralık
  int mid = (lo + hi) / 2;
  if (a[mid] == x) return mid;  // taban: bulundu
  if (x < a[mid])
    return ikiliAra(a, x, lo, mid - 1); // sol yarı
  return ikiliAra(a, x, mid + 1, hi);   // sağ yarı
}
System.out.println(ikiliAra(a, 23, 0, 9));

Hanoi ve Geri İzleme

Hanoi Kuleleri (Towers of Hanoi)

Bilgisayar bilimi ve matematikte klasik bir problem: bir disk yığını, kurallara uyularak kaynak çubuktan hedef çubuğa taşınır.

  • Bir hamlede yalnızca bir disk taşınır (bir çubuğun en üstündeki).
  • Bir disk yalnızca daha büyük bir diskin üstüne ya da boş çubuğa konabilir.
  • Amaç: yardımcı çubuğu kullanarak tüm diskleri kaynaktan (A) hedefe (C) taşımak.
ABC321
Özyinelemeli fikir: n−1 diski A'dan B'ye taşı → en büyük diski A'dan C'ye taşı → n−1 diski B'den C'ye taşı. Hamle sayısı T(n) = 2T(n−1) + 1 = 2n − 1.

Hanoi ve Geri İzleme

Hanoi Kuleleri: 3 Diskle 7 Hamle

ABC321

Başlangıç: 3 disk A çubuğunda (büyük altta). Hedef: hepsini C'ye taşımak.

ABC321

Hamle 1: disk 1, A → C.

ABC321

Hamle 2: disk 2, A → B.

ABC321

Hamle 3: disk 1, C → B.

ABC213

Hamle 4: disk 3, A → C.

ABC123

Hamle 5: disk 1, B → A.

ABC132

Hamle 6: disk 2, B → C.

ABC321

Hamle 7: disk 1, A → C. Bitti: 2³ − 1 = 7 hamle.

Hanoi ve Geri İzleme · Kodu adım adım çalıştır

Kod İzleme: Özyinelemeli hanoi(3, A, B, C)

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

main, hanoi(3, 'A', 'B', 'C') çağırır. Ağaçtaki her kutu bir çağrı: n:kaynak→hedef.

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

main: yeni çerçeve açıldı. n = 3 ≠ 1 → önce üstteki 2 disk A çubuğundan B çubuğuna taşınmalı.

A321BC3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 6: hanoi(2,A,C,B): yeni çerçeve açıldı. n = 2 ≠ 1 → önce üstteki 1 disk A çubuğundan C çubuğuna taşınmalı.

A32BC13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 6: hanoi(1,A,B,C): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, A → C.

A32BC13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: çerçeve yığından atılır, kontrol çağırana döner.

A3B2C13:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Sol alt çağrı bitti; A çubuğunda yalnızca disk 2 kaldı → Disk 2: A → B.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 8: hanoi(1,C,A,B): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, C → B.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: çerçeve yığından atılır, kontrol çağırana döner.

A3B21C3:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(2,A,C,B) biter; çerçevesi atılır.

AB21C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Sol alt çağrı bitti; A çubuğunda yalnızca disk 3 kaldı → Disk 3: A → C.

AB21C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 8: hanoi(2,B,A,C): yeni çerçeve açıldı. n = 2 ≠ 1 → önce üstteki 1 disk B çubuğundan A çubuğuna taşınmalı.

A1B2C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 6: hanoi(1,B,C,A): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, B → A.

A1B2C33:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: çerçeve yığından atılır, kontrol çağırana döner.

A1BC323:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Sol alt çağrı bitti; B çubuğunda yalnızca disk 2 kaldı → Disk 2: B → C.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

Satır 8: hanoi(1,A,B,C): yeni çerçeve açıldı. n == 1 → taban durum: disk 1, A → C.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

return: çerçeve yığından atılır, kontrol çağırana döner.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(2,B,A,C) biter; çerçevesi atılır.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

hanoi(3,A,B,C) biter; çerçevesi atılır. Ağaçtaki 7 çağrı = 7 hamle = 2³ − 1.

ABC3213:A→C2:A→B2:B→C1:A→C1:C→B1:B→A1:A→C

main'e dönüldü: tüm diskler C'de. Çağrı yığınının en fazla derinliği n = 3 (+ main) → bellek O(n), zaman O(2ⁿ).

void hanoi(int n, char k, char y, char h) {
  if (n == 1) {
    System.out.println("1: " + k + "→" + h);
    return;
  }
  hanoi(n - 1, k, h, y);
  System.out.println(n + ": " + k + "→" + h);
  hanoi(n - 1, y, k, h);
}
// main içinde:
hanoi(3, 'A', 'B', 'C');

Hanoi ve Geri İzleme

Geri İzleme (Backtracking)

  • Çözüm adım adım kurulur; her adımda olası seçeneklerden biri denenir.
  • Seçimle bir sonraki adıma geçilir: özyinelemeli çağrı.
  • Çağrı döndüğünde seçim geri alınır ve sıradaki seçenek denenir. Böylece her dal aynı başlangıç durumunu görür.
  • Bir seçim çözüme götüremeyecekse o dal bırakılır, daha derine inilmez.
  • Örnekler: alt kümeleri ya da permütasyonları listeleme, labirentte yol bulma, sudoku, n vezir.
seçilerle (çağrı)geri alsıradakiçözüm tamam ya daseçenek kalmadı → dön
Hanoi'de olduğu gibi her çağrı birden çok özyinelemeli çağrı yapar; çağrı ağacı, denenen tüm seçimlerin ağacıdır (karar ağacı).

Hanoi ve Geri İzleme

Alt Kümeleri Listeleme

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]
  • Her öğe için iki karar: al ya da alma. Sol dal öğeyi alır (+1, +2, +3), sağ dal almaz.
  • i. seviyede a[i] için karar verilir; yaprakta (i = n) tüm kararlar bitmiştir → s bir alt kümedir.
  • n öğe → 2ⁿ yaprak = 2ⁿ alt küme; çağrı sayısı 2ⁿ⁺¹ − 1 (n = 3: 8 alt küme, 15 çağrı).
  • Çıktı sırası: [1, 2, 3] [1, 2] [1, 3] [1] [2, 3] [2] [3] []

Hanoi ve Geri İzleme · Kodu adım adım çalıştır

Kod İzleme: altKumeler ile {1, 2, 3}

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

main, a = {1, 2, 3} ve boş s ile altKumeler(a, 0, s)'yi çağırır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 0: a[0] = 1 alınır (s = [1]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 1: a[1] = 2 alınır (s = [1, 2]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 2: a[2] = 3 alınır (s = [1, 2, 3]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 2, 3] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 3 geri alınır (s = [1, 2]); aynı i ile sağ dala, 3'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 2] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 2 geri alınır (s = [1]); aynı i ile sağ dala, 2'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 2: a[2] = 3 alınır (s = [1, 3]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [1, 3] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 3 geri alınır (s = [1]); aynı i ile sağ dala, 3'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [1] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 1 geri alınır (s = []); aynı i ile sağ dala, 1'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 1: a[1] = 2 alınır (s = [2]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 2: a[2] = 3 alınır (s = [2, 3]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [2, 3] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 3 geri alınır (s = [2]); aynı i ile sağ dala, 3'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [2] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 2 geri alınır (s = []); aynı i ile sağ dala, 2'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 2: a[2] = 3 alınır (s = [3]), sol dala inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [3] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Sol daldan dönüldü. 3 geri alınır (s = []); aynı i ile sağ dala, 3'siz alt kümelere inilir.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

i = 3 = a.length: taban durum. Tüm kararlar verildi → [] yazılır.

+1+2+3+3+2+3+3[][1][1,2][1,2,3][1,2][1][1,3][1][][2][2,3][2][][3][]şimdi çalışanyığında, bekliyordeğerini döndürdühenüz çağrılmadı

Bitti: 8 alt küme, 15 çağrı. Yığında aynı anda en çok main + 4 çerçeve vardı.

void altKumeler(int[] a, int i, List<Integer> s) {
  if (i == a.length) {              // taban durum
    System.out.print(s + " "); return;
  }
  s.add(a[i]);                      // a[i]'yi al
  altKumeler(a, i + 1, s);
  s.remove(s.size() - 1);           // geri al
  altKumeler(a, i + 1, s);          // a[i]'siz dene
}
// main içinde:
altKumeler(new int[]{1, 2, 3}, 0, new ArrayList<>());

Hanoi ve Geri İzleme

Permütasyonları Listeleme

void perm(char[] h, boolean[] kul, StringBuilder s) {
  if (s.length() == h.length) {  // taban durum
    System.out.print(s + " "); return;
  }
  for (int k = 0; k < h.length; k++) {
    if (kul[k]) continue;        // harf kullanıldı
    kul[k] = true; s.append(h[k]);   // seç
    perm(h, kul, s);
    kul[k] = false;              // geri al
    s.deleteCharAt(s.length() - 1);
  }
}
boşAABABCACACBBBABACBCBCACCACABCBCBA
  • Çıktı: ABC ACB BAC BCA CAB CBA
  • Her seviyede kalan harflerden biri seçilir: 3 · 2 · 1 = 3! = 6 yaprak.
  • Çağrı sayısı 1 + 3 + 6 + 6 = 16.
Bir çağrı, kalan harf sayısı kadar özyinelemeli çağrı yapar: çoklu özyineleme. kul[] ve s tüm çağrılarca paylaşılır; bu yüzden her denemeden sonra seçim geri alınır.

Yineleme Bağıntıları

Yineleme Bağıntısı (Recurrence)

  • T(n): n boyutlu girdide metodun yaptığı toplam iş. Özyinelemeli metodun maliyeti, aynı metodun daha küçük girdilerdeki maliyetiyle yazılır: yineleme bağıntısı.
  • Yazmak için üç soru: taban durumun maliyeti nedir (genellikle sabit c)? Kaç özyinelemeli çağrı yapılıyor ve girdileri ne kadar küçük? Çağrılar dışındaki iş ne kadar?
MetotÇağrılarYineleme bağıntısıÇözüm
faktoriyel, toplam, geriSaybir çağrı, n − 1T(n) = T(n − 1) + cO(n)
tersbir çağrı, n − 2T(n) = T(n − 2) + cO(n)
ikiliAra, hizliUsbir çağrı, n / 2T(n) = T(n/2) + cO(log n)
hanoiiki çağrı, n − 1T(n) = 2T(n − 1) + cO(2ⁿ)
fibiki çağrı, n − 1 ve n − 2T(n) = T(n − 1) + T(n − 2) + cüstel
Bağıntı, çağrı ağacı çizilerek çözülür: her düğüm sabit iş yapıyorsa toplam iş ≈ c · (çağrı sayısı).

Yineleme Bağıntıları

T(n) = T(n − 1) + c

T(n) = T(n − 1) + c
     = T(n − 2) + 2c
     = T(n − 3) + 3c
     …
     = T(0) + n · c
     = c + n · c   →  O(n)
ncn − 1cn − 2c…1c0cn + 1 çağrı × c iş
  • Bağıntı açılarak çözülür: her açılışta bir c daha eklenir, n azalır. n adımda taban duruma inilir.
  • Çağrı ağacı dallanmayan bir zincirdir: n + 1 çağrı. Zincirin tamamı aynı anda yığındadır → bellek de O(n).
  • Örnekler: faktoriyel, toplam, geriSay, yavaş üs alma, bağlı listenin uzunluğu. ters için T(n) = T(n − 2) + c: yarı sayıda çağrı, yine O(n).

Yineleme Bağıntıları

T(n) = T(n/2) + c

T(n) = T(n/2) + c
     = T(n/4) + 2c
     = T(n/8) + 3c
     …
     = T(n/2ᵏ) + k · c

n/2ᵏ = 1 olduğunda durulur: k = log₂ n.
T(n) = T(1) + c · log₂ n → O(log n)

16c8c4c2c1clog₂ 16 + 1 = 5 çağrı
n161 0241 000 000
yarılama zinciri (çağrı)51120

Örnekler: ikili arama, hızlı üs alma. Girdi bin kat büyüyünce çağrı sayısı yalnızca yaklaşık 10 artar; zincir derinliği de log n olduğundan bellek O(log n).

Yineleme Bağıntıları

T(n) = 2T(n − 1) + c

  • Her çağrı n − 1 boyutlu iki çağrı yapar: çağrı ağacının her seviyesinde çağrı sayısı ikiye katlanır.
  • Seviye k'de 2ᵏ çağrı; seviyeler 0 … n − 1:
    1 + 2 + 4 + … + 2ⁿ⁻¹ = 2ⁿ − 1 çağrı → O(2ⁿ).
  • Hanoi: tam olarak 2ⁿ − 1 hamle. n = 20 için 1 048 575; n = 64 için yaklaşık 1,8 · 10¹⁹.
  • fib: T(n − 2) ≤ T(n − 1) olduğundan T(n) ≤ 2T(n − 1) + c → üst sınır O(2ⁿ); gerçek büyüme yaklaşık 1,6ⁿ.
n = 4: 1 çağrın = 3: 2 çağrın = 2: 4 çağrın = 1: 8 çağrıtoplam 1 + 2 + 4 + 8 = 15 = 2⁴ − 1
Ağaç üstel büyüse de derinliği n'dir: aynı anda yığında en çok n çerçeve bulunur.

Yineleme Bağıntıları

Alan Maliyeti: En Derin Çağrı Zinciri

  • Bir çağrı, kardeşi başlamadan biter. Bu yüzden yığında aynı anda yalnızca kökten bir düğüme giden tek bir yol bulunur.
  • Bellek = en derin çağrı zinciri × çerçeve boyutu. Toplam çağrı sayısı değil, derinlik belirler.
  • fib(4): 9 çağrı, ama aynı anda en çok 4 çerçeve (+ main).
f(4)f(3)f(2)f(1)f(0)f(1)f(2)f(1)f(0)
ÇağrıToplam çağrıEn derin zincirZamanAlan
faktoriyel(4)44O(n)O(n)
fib(40)331 160 28140üstelO(n)
hanoi(3, …)73O(2ⁿ)O(n)
altKumeler, n = 3154O(n · 2ⁿ)O(n)

Kuyruk Özyinelemesi

Kuyruk Özyinelemesi (Tail Recursion)

  • Özyinelemeli çağrı metodun son işiyse, yani çağrı döndükten sonra yapılacak bir iş kalmıyorsa, metot kuyruk özyinelemelidir.
  • Ara sonuç bir biriktirici parametreyle (acc) aşağı taşınır; çarpma çağrıdan önce yapılır. İlk çağrı: faktKuyruk(n, 1).

Kuyruk özyinelemesi değil

int faktoriyel(int n) {
  if (n <= 1) return 1;
  return n * faktoriyel(n - 1);
  // çarpma, çağrı dönünce yapılır
}

Kuyruk özyinelemeli

int faktKuyruk(int n, int acc) {
  if (n <= 1) return acc;
  return faktKuyruk(n - 1, n * acc);
  // son iş: çağrı (çarpma önce)
}
Kuyruk özyinelemeli: geriSay, ters, ikiliAra, faktKuyruk. Değil: faktoriyel, toplam, fib, ileriSay (çağrıdan sonra iş var).

Kuyruk Özyinelemesi · Kodu adım adım çalıştır

Kod İzleme: faktKuyruk(4, 1)

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)faktKuyruk(1, 24)return acc→ çağrı · ← dönüş

main, faktKuyruk(4, 1)'i çağırır; acc başlangıçta 1.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)faktKuyruk(1, 24)return acc→ çağrı · ← dönüş

n = 4 > 1 → çarpma önce: 4 · 1 = 4. Sonra faktKuyruk(3, 4) çağrılır.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)faktKuyruk(1, 24)return acc→ çağrı · ← dönüş

n = 3 > 1 → çarpma önce: 3 · 4 = 12. Sonra faktKuyruk(2, 12) çağrılır.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)faktKuyruk(1, 24)return acc→ çağrı · ← dönüş

n = 2 > 1 → çarpma önce: 2 · 12 = 24. Sonra faktKuyruk(1, 24) çağrılır.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)faktKuyruk(1, 24)return acc= 24→ çağrı · ← dönüş

n = 1: taban durum → acc = 24 döner. Sonuç burada hazır.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)faktKuyruk(2, 12)çağrı: (1, 24)= 24faktKuyruk(1, 24)return acc= 24→ çağrı · ← dönüş

faktKuyruk(2, 12), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.

faktKuyruk(4, 1)çağrı: (3, 4)faktKuyruk(3, 4)çağrı: (2, 12)= 24faktKuyruk(2, 12)çağrı: (1, 24)= 24faktKuyruk(1, 24)return acc= 24→ çağrı · ← dönüş

faktKuyruk(3, 4), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.

faktKuyruk(4, 1)çağrı: (3, 4)= 24faktKuyruk(3, 4)çağrı: (2, 12)= 24faktKuyruk(2, 12)çağrı: (1, 24)= 24faktKuyruk(1, 24)return acc= 24→ çağrı · ← dönüş

faktKuyruk(4, 1), 24'ü hiç değiştirmeden iletir: dönüşte yapılacak iş yok.

faktKuyruk(4, 1)çağrı: (3, 4)= 24faktKuyruk(3, 4)çağrı: (2, 12)= 24faktKuyruk(2, 12)çağrı: (1, 24)= 24faktKuyruk(1, 24)return acc= 24→ çağrı · ← dönüş

Sonuç 24. Java'da yine 4 çerçeve açıldı; dönüşte bu çerçevelere ihtiyaç yoktu.

int faktKuyruk(int n, int acc) {
  if (n <= 1) return acc;   // taban durum
  return faktKuyruk(n - 1, n * acc);  // son iş
}
// main içinde:
System.out.println(faktKuyruk(4, 1));

Kuyruk Özyinelemesi

Kuyruk Özyinelemesini Döngüye Çevirme

int faktDongu(int n) {
  int acc = 1;
  while (n > 1) {   // taban durum değilken
    acc = n * acc;  // çağrının yeni
    n = n - 1;      //   parametreleri
  }
  return acc;       // taban: return acc
}
  • Kuyruk çağrısı, parametreleri yeni değerlerle güncelleyip metodun başına dönmek demektir: parametreler değişken, taban durum döngü koşulu olur.
  • Çevirme mekaniktir; döngülü ikili arama da ikiliAra'nın böyle çevrilmiş hâlidir.
  • Döngü sabit bellekle çalışır: O(1), çerçeve açılmaz.
Java kuyruk çağrısını eniyilemez: derleyici ve JVM faktKuyruk için de her çağrıda yeni çerçeve açar; çok derin çağrıda StackOverflowError yine oluşur. Scheme gibi bazı diller bu eniyilemeyi garanti eder; Scala ve Kotlin derleyicileri kendini çağıran kuyruk özyinelemesini döngüye çevirebilir.

Kuyruk Özyinelemesi

Özyineleme mi, Döngü mü?

ÖlçütÖzyinelemeDöngü
Okunabilirliktanıma yakın, kısadurum değişkenleri elle yönetilir
BellekO(derinlik) çağrı yığınıçoğunlukla O(1)
Hızher çağrının ek maliyeti vargenellikle daha hızlı
Derinlik sınırıStackOverflowError riskiyok
Özyineleme doğal seçimAğaç gibi iç içe yapılar, geri izleme (alt küme, permütasyon), problemi yarıya bölen yöntemler, Hanoi. Derinlik küçük (log n ya da n küçük).
Döngü daha iyiBasit sayma ve toplama; derinliği n'ye ulaşan doğrusal özyineleme (uzun liste, büyük n); tekrarlanan alt problemli ikili özyineleme (fib).
Her özyinelemeli metot, çağrı yığınının işini açık bir yığınla yaparak döngüye çevrilebilir.

Bağlı Listede Özyineleme

Bağlı Listede Özyineleme

  • Bir bağlı liste ya boştur (null) ya da bir düğüm ve onu izleyen daha kısa bir listedir.
  • Tanım özyinelemelidir: düğüm işlenir, kalan liste (d.sonraki) aynı metoda verilir.
  • Taban durum: d == null.
bas739nullkalan liste: d.sonraki
class Dugum { int veri; Dugum sonraki; }

int uzunluk(Dugum d) {
  if (d == null) return 0;        // boş liste
  return 1 + uzunluk(d.sonraki);  // 1 + kalan
}
void yazdir(Dugum d) {
  if (d == null) return;
  System.out.print(d.veri + " ");
  yazdir(d.sonraki);
}

Bağlı Listede Özyineleme · Kodu adım adım çalıştır

Kod İzleme: uzunluk(bas)

bas739null

main, uzunluk(bas)'ı çağırır; d ilk düğümü (7) gösterir.

bas739null

d ≠ null → 1 + uzunluk(d.sonraki): kalan liste (3 ile başlayan) için çağrı; bu çerçeve bekler.

bas739null

d ≠ null → 1 + uzunluk(d.sonraki): kalan liste (9 ile başlayan) için çağrı; bu çerçeve bekler.

bas739null

d ≠ null → 1 + uzunluk(d.sonraki): d.sonraki = null, boş liste için çağrı.

bas739null↑ 0

d = null: taban durum, boş listenin uzunluğu 0 döner.

bas739↑ 1null↑ 0

Kalan listeden 0 geldi → 1 + 0 = 1 döner.

bas73↑ 29↑ 1null↑ 0

Kalan listeden 1 geldi → 1 + 1 = 2 döner.

bas7↑ 33↑ 29↑ 1null↑ 0

Kalan listeden 2 geldi → 1 + 2 = 3 döner.

bas7↑ 33↑ 29↑ 1null↑ 0

Sonuç 3. 4 çağrı (3 düğüm + null): zaman O(n), yığın O(n).

int uzunluk(Dugum d) {
  if (d == null) return 0;        // boş liste
  return 1 + uzunluk(d.sonraki);  // 1 + kalan
}
// main içinde (bas → 7 → 3 → 9 → null):
System.out.println(uzunluk(bas));

Bağlı Listede Özyineleme

Listeyi Tersten Yazdırma

bas739null
void yazdir(Dugum d) {
  if (d == null) return;
  System.out.print(d.veri + " ");
  yazdir(d.sonraki);
}

Çıktı: 7 3 9 (yazdırma çağrıdan önce)

void tersYazdir(Dugum d) {
  if (d == null) return;
  tersYazdir(d.sonraki);  // önce kalan
  System.out.print(d.veri + " ");
}

Çıktı: 9 3 7 (yazdırma çağrıdan sonra)

  • Tek yönlü listede geriye bağlantı yoktur; geri dönüşü çağrı yığını sağlar: düğümler, yığın çözülürken sondan başa yazılır.
  • Bedeli O(n) yığın belleğidir. Çok uzun listelerde döngü ve açık bir yığın daha güvenlidir.

Özet

Sık Yapılan Hatalar

Taban durum eksik ya da ulaşılamaz0 ya da negatif girdi tabanı atlarsa çağrılar bitmez: StackOverflowError. n == 1 yerine n <= 1 daha güvenlidir.
Problem küçülmüyorf(n) içinden yine f(n) ya da f(n + 1) çağrılırsa taban duruma yaklaşılmaz.
Dönüş değerini kaybetmekikiliAra'da sol yarı çağrısının önündeki return unutulursa sonuç atılır, akış sağ yarı çağrısına düşer: yanlış cevap, derleyici uyarmaz.
Geri almayı unutmakGeri izlemede s.remove(...) yapılmazsa bir dalın seçimleri sonraki dallara taşar; alt kümeler yanlış çıkar.
Tekrarlanan hesapfib gibi ikili özyinelemede aynı alt problem üstel sayıda çözülür. Sonuçları saklamak ya da döngü O(n) verir.
Derin doğrusal özyinelemeMilyon düğümlü listede uzunluk yığını taşırır. Java kuyruk çağrısını eniyilemez → döngü.

Özet

Özet

ÖrnekÖzyineleme türüZamanAlan (yığın)
faktoriyel, toplam, tersdoğrusalO(n)O(n)
hizliUs, ikiliAradoğrusal, yarılayanO(log n)O(log n)
fibikiliüstel (≈ 1,6ⁿ)O(n)
hanoiikiliO(2ⁿ)O(n)
altKumelergeri izlemeO(n · 2ⁿ)O(n)
permçoklu, geri izlemeO(n · n!)O(n)
faktKuyrukkuyruk özyinelemesiO(n)O(n) (Java'da)
uzunluk, tersYazdirdoğrusal (bağlı liste)O(n)O(n)
Özyineleme = taban durum + taban duruma yaklaşan özyinelemeli adım. Zaman çağrı sayısından, alan en derin çağrı zincirinden gelir.

Özet

Etkileşimli Simülatörler

Hanoi Kuleleri1–6 diskle bulmacayı elle çözme; en kısa çözümü adım adım ileri ve geri oynatma ya da otomatik çözdürme. Yapılan hamle sayısı en az hamle sayısıyla (2ⁿ − 1) karşılaştırılır: n = 6 için 63.
Her kule bir yığın gibi davranır: yalnızca en üstteki disk alınabilir ve yalnızca daha büyük bir diskin üstüne konabilir.

Özet

Örnek Problemler

  1. int f(int n) { if (n <= 0) return 0; return n % 10 + f(n / 10); } için f(2026) kaçtır, kaç çağrı yapılır?
  2. ileriSay(4) ne yazar? Yığında (main dahil) en çok kaç çerçeve bulunur?
  3. hizliUs(3, 13) kaç çağrı yapar, sonuç nedir?
  4. fib(6) kaç çağrı yapar?
  5. {1, 2, 3, 4} için altKumeler kaç alt küme yazar, kaç çağrı yapar?
  6. T(n) = T(n/2) + c ve T(1) = c ise n = 64 için zincirde kaç çağrı vardır?
Cevapları göster
  1. Basamaklar toplamı: 6 + 2 + 0 + 2 = 10; çağrılar 2026, 202, 20, 2, 0 → 5.
  2. 1 2 3 4; ileriSay(4 … 0) = 5 çerçeve + main = 6.
  3. n: 13, 6, 3, 1, 0 → 5 çağrı; 3¹³ = 1 594 323.
  4. 2 · F(7) − 1 = 2 · 13 − 1 = 25 çağrı (fib(6) = 8).
  5. 2⁴ = 16 alt küme; 2⁵ − 1 = 31 çağrı.
  6. 64, 32, 16, 8, 4, 2, 1 → 7 = log₂ 64 + 1.

Özet

Alıştırmalar

  1. faktoriyel(5) çalışırken çağrı yığınında (main dahil) en fazla kaç çerçeve olur?
  2. 4 diskli Hanoi kaç hamle ister?
  3. Dizinin en büyük öğesini döndüren özyinelemeli enBuyuk(a, n) için taban durum ve özyinelemeli adım nedir?
  4. palindrom(s, i, j) uçları karşılaştırıp içe ilerler: "kayak" için kaç çağrı yapılır?
  5. int g(int n) { if (n <= 1) return 1; return g(n - 1) + g(n - 1); }: g(5) değeri ve çağrı sayısı? Nasıl O(n) yapılır?
  6. faktKuyruk'un döngülü sürümünde her turda n ve acc nasıl değişir?
Cevaplar / ipuçları
  1. faktoriyel(5), (4), (3), (2), (1) + main = 6.
  2. 2⁴ − 1 = 15 hamle.
  3. Taban: n = 1 → a[0]. Adım: max(enBuyuk(a, n − 1), a[n − 1]). O(n).
  4. (0, 4) k = k → (1, 3) a = a → (2, 2) taban: 3 çağrı, true.
  5. g(5) = 2⁴ = 16; 2⁵ − 1 = 31 çağrı. Tek çağrı yeter: return 2 * g(n - 1); → O(n).
  6. acc = n * acc; n = n - 1; döngü n > 1 iken sürer; sonunda acc döner.

Özet

Kaynaklar

  • Goodrich, Tamassia ve Goldwasser, Data Structures and Algorithms in Java, 6. baskı, 2014.
  • Sedgewick ve Wayne, Computer Science: An Interdisciplinary Approach, 2016.
  • Roberts, Thinking Recursively with Java, 2006.
  • Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022.

Son

Bölüm 4: Özyineleme
Sonraki sunum: Bölüm 5: Bağlı Listeler · Çalışma soruları · Simülatör: Tower of Hanoi Simulator · Terim sözlüğü
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.