Bölüm 3: Özyineleme, İkili Arama ve Geri İzleme

C ile Veri Yapıları ve Algoritmalar
Yığın çerçevesiKuyruk özyinelemesiİkili aramaHanoi kuleleriPermütasyonSatisfiabilityn-vezirAt gezisi
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Özyinelemeli düşünmek

Bir problemi taban durum + küçülen alt problem olarak ifade etmek; özyineleme ile döngüyü karşılaştırmak.

Yığını izlemek

Her çağrının yığın çerçevesini (parametreler, yereller, dönüş adresi) çizmek.

İkili arama

Döngüsel ve özyinelemeli ikili aramayı yazmak; O(log n) sınırını göstermek.

Yineleme bağıntısı

Hanoi için h(n) = 2h(n−1) + 1 bağıntısını kurup çözmek; permütasyon için O(n·n!).

Geri izleme

Kaba kuvvet ile geri izlemeyi ayırmak; SAT, n-vezir, alt küme üretimi.

Sezgisel yaklaşım

At gezisinde Warnsdorff kuralının geri izlemeye göre neden hızlı olduğunu açıklamak.

Özyineleme

Özyinelemeli Algoritmalar (Recursive Algorithms)

Bütündeki çözüm mantığı alt problemlerde de geçerliyse özyinelemeli algoritma kullanılabilir.

Doğrudan özyineleme

Fonksiyon kendini çağırır: fakt → fakt.

Dolaylı özyineleme

Çağırdığı bir fonksiyon onu geri çağırır: A → B → A.

Taban durum

Özyinelemesiz çözülen en küçük örnek. Yoksa çağrılar sonsuza gider (yığın taşar).

Özyinelemeli adım

Problemi daha küçük bir örneğe indirger; her çağrı taban duruma yaklaşmalı.

Hocadan“‘Bu özyinelemeli çözüm çok bellek yemez mi?’ Bu soru için erken. Önce kodu yazmak öğrenilir; algoritma karmaşıklığı hesaplanabildiğinde ancak böyle denebilir. Özyinelemeli kod yazmak öğrenilmeden bu yargıya varılmaz.”

Özyineleme

Yığın Çerçevesi (Stack Activation Frame)

sistem yığını (aşağıdan yukarı büyür)main()• dönüş adresi• önceki çerçeve işaretçisi• yerel değişkenlerbinsearch(list, 18, 0, 9)• parametreler: list, searchnum, left, right• yerel: middle• dönüş adresi → mainyeni çağrı üste
  • Fonksiyon çağrılınca sistem yığını kullanılır.
  • Denetim fonksiyona geçmeden önce bir hazırlık yapılır: nereye dönüleceği, parametrelerin saklanması…
  • Bu iletişim–denetim yapısına yığın çerçevesi denir.
  • Başlangıçta yığında iki değer vardır: dönüş adresi ve önceki çerçeveye işaretçi.
  • Her özyinelemeli çağrı yeni bir çerçeve demektir → derinlik d ise ek bellek O(d).

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

Kod İzleme: fakt(3) ve çağrı yığını

çağrı yığınımain()

main, fakt(3)’ü çağırır: yeni çerçeve açılacak.

çağrı yığınımain()fakt(n=3)

fakt(3): n = 3 > 1, taban durum değil.

çağrı yığınımain()fakt(n=3)

3 * fakt(2): çarpma bekler; önce fakt(2) çağrılır.

çağrı yığınımain()fakt(n=3)fakt(n=2)

fakt(2): n = 2 > 1, taban durum değil.

çağrı yığınımain()fakt(n=3)fakt(n=2)

2 * fakt(1): çarpma bekler; önce fakt(1) çağrılır.

çağrı yığınımain()fakt(n=3)fakt(n=2)fakt(n=1)→ 1

fakt(1): taban durum → return 1. Yığın en derin hâlde (4 çerçeve).

çağrı yığınımain()fakt(n=3)fakt(n=2)= 2→ 2

fakt(2) kaldığı yerden devam: 2 · 1 = 2, çerçeve kapanır.

çağrı yığınımain()fakt(n=3)= 6→ 6

fakt(3) kaldığı yerden devam: 3 · 2 = 6, çerçeve kapanır.

çağrı yığınımain()

main 6 yazar. Aynı n değişkeni her çerçevede ayrı bir kopyadır.

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

int main(void) {
  printf("%ld\n", fakt(3));
  return 0;
}

Özyineleme

Özyineleme mi, Döngü mü?

/* ozyinelemeli */
long fakt(int n) {
  return n <= 1 ? 1 : n * fakt(n - 1);
}

/* dongulu */
long fakt_d(int n) {
  long f = 1;
  while (n > 1)
    f *= n--;
  return f;
}
ÖzyinelemeliDöngülü
OkunabilirlikTanıma çok yakınDurum değişkenleri gerekir
Ek bellekO(derinlik) yığınO(1)
Çağrı maliyetiHer adımda çerçeveYok
RiskDerin özyinelemede yığın taşmasıSonsuz döngü
Doğal olduğu yerAğaç, böl-yönet, geri izlemeDüz tarama, sayaçlar
Her özyinelemeli algoritma, çağrı yığınını açık bir yığınla taklit ederek döngüye çevrilebilir (ve tersi). Seçim; açıklık, bellek ve hız arasındaki bir dengedir.

Horowitz–Sahni 1.2.2

Özyineleme

Kuyruk Özyinelemesi (Tail Recursion)

Özyinelemeli çağrı fonksiyonun son işiyse (dönüşten sonra yapılacak iş kalmıyorsa) buna kuyruk özyinelemesi denir.

long fakt_k(int n, long acc) {
  if (n <= 1) return acc;
  return fakt_k(n - 1, n * acc);  /* son is: cagri */
}
/* ilk cagri: fakt_k(n, 1) */
fakt_k(4, 1)fakt_k(3, 4)fakt_k(2, 12)fakt_k(1, 24)→ 24 doğrudan döner
fakt kuyruk özyinelemeli değil

n * fakt(n-1): dönüşten sonra çarpma bekler, çerçeve saklanmalıdır.

Birikimli parametre

acc kısmi sonucu taşır; dönüşte yapılacak iş kalmaz.

Derleyici

Kuyruk çağrısı bir atlamaya çevrilebilir (O(1) yığın). C standardı bunu garanti etmez; gcc/clang optimizasyonla çoğu zaman yapar.

İkili Arama

İkili Arama (Binary Search)

Sıralı bir dizide değer arıyoruz: bulunursa indisini, bulunamazsa −1 döndüren yordam. Her adımda ortadaki öğeye bakılır ve aralığın yarısı atılır.

04113218319424527633736839940

Test dizisi

#define COMPARE(x, y) ((x) < (y) ? -1 : (y) < (x) ? 1 : 0)
Hocadan“list[i] değil, i döndürülür. Neden? Hata değeri olan −1 ile dizi öğesi olabilecek bir −1 karışmasın diye.”

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

Kod İzleme: döngüsel binsearch(list, 18, 0, 9)

04113218319424527633736839940▲ left▲ right

binsearch(list, 18, 0, 9): aralık tüm dizi.

04113218319424527633736839940▲ left▲ right▲ middle

0 ≤ 9: 1. tur. middle = (0 + 9) / 2 = 4, list[4] = 24.

04113218319424527633736839940▲ left▲ right

24 > 18 → COMPARE = 1: sağ yarı atılır, right = 3.

04113218319424527633736839940▲ left▲ right▲ middle

0 ≤ 3: 2. tur. middle = (0 + 3) / 2 = 1, list[1] = 13.

04113218319424527633736839940▲ left▲ right

13 < 18 → COMPARE = −1: sol yarı atılır, left = 2.

04113218319424527633736839940▲ leftmiddle▲ right

2 ≤ 3: 3. tur. middle = (2 + 3) / 2 = 2, list[2] = 18.

04113218319424527633736839940▲ leftmiddle▲ right

COMPARE = 0 → return 2. 3 turda bulundu.

int binsearch(int list[], int aranan,
              int left, int right) {
  while (left <= right) {
    int middle = (left + right) / 2;
    switch (COMPARE(list[middle], aranan)) {
      case -1: left = middle + 1; break;
      case 0: return middle;
      case 1: right = middle - 1;
    }
  }
  return -1;
}

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

Kod İzleme: binsearch(list, 25, 0, 9) → −1

04113218319424527633736839940▲ left▲ right

binsearch(list, 25, 0, 9): aralık tüm dizi.

04113218319424527633736839940▲ left▲ right▲ middle

0 ≤ 9: 1. tur. middle = (0 + 9) / 2 = 4, list[4] = 24.

04113218319424527633736839940▲ left▲ right

24 < 25 → COMPARE = −1: sol yarı atılır, left = 5.

04113218319424527633736839940▲ left▲ right▲ middle

5 ≤ 9: 2. tur. middle = (5 + 9) / 2 = 7, list[7] = 36.

04113218319424527633736839940▲ left▲ right

36 > 25 → COMPARE = 1: sağ yarı atılır, right = 6.

04113218319424527633736839940▲ leftmiddle▲ right

5 ≤ 6: 3. tur. middle = (5 + 6) / 2 = 5, list[5] = 27.

04113218319424527633736839940left = 5 > right = 4: aralık boş

27 > 25 → COMPARE = 1: sağ yarı atılır, right = 4.

04113218319424527633736839940left = 5 > right = 4: aralık boş

left > right: döngü biter → return −1 (3 tur).

int binsearch(int list[], int aranan,
              int left, int right) {
  while (left <= right) {
    int middle = (left + right) / 2;
    switch (COMPARE(list[middle], aranan)) {
      case -1: left = middle + 1; break;
      case 0: return middle;
      case 1: right = middle - 1;
    }
  }
  return -1;
}

İkili Arama

Özyinelemeli İkili Arama

int binsearch(int list[], int x, int l, int r) {
  int m;
  if (l > r) return -1;          /* aralik bos */
  m = (l + r) / 2;
  switch (COMPARE(list[m], x)) {
    case -1: return binsearch(list, x, m + 1, r);
    case 0:  return m;
    case 1:  return binsearch(list, x, l, m - 1);
  }
  return -1;                     /* ulasilmaz */
}
  • Döngü yerine aralığı küçülterek kendini çağırır.
  • Taban durumlar: bulundu (case 0) ya da aralık boş (l > r). Kısa olsun diye x = aranan, l/r = left/right, m = middle.
  • Her iki özyinelemeli çağrı da kuyruk çağrısıdır: sonucu olduğu gibi döndürür.
Sık yapılan hata: özyinelemeli sürümde if yerine while yazmak. Her dal return ile çıktığı için döngü bir kez döner ve sonuç doğrudur, ama yanıltıcıdır: burada if olmalı.

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

Kod İzleme: özyinelemeli binsearch(list, 18, 0, 9)

04113218319424527633736839940▲ ml=0 r=9 m=4

Yeni çerçeve: l = 0, r = 9 → m = 4 (list[4] = 24).

04113218319424527633736839940▲ ml=0 r=9 m=4

24 > 18 → case 1: binsearch(list, 18, 0, 3) çağrılır.

04113218319424527633736839940▲ ml=0 r=9 m=4l=0 r=3 m=1

Yeni çerçeve: l = 0, r = 3 → m = 1 (list[1] = 13).

04113218319424527633736839940▲ ml=0 r=9 m=4l=0 r=3 m=1

13 < 18 → case −1: binsearch(list, 18, 2, 3) çağrılır.

04113218319424527633736839940▲ ml=0 r=9 m=4l=0 r=3 m=1l=2 r=3 m=2

Yeni çerçeve: l = 2, r = 3 → m = 2 (list[2] = 18).

04113218319424527633736839940▲ ml=0 r=9 m=4l=0 r=3 m=1l=2 r=3 m=2→2

list[2] = 18: case 0 → return 2.

04113218319424527633736839940▲ ml=0 r=9 m=4l=0 r=3 m=1→2

Dönen 2 olduğu gibi yukarı iletilir (case −1 çerçevesi kapanır).

04113218319424527633736839940▲ ml=0 r=9 m=4→2

Dönen 2 olduğu gibi yukarı iletilir (case 1 çerçevesi kapanır).

04113218319424527633736839940▲ m

main’e 2 döner: “2’yi case 1’e döndürür, sonra da main’e döner.” Derinlik 3 → O(log n) yığın.

int binsearch(int list[], int x, int l, int r) {
  int m;
  if (l > r) return -1;          /* aralik bos */
  m = (l + r) / 2;
  switch (COMPARE(list[m], x)) {
    case -1: return binsearch(list, x, m + 1, r);
    case 0:  return m;
    case 1:  return binsearch(list, x, l, m - 1);
  }
  return -1;                     /* ulasilmaz */
}

İkili Arama

İkili Aramanın Karmaşıklığı

Her turda aralık en az yarıya iner: n → n/2 → n/4 → … → 1.

T(n) = T(n/2) + c ⇒ T(n) = O(log n)
  • En fazla ⌊log₂ n⌋ + 1 tur.
  • Döngüsel sürüm ek bellek: O(1).
  • Özyinelemeli sürüm: derinlik kadar çerçeve → O(log n).
  • Ön koşul: dizi sıralı olmalı ve indisle O(1) erişilebilmeli (bağlı listede işe yaramaz).
nen çok turdoğrusal arama
10410
1.000101.000
1.000.000201.000.000
1.000.000.000301.000.000.000
Örnek dizide (n = 10) en çok 4 tur: 18 için 3 tur yetti.

Hanoi Kuleleri

Hanoi Kuleleri Problemi

kaynak321arahedef

Kaynak kulede n disk var; her disk üzerindekinden büyüktür. Amaç: hepsini hedef kuleye taşımak, ara kuleden yardım alarak.

  1. Her seferinde tek disk taşınır.
  2. Bir disk kendinden küçük bir diskin üzerine konamaz.
Özyinelemeli çözüm: En büyük diski hedefe taşıyabilmek için üstündeki n−1 diskin ara kulede olduğu bir an olmalıdır: n−1 disk kaynak→ara, en büyük disk kaynak→hedef, n−1 disk ara→hedef. h(n) = h(n−1) + 1 + h(n−1).
Hocadan“Öyle problemler var ki, ancak özyinelemeli yolla çözülebilir.”

Kuramsal olarak her özyineleme açık bir yığınla döngüye çevrilebilir; vurgu, özyinelemenin bu tür problemlerde tek doğal ifade olmasıdır.

Hanoi Kuleleri · Kodu adım adım çalıştır

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

A321BC

n = 3: önce üstteki 2 disk A → B (hedef C yardımcı olur).

A321BC

n = 2: önce üstteki 1 disk A → C (hedef B yardımcı olur).

A32BC1

n = 1: taban durum. Disk 1: A → C.

A3B2C1

Geri dönüldü; en büyük disk 2: A → B. Sonra 1 disk C → B.

A3B21C

n = 1: taban durum. Disk 1: C → B.

AB21C3

Geri dönüldü; en büyük disk 3: A → C. Sonra 2 disk B → C.

AB21C3

n = 2: önce üstteki 1 disk B → A (hedef C yardımcı olur).

A1B2C3

n = 1: taban durum. Disk 1: B → A.

A1BC32

Geri dönüldü; en büyük disk 2: B → C. Sonra 1 disk A → C.

ABC321

n = 1: taban durum. Disk 1: A → C.

ABC321

Bitti: 7 = 2³ − 1 hamle, 7 çağrı. Yığın en çok n = 3 çerçeve derinleşti.

void hanoi(int n, char *kaynak, char *ara,
           char *hedef) {
  if (n == 1)
    printf("%s -> %s\n", kaynak, hedef);
  else {
    hanoi(n - 1, kaynak, hedef, ara);
    printf("%s -> %s\n", kaynak, hedef);
    hanoi(n - 1, ara, kaynak, hedef);
  }
}
/* main: hanoi(3, "A", "B", "C"); */

Hanoi Kuleleri ödev

Ödev: hanoi(3, "1", "2", "3") Yığın Çerçeveleri

Her kutu bir çağrının çerçevesidir: n, kaynak, ara, amaç. Sol çocuk ilk özyinelemeli çağrı, sağ çocuk ikincisi. Yığının herhangi bir andaki içeriği, kökten o düğüme giden yoldur.

3,1,2,32,1,3,21,1,2,31,3,1,22,2,1,31,2,3,11,1,2,3
Hamle sırası (kök ortada gezinme)

1→3 · 1→2 · 3→2 · 1→3 · 2→1 · 2→3 · 1→3 — her düğümün hamlesi, sol alt ağacı bittikten sonra yazılır.

Hanoi Kuleleri · Analiz

Yineleme Bağıntısını Çözmek: h(n) = 2·h(n−1) + 1

h(1) = 1
h(n) = 2·h(n−1) + 1
     = 4·h(n−2) + 2 + 1
     = 8·h(n−3) + 4 + 2 + 1
     = …
     = 2ⁿ⁻¹·h(1) + 2ⁿ⁻² + … + 1
     = 2ⁿ − 1
nhamle
11
23
37
415
101.023
201.048.575
6418.446.744.073.709.551.615
64 disk: 2⁶⁴ − 1 ≈ 1,8·10¹⁹ hamle. Saniyede bir hamleyle ≈ 585 milyar yıl. Algoritma O(2ⁿ) zaman, O(n) yığın kullanır ve bundan iyisi yoktur: 2ⁿ − 1 hamle gerekli ve yeterlidir.

Permütasyon

Permütasyon Problemi [O(n!)]

n ≥ 1 öğeli bir kümenin tüm dizilişlerini yazdırmak istiyoruz: n! diziliş vardır. {a, b, c} → abc, acb, bac, bca, cab, cba.

Özyinelemeli fikir

{a, b, c, d} kümesinin dizilişleri:
• a’yı izleyen {b, c, d} dizilişleri
• b’yi izleyen {a, c, d} dizilişleri
• c’yi izleyen {a, b, d} dizilişleri
• d’yi izleyen {a, b, c} dizilişleri

Takasla gerçekleme

i. konuma sırayla her öğe takasla getirilir, kalan n−i−1 konum özyinelemeli dizilir, sonra geri takasla dizi eski hâline döndürülür (geri izleme!).

Permütasyon · Kodu adım adım çalıştır

Kod İzleme: perm("abc", 0, 3)

0a1b2c▲ ij

i = 0, j = 0: kendisiyle takas → abc; perm(i = 1) çağrılır.

0a1b2c▲ ij

i = 1, j = 1: kendisiyle takas → abc; perm(i = 2) çağrılır.

0a1b2c

i = n: diziliş hazır → abc yazılır.

0a1c2b▲ i▲ j

i = 1, j = 2: list[1] ↔ list[2] → acb; perm(i = 2) çağrılır.

0a1c2b

i = n: diziliş hazır → acb yazılır.

0a1b2c▲ i▲ j

Geri takas: list[1] ↔ list[2] → abc (eski hâl).

0b1a2c▲ i▲ j

i = 0, j = 1: list[0] ↔ list[1] → bac; perm(i = 1) çağrılır.

0b1a2c▲ ij

i = 1, j = 1: kendisiyle takas → bac; perm(i = 2) çağrılır.

0b1a2c

i = n: diziliş hazır → bac yazılır.

0b1c2a▲ i▲ j

i = 1, j = 2: list[1] ↔ list[2] → bca; perm(i = 2) çağrılır.

0b1c2a

i = n: diziliş hazır → bca yazılır.

void perm(char *list, int i, int n) {
  int j, temp;
  if (i == n) {                  /* bir dizilis */
    for (j = 0; j < n; j++) printf("%c", list[j]);
    printf("\n");
  } else
    for (j = i; j < n; j++) {
      SWAP(list[i], list[j], temp);
      perm(list, i + 1, n);
      SWAP(list[i], list[j], temp);
    }
}

Permütasyon · Kodu adım adım çalıştır

Kod İzleme: perm("abc", 0, 3) (devam)

0b1a2c▲ i▲ j

Geri takas: list[1] ↔ list[2] → bac (eski hâl).

0a1b2c▲ i▲ j

Geri takas: list[0] ↔ list[1] → abc (eski hâl).

0c1b2a▲ i▲ j

i = 0, j = 2: list[0] ↔ list[2] → cba; perm(i = 1) çağrılır.

0c1b2a▲ ij

i = 1, j = 1: kendisiyle takas → cba; perm(i = 2) çağrılır.

0c1b2a

i = n: diziliş hazır → cba yazılır.

0c1a2b▲ i▲ j

i = 1, j = 2: list[1] ↔ list[2] → cab; perm(i = 2) çağrılır.

0c1a2b

i = n: diziliş hazır → cab yazılır.

0c1b2a▲ i▲ j

Geri takas: list[1] ↔ list[2] → cba (eski hâl).

0a1b2c▲ i▲ j

Geri takas: list[0] ↔ list[2] → abc (eski hâl).

void perm(char *list, int i, int n) {
  int j, temp;
  if (i == n) {                  /* bir dizilis */
    for (j = 0; j < n; j++) printf("%c", list[j]);
    printf("\n");
  } else
    for (j = i; j < n; j++) {
      SWAP(list[i], list[j], temp);
      perm(list, i + 1, n);
      SWAP(list[i], list[j], temp);
    }
}

Permütasyon · Analiz

Özyineleme Ağacı ve Karmaşıklık

·aababcacacbbbabacbcbcaccbcbacacab

Kök: sabitlenmiş önek yok · her seviye bir konumu sabitler · yapraklar: çıktıdaki sırayla 6 diziliş

Çağrı sayısı

1 + n + n(n−1) + … + n! çağrı; yaprak sayısı n!.

Zaman

Her yaprakta n karakter yazılır → O(n·n!). Sık görülen kısa yazım O(n!) bu n çarpanını gözden kaçırır.

Neden geri takas?

Kardeş dallar aynı başlangıç dizisini görmeli; ikinci SWAP diziyi onarır.

Geri İzleme

Kaba Kuvvet ve Geri İzleme (Backtracking)

Kaba kuvvet (brute force)

Olası tüm adaylar üretilir, her biri sınanır. Bilgisayarın gücüne güvenir; aday sayısı 2ⁿ, n! … olabilir.

Geri izleme

Çözüm adım adım kurulur; kısmi çözüm bir kuralı bozduğu anda o dal budanır, son seçim geri alınır, sıradaki denenir.

void geri_izle(kismi_cozum) {
  if (tam_cozum_mu) { yaz; return; }
  for (her aday secim)
    if (kurallara_uygun) { sec; geri_izle(...); secimi_geri_al; }
}
Hocadan“Daha iyi çözüm aramak: Veri yapılarıyla problem çözerken seçilen veri yapısından asla tam emin olunmamalı. Sürekli yazılmalı, tartışılmalı; “Daha iyi bir çözüm yöntemi vardır da biz bulamıyoruz.” denmeli.”

Geri İzleme

Satisfiability (SAT) Problemi

∨∨∧x1¬x2∧¬x1x3¬x3

(x1 ∧ ¬x2) ∨ (¬x1 ∧ x3) ∨ ¬x3

x1, x2, … true/false alan mantıksal değişkenler; ∧, ∨, ¬ işleçler. Soru: formülü true yapan bir atama var mı?

  • 3 değişken → 2³ = 8 olası atama: {F,F,F}, {F,F,T}, …
  • n değişken → 2ⁿ durum; kaba kuvvet O(2ⁿ). m işlemciye eşit bölünse bile O(2ⁿ/m): hâlâ üstel.
  • SAT, polinom zamanlı algoritması bilinmeyen NP-tam bir problemdir. 2-SAT polinom zamanda çözülür, 3-SAT NP-tamdır.
Hocadan“Keşke Computational Theory (hesaplama kuramı) dersi de alınmış olsa; problemler orada sınıflandırılıyor.”

Geri İzleme

SAT: Kök Sonda Gezinmeyle Değerlendirme

typedef enum { DEGIL, VE, VEYA, DOGRU, YANLIS } mantik;
typedef struct agac *agac_gost;
struct agac {
  agac_gost solcocuk, sagcocuk;
  mantik veri;
  short deger;
};

void kok_sonda_degerlendir(agac_gost L) {
  if (L) {
    kok_sonda_degerlendir(L->solcocuk);
    kok_sonda_degerlendir(L->sagcocuk);
    switch (L->veri) {
      case DEGIL: L->deger = !L->sagcocuk->deger; break;
      case VE:   L->deger = L->solcocuk->deger
                         && L->sagcocuk->deger; break;
      case VEYA: L->deger = L->solcocuk->deger
                         || L->sagcocuk->deger; break;
      case DOGRU:  L->deger = 1; break;
      case YANLIS: L->deger = 0; break;
    }
  }
}
  • Kök-sonda (postorder): önce iki çocuk, sonra düğümün kendisi.
  • ¬ düğümünün tek işleneni sağ çocuktadır.
  • Yapraklar: her atamada değişkenlerin değeri DOGRU/YANLIS olarak yerleştirilir.
enum { not, and, or, true, false } gibi adlar C99’un <stdbool.h> makrolarıyla ve C23 anahtar sözcükleriyle çakışır; typedef struct agac {…}; da ad vermeyen bir typedef’tir. Burada ASCII ve çakışmasız adlar kullandık.

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

Kod İzleme: 2³ atamanın hepsini denemek

∨=T∨=F∧=Fx1=F¬=Tx2=F∧=F¬=Tx1=Fx3=F¬=Tx3=F

Atama 000: ¬x3 = T → kök true: yazdır.

∨=T∨=T∧=Fx1=F¬=Tx2=F∧=T¬=Tx1=Fx3=T¬=Fx3=T

Atama 001: ¬x1 ∧ x3 = T → kök true: yazdır.

∨=T∨=F∧=Fx1=F¬=Fx2=T∧=F¬=Tx1=Fx3=F¬=Tx3=F

Atama 010: ¬x3 = T → kök true: yazdır.

∨=T∨=T∧=Fx1=F¬=Fx2=T∧=T¬=Tx1=Fx3=T¬=Fx3=T

Atama 011: ¬x1 ∧ x3 = T → kök true: yazdır.

∨=T∨=T∧=Tx1=T¬=Tx2=F∧=F¬=Fx1=Tx3=F¬=Tx3=F

Atama 100: ¬x3 = T → kök true: yazdır.

∨=T∨=T∧=Tx1=T¬=Tx2=F∧=F¬=Fx1=Tx3=T¬=Fx3=T

Atama 101: x1 ∧ ¬x2 = T → kök true: yazdır.

∨=T∨=F∧=Fx1=T¬=Fx2=T∧=F¬=Fx1=Tx3=F¬=Tx3=F

Atama 110: ¬x3 = T → kök true: yazdır.

∨=F∨=F∧=Fx1=T¬=Fx2=T∧=F¬=Fx1=Tx3=T¬=Fx3=T

Atama 111: hiçbir terim T değil → kök false.

∨=F∨=F∧=Fx1=T¬=Fx2=T∧=F¬=Fx1=Tx3=T¬=Fx3=T

8 atamanın 7’si formülü sağlar; yalnız x1 = x2 = x3 = 1 sağlamaz. İlk çözümde return eden bir yordam 000’da dururdu.

int m, x1, x2, x3;
for (m = 0; m < 8; m++) {    /* 2^3 atama */
  x1 = (m >> 2) & 1;
  x2 = (m >> 1) & 1;
  x3 = m & 1;
  yapraklari_yerlestir(kok, x1, x2, x3);
  kok_sonda_degerlendir(kok);
  if (kok->deger)
    printf("%d%d%d\n", x1, x2, x3);
}

Geri İzleme

SAT Türleri ve Ön Hesaplama (Precomputing)

CIRCUIT-SAT

Verilen bir mantık devresinin çıkışını 1 yapacak bir giriş ataması var mı?

Formula SAT

Yukarıdaki gibi ∧, ∨, ¬ ile kurulmuş bir önerme formülü.

3-CNF-SAT

Her alt cümle (clause) 3 değişken içeren ∧’lerin ∨’si: (x1 ∨ ¬x1 ∨ ¬x2) ∧ (x3 ∨ x2 ∨ x4) ∧ (¬x1 ∨ ¬x3 ∨ ¬x4).

Ön hesaplama

İşlem ağacı bir kez kurulur; her atamada yalnız yapraklar değişir. Defalarca yapılacak iş için bir kez hazırlanan veri yapısı verimi çok artırır.

Hocadan“Google’a yazılır, tak bulunur! İşte hızlı algoritma: Knuth–Morris–Pratt. Algoritma analizi kitaplarındaki greedy algoritmalar, branch and bound ve precomputing konuları mutlaka çalışılmalı!”

“I haven’t failed, I’ve found 10,000 ways that don’t work.” — Thomas Edison’a atfedilen söz.

Geri İzleme

n-Vezir (n-Queens) Problemi

n×n satranç tahtasına n veziri, hiçbiri diğerini tehdit etmeyecek biçimde yerleştirmek: aynı satır, sütun ve çaprazda iki vezir olamaz.

  • Her satıra bir vezir → çözüm bir q[0..n−1] dizisi.
  • Kaba kuvvet: nⁿ aday (satır başına bir vezirle; n = 8 için 16,7 milyon).
  • Geri izleme: satır satır ilerlenir; uygun sütun yoksa bir önceki satıra dönülür.
  • Çapraz testi: |q[i] − c| == r − i.
int q[8];  /* q[r]: r. satirdaki vezirin sutunu */

int uygun(int r, int c) {
  int i;
  for (i = 0; i < r; i++)
    if (q[i] == c || abs(q[i] - c) == r - i)
      return 0;         /* ayni sutun ya da capraz */
  return 1;
}

Horowitz–Sahni; Sedgewick. n = 4 için 2, n = 8 için 92 çözüm vardır.

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

Kod İzleme: yerlestir(0, 4)

Vr →

(0, 0) uygun → q[0] = 0, sonraki satıra geçilir.

VV××r →

Sütun 0, 1 çakışıyor (×); (1, 2) uygun → q[1] = 2, sonraki satıra geçilir.

VV××××r →

Sütun 0, 1, 2, 3 çakışıyor; satır 2’ye vezir konamaz → return 0: geri dönülür, bir üst satır sıradaki sütunu dener.

VVr →

(1, 3) uygun → q[1] = 3, sonraki satıra geçilir.

VVV×r →

Sütun 0 çakışıyor (×); (2, 1) uygun → q[2] = 1, sonraki satıra geçilir.

VVV××××r →

Sütun 0, 1, 2, 3 çakışıyor; satır 3’e vezir konamaz → return 0: geri dönülür, bir üst satır sıradaki sütunu dener.

VV××r →

Sütun 2, 3 çakışıyor; satır 2’ye vezir konamaz → return 0: geri dönülür, bir üst satır sıradaki sütunu dener.

Vr →

Kalan sütun yok; satır 1’e vezir konamaz → return 0: geri dönülür, bir üst satır sıradaki sütunu dener.

Vr →

(0, 1) uygun → q[0] = 1, sonraki satıra geçilir.

VV×××r →

Sütun 0, 1, 2 çakışıyor (×); (1, 3) uygun → q[1] = 3, sonraki satıra geçilir.

VVVr →

(2, 0) uygun → q[2] = 0, sonraki satıra geçilir.

VVVV××r →

Sütun 0, 1 çakışıyor (×); (3, 2) uygun → q[3] = 2, sonraki satıra geçilir.

VVVV

r = n: dört vezir yerleşti → çözüm q = [1, 3, 0, 2]. 8 yerleştirme yaptık; kaba kuvvet 4⁴ = 256 aday.

int yerlestir(int r, int n) {
  int c;
  if (r == n) return 1;             /* cozum */
  for (c = 0; c < n; c++)
    if (uygun(r, c)) {
      q[r] = c;
      if (yerlestir(r + 1, n)) return 1;
    }
  return 0;                  /* geri don */
}

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

Kod İzleme: altkume("abc", 3, 0, sec)

0a1b2c→ {abc}

Kararlar: al a, al b, al c → {abc}.

0a1b2c→ {ab}

Kararlar: al a, al b, alma c → {ab}.

0a1b2c→ {ac}

Kararlar: al a, alma b, al c → {ac}.

0a1b2c→ {a}

Kararlar: al a, alma b, alma c → {a}.

0a1b2c→ {bc}

Kararlar: alma a, al b, al c → {bc}.

0a1b2c→ {b}

Kararlar: alma a, al b, alma c → {b}.

0a1b2c→ {c}

Kararlar: alma a, alma b, al c → {c}.

0a1b2c→ {}

Kararlar: alma a, alma b, alma c → {}.

void altkume(char *s, int n, int i,
             int sec[]) {
  int j;
  if (i == n) {                  /* karar bitti */
    printf("{");
    for (j = 0; j < n; j++)
      if (sec[j]) printf("%c", s[j]);
    printf("}\n");
    return;
  }
  sec[i] = 1; altkume(s, n, i + 1, sec);
  sec[i] = 0; altkume(s, n, i + 1, sec);
}

Geri İzleme

Satranç Tahtasında At Gezisi (Knight’s Tour)

x01234567

At, her kareden bir kez geçmek şartıyla tüm kareleri gezecek. Hareket L biçimli: 2 yatay + 1 dikey ya da 2 dikey + 1 yatay.

Hamle01234567
Δi−2−1+1+2+2+1−1−2
Δj+1+2+2+1−1−2−2−1
  • Geri izleme: hamle denenir; çıkmaza girilince son hamle geri alınır, sıradaki denenir. Her zaman çözüm bulur, ama kaba kuvvet niteliğindedir: 8×8’de çok yavaş.
  • Sezgisel yaklaşım (heuristic): “akıllıca” seçim; optimale yakın çözer, garanti vermez (mayın tarlası gibi).

Geri izleme “senaryoyu sondan başa geri sarmak” değildir: kısmi çözümü ileri kurar, çıkmazda son seçimi geri alır.

Geri İzleme

Warnsdorff’un Sezgisel Kuralı (1823)

“Atın bir sonraki gideceği yer, ilerleme sayısı sıfır olmayan ama en az olan karedir.” İlerleme sayısı: o kareden doğrudan gidilebilecek boş kare sayısı. Önce köşeler ve kenarlar tüketilir, çıkmaz sonraya kalmaz.

155

1. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5, 5 → en küçük pozitif: 5, (1, 2) seçilir.

1213333

2. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 1, 3, 3, 3, 3 → en küçük pozitif: 1, (0, 4) seçilir.

1325

3. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5 → en küçük pozitif: 5, (2, 3) seçilir.

132413233

4. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 1, 3, 2, 3, 3 → en küçük pozitif: 1, (4, 4) seçilir.

132455

5. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5 → en küçük pozitif: 5, (3, 2) seçilir.

19328467522

9. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 2, 2 → en küçük pozitif: 2, (1, 4) seçilir.

1149321510138461116712514

16. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 1, 4 → en küçük pozitif: 1, (4, 2) seçilir.

1149203241921510138421182361116712172250

24. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 0 → en küçük pozitif: 0, (2, 2) seçilir.

11492032419215101382542118236111671217225

25 kare, hiç geri dönmeden gezildi. Warnsdorff her adımda yalnız 8 komşuya bakar: toplam iş O(n²).

Kural adını H. C. von Warnsdorff’tan alır. Eşitlikte hamle tablosundaki sıra (0…7) kullanıldı.

Özet

Karmaşıklık Özeti

AlgoritmaZamanYığın / ek bellekNot
fakt (özyinelemeli)Θ(n)O(n)kuyruk sürümü O(1)’e indirilebilir
İkili arama (döngüsel)O(log n)O(1)dizi sıralı olmalı
İkili arama (özyinelemeli)O(log n)O(log n)kuyruk özyinelemesi
HanoiΘ(2ⁿ)O(n)2ⁿ − 1 hamle
PermütasyonΘ(n·n!)O(n)n! diziliş
Alt küme üretimiΘ(n·2ⁿ)O(n)2ⁿ alt küme
SAT (kaba kuvvet)O(2ⁿ·|F|)O(|F|)|F|: formül ağacının boyu
n-vezir (geri izleme)O(n!) üst sınırO(n)budama pratikte çok daha az
At gezisi (Warnsdorff)O(n²)O(n²)sezgisel; geri izleme üstel

Özet

Sık Yapılan Hatalar

1Taban durumu unutmak ya da ona hiç ulaşmayan adım yazmak → yığın taşması.
2Özyinelemeli çağrının sonucunu return etmemek: binsearch(...) yazıp değeri atmak.
3İkili aramayı sırasız dizide ya da bağlı listede kullanmak.
4Bulunan öğenin değerini döndürmek: −1 değeriyle karışır; indis döndürülmeli.
5Geri izlemede seçimi geri almayı (ikinci SWAP, sec[i] = 0) unutmak.
6Üstel algoritmayı “makine hızlı” diye büyük n ile çalıştırmak.

Özet

Alıştırmalar

  1. Dizide en büyük öğeyi bulan özyinelemeli yordamı yazınız. Yığın derinliği ne olur?
  2. binsearch 1000 öğeli dizide en çok kaç tur döner? Aranan yoksa?
  3. hanoi(4, …) kaç kez printf çağırır, kaç kez kendini çağırır?
  4. perm("abcd", 0, 4) çıktısının ilk 6 satırını yazınız.
  5. (x1 ∨ x2) ∧ (¬x1 ∨ ¬x2) formülünü sağlayan atamaları kaba kuvvetle bulunuz.
  6. 5-vezir için geri izleme ile ilk çözümü bulunuz (q dizisi).
Cevapları / ipuçlarını göster
  1. en_buyuk(a, n) = max(a[n−1], en_buyuk(a, n−1)); taban n == 1. Derinlik n.
  2. ⌊log₂ 1000⌋ + 1 = 10 tur; aranan yoksa da en çok 10 tur sonra left > right.
  3. 15 printf (2⁴ − 1); 15 çağrı (her çağrı tam bir printf yapar).
  4. abcd, abdc, acbd, acdb, adcb, adbc.
  5. 01 ve 10 (dışlayıcı veya).
  6. q = [0, 2, 4, 1, 3].

Özet

Bölüm Özeti

Özyineleme

Taban durum + küçülen adım. Her çağrı bir yığın çerçevesi; kuyruk özyinelemesi döngüye çevrilebilir.

İkili arama

Sıralı dizide aralığı yarılar: O(log n). İndis döndürülür, −1 hata demektir.

Hanoi

h(n) = 2h(n−1) + 1 = 2ⁿ − 1. Çağrı ağacı = yığının tarihçesi.

Permütasyon

Takas + özyineleme + geri takas: n! çıktı, O(n·n!).

Geri izleme

Kısmi çözüm kurulur, kural bozulunca budanır ve geri alınır: SAT, n-vezir, alt küme.

Sezgisel

Warnsdorff: en az ilerleme sayılı kare. Daha iyi çözüm aramaktan vazgeçilmez.

Özet

Kaynaklar

  • Horowitz, Sahni, Anderson-Freed, Fundamentals of Data Structures in C, 2. baskı, Bölüm 1.2 (özyinelemeli algoritmalar).
  • Cormen ve diğ., Introduction to Algorithms, 3. baskı: Bölüm 4 (yineleme bağıntıları), Bölüm 34 (NP-tamlık, SAT).
  • Sedgewick, Algorithms in C, Bölüm 5 (özyineleme).

Son

Bölüm 3: Özyineleme, İkili Arama ve Geri İzleme
Sonraki bölüm: Bölüm 4: Bağlı Listeler · Ders sayfası
1 / 1 C ile Veri Yapıları