Bölüm 3: Özyineleme, İkili Arama ve Geri İzleme
Giriş
Öğrenme Çıktıları
Bir problemi taban durum + küçülen alt problem olarak ifade etmek; özyineleme ile döngüyü karşılaştırmak.
Her çağrının yığın çerçevesini (parametreler, yereller, dönüş adresi) çizmek.
Döngüsel ve özyinelemeli ikili aramayı yazmak; O(log n) sınırını göstermek.
Hanoi için h(n) = 2h(n−1) + 1 bağıntısını kurup çözmek; permütasyon için O(n·n!).
Kaba kuvvet ile geri izlemeyi ayırmak; SAT, n-vezir, alt küme üretimi.
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.
Fonksiyon kendini çağırır: fakt → fakt.
Çağırdığı bir fonksiyon onu geri çağırır: A → B → A.
Özyinelemesiz çözülen en küçük örnek. Yoksa çağrılar sonsuza gider (yığın taşar).
Problemi daha küçük bir örneğe indirger; her çağrı taban duruma yaklaşmalı.
Özyineleme
Yığın Çerçevesi (Stack Activation Frame)
- 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ı
main, fakt(3)’ü çağırır: yeni çerçeve açılacak.
fakt(3): n = 3 > 1, taban durum değil.
3 * fakt(2): çarpma bekler; önce fakt(2) çağrılır.
fakt(2): n = 2 > 1, taban durum değil.
2 * fakt(1): çarpma bekler; önce fakt(1) çağrılır.
fakt(1): taban durum → return 1. Yığın en derin hâlde (4 çerçeve).
fakt(2) kaldığı yerden devam: 2 · 1 = 2, çerçeve kapanır.
fakt(3) kaldığı yerden devam: 3 · 2 = 6, çerçeve kapanır.
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; }
| Özyinelemeli | Döngülü | |
|---|---|---|
| Okunabilirlik | Tanıma çok yakın | Durum değişkenleri gerekir |
| Ek bellek | O(derinlik) yığın | O(1) |
| Çağrı maliyeti | Her adımda çerçeve | Yok |
| Risk | Derin özyinelemede yığın taşması | Sonsuz döngü |
| Doğal olduğu yer | Ağaç, böl-yönet, geri izleme | Düz tarama, sayaçlar |
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) */
n * fakt(n-1): dönüşten sonra çarpma bekler, çerçeve saklanmalıdır.
acc kısmi sonucu taşır; dönüşte yapılacak iş kalmaz.
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.
Test dizisi
#define COMPARE(x, y) ((x) < (y) ? -1 : (y) < (x) ? 1 : 0)İkili Arama · Kodu adım adım çalıştır
Kod İzleme: döngüsel binsearch(list, 18, 0, 9)
binsearch(list, 18, 0, 9): aralık tüm dizi.
0 ≤ 9: 1. tur. middle = (0 + 9) / 2 = 4, list[4] = 24.
24 > 18 → COMPARE = 1: sağ yarı atılır, right = 3.
0 ≤ 3: 2. tur. middle = (0 + 3) / 2 = 1, list[1] = 13.
13 < 18 → COMPARE = −1: sol yarı atılır, left = 2.
2 ≤ 3: 3. tur. middle = (2 + 3) / 2 = 2, list[2] = 18.
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
binsearch(list, 25, 0, 9): aralık tüm dizi.
0 ≤ 9: 1. tur. middle = (0 + 9) / 2 = 4, list[4] = 24.
24 < 25 → COMPARE = −1: sol yarı atılır, left = 5.
5 ≤ 9: 2. tur. middle = (5 + 9) / 2 = 7, list[7] = 36.
36 > 25 → COMPARE = 1: sağ yarı atılır, right = 6.
5 ≤ 6: 3. tur. middle = (5 + 6) / 2 = 5, list[5] = 27.
27 > 25 → COMPARE = 1: sağ yarı atılır, right = 4.
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.
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)
Yeni çerçeve: l = 0, r = 9 → m = 4 (list[4] = 24).
24 > 18 → case 1: binsearch(list, 18, 0, 3) çağrılır.
Yeni çerçeve: l = 0, r = 3 → m = 1 (list[1] = 13).
13 < 18 → case −1: binsearch(list, 18, 2, 3) çağrılır.
Yeni çerçeve: l = 2, r = 3 → m = 2 (list[2] = 18).
list[2] = 18: case 0 → return 2.
Dönen 2 olduğu gibi yukarı iletilir (case −1 çerçevesi kapanır).
Dönen 2 olduğu gibi yukarı iletilir (case 1 çerçevesi kapanır).
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.
- 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).
| n | en çok tur | doğrusal arama |
|---|---|---|
| 10 | 4 | 10 |
| 1.000 | 10 | 1.000 |
| 1.000.000 | 20 | 1.000.000 |
| 1.000.000.000 | 30 | 1.000.000.000 |
Hanoi Kuleleri
Hanoi Kuleleri Problemi
Kaynak kulede n disk var; her disk üzerindekinden büyüktür. Amaç: hepsini hedef kuleye taşımak, ara kuleden yardım alarak.
- Her seferinde tek disk taşınır.
- Bir disk kendinden küçük bir diskin üzerine konamaz.
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")
n = 3: önce üstteki 2 disk A → B (hedef C yardımcı olur).
n = 2: önce üstteki 1 disk A → C (hedef B yardımcı olur).
n = 1: taban durum. Disk 1: A → C.
Geri dönüldü; en büyük disk 2: A → B. Sonra 1 disk C → B.
n = 1: taban durum. Disk 1: C → B.
Geri dönüldü; en büyük disk 3: A → C. Sonra 2 disk B → C.
n = 2: önce üstteki 1 disk B → A (hedef C yardımcı olur).
n = 1: taban durum. Disk 1: B → A.
Geri dönüldü; en büyük disk 2: B → C. Sonra 1 disk A → C.
n = 1: taban durum. Disk 1: A → C.
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.
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| n | hamle |
|---|---|
| 1 | 1 |
| 2 | 3 |
| 3 | 7 |
| 4 | 15 |
| 10 | 1.023 |
| 20 | 1.048.575 |
| 64 | 18.446.744.073.709.551.615 |
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.
{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
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)
i = 0, j = 0: kendisiyle takas → abc; perm(i = 1) çağrılır.
i = 1, j = 1: kendisiyle takas → abc; perm(i = 2) çağrılır.
i = n: diziliş hazır → abc yazılır.
i = 1, j = 2: list[1] ↔ list[2] → acb; perm(i = 2) çağrılır.
i = n: diziliş hazır → acb yazılır.
Geri takas: list[1] ↔ list[2] → abc (eski hâl).
i = 0, j = 1: list[0] ↔ list[1] → bac; perm(i = 1) çağrılır.
i = 1, j = 1: kendisiyle takas → bac; perm(i = 2) çağrılır.
i = n: diziliş hazır → bac yazılır.
i = 1, j = 2: list[1] ↔ list[2] → bca; perm(i = 2) çağrılır.
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)
Geri takas: list[1] ↔ list[2] → bac (eski hâl).
Geri takas: list[0] ↔ list[1] → abc (eski hâl).
i = 0, j = 2: list[0] ↔ list[2] → cba; perm(i = 1) çağrılır.
i = 1, j = 1: kendisiyle takas → cba; perm(i = 2) çağrılır.
i = n: diziliş hazır → cba yazılır.
i = 1, j = 2: list[1] ↔ list[2] → cab; perm(i = 2) çağrılır.
i = n: diziliş hazır → cab yazılır.
Geri takas: list[1] ↔ list[2] → cba (eski hâl).
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
Kök: sabitlenmiş önek yok · her seviye bir konumu sabitler · yapraklar: çıktıdaki sırayla 6 diziliş
1 + n + n(n−1) + … + n! çağrı; yaprak sayısı n!.
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.
Kardeş dallar aynı başlangıç dizisini görmeli; ikinci SWAP diziyi onarır.
Geri İzleme
Kaba Kuvvet ve Geri İzleme (Backtracking)
Olası tüm adaylar üretilir, her biri sınanır. Bilgisayarın gücüne güvenir; aday sayısı 2ⁿ, n! … olabilir.
Çö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; }
}Geri İzleme
Satisfiability (SAT) Problemi
(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.
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/YANLISolarak 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
Atama 000: ¬x3 = T → kök true: yazdır.
Atama 001: ¬x1 ∧ x3 = T → kök true: yazdır.
Atama 010: ¬x3 = T → kök true: yazdır.
Atama 011: ¬x1 ∧ x3 = T → kök true: yazdır.
Atama 100: ¬x3 = T → kök true: yazdır.
Atama 101: x1 ∧ ¬x2 = T → kök true: yazdır.
Atama 110: ¬x3 = T → kök true: yazdır.
Atama 111: hiçbir terim T değil → kök false.
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)
Verilen bir mantık devresinin çıkışını 1 yapacak bir giriş ataması var mı?
Yukarıdaki gibi ∧, ∨, ¬ ile kurulmuş bir önerme formülü.
Her alt cümle (clause) 3 değişken içeren ∧’lerin ∨’si: (x1 ∨ ¬x1 ∨ ¬x2) ∧ (x3 ∨ x2 ∨ x4) ∧ (¬x1 ∨ ¬x3 ∨ ¬x4).
İş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.
“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)
(0, 0) uygun → q[0] = 0, sonraki satıra geçilir.
Sütun 0, 1 çakışıyor (×); (1, 2) uygun → q[1] = 2, sonraki satıra geçilir.
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.
(1, 3) uygun → q[1] = 3, sonraki satıra geçilir.
Sütun 0 çakışıyor (×); (2, 1) uygun → q[2] = 1, sonraki satıra geçilir.
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.
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.
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.
(0, 1) uygun → q[0] = 1, sonraki satıra geçilir.
Sütun 0, 1, 2 çakışıyor (×); (1, 3) uygun → q[1] = 3, sonraki satıra geçilir.
(2, 0) uygun → q[2] = 0, sonraki satıra geçilir.
Sütun 0, 1 çakışıyor (×); (3, 2) uygun → q[3] = 2, sonraki satıra geçilir.
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)
Kararlar: al a, al b, al c → {abc}.
Kararlar: al a, al b, alma c → {ab}.
Kararlar: al a, alma b, al c → {ac}.
Kararlar: al a, alma b, alma c → {a}.
Kararlar: alma a, al b, al c → {bc}.
Kararlar: alma a, al b, alma c → {b}.
Kararlar: alma a, alma b, al c → {c}.
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)
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.
| Hamle | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Δ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.
1. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5, 5 → en küçük pozitif: 5, (1, 2) seçilir.
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.
3. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5 → en küçük pozitif: 5, (2, 3) seçilir.
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.
5. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 5 → en küçük pozitif: 5, (3, 2) seçilir.
9. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 2, 2 → en küçük pozitif: 2, (1, 4) seçilir.
16. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 1, 4 → en küçük pozitif: 1, (4, 2) seçilir.
24. kare. Adaylar (yeşil sayı = oradan sonraki hamle sayısı): 0 → en küçük pozitif: 0, (2, 2) seçilir.
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
| Algoritma | Zaman | Yığın / ek bellek | Not |
|---|---|---|---|
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ır | O(n) | budama pratikte çok daha az |
| At gezisi (Warnsdorff) | O(n²) | O(n²) | sezgisel; geri izleme üstel |
Özet
Sık Yapılan Hatalar
return etmemek: binsearch(...) yazıp değeri atmak.sec[i] = 0) unutmak.Özet
Alıştırmalar
- Dizide en büyük öğeyi bulan özyinelemeli yordamı yazınız. Yığın derinliği ne olur?
binsearch1000 öğeli dizide en çok kaç tur döner? Aranan yoksa?- hanoi(4, …) kaç kez
printfçağırır, kaç kez kendini çağırır? - perm("abcd", 0, 4) çıktısının ilk 6 satırını yazınız.
- (x1 ∨ x2) ∧ (¬x1 ∨ ¬x2) formülünü sağlayan atamaları kaba kuvvetle bulunuz.
- 5-vezir için geri izleme ile ilk çözümü bulunuz (q dizisi).
Cevapları / ipuçlarını göster
- en_buyuk(a, n) = max(a[n−1], en_buyuk(a, n−1)); taban n == 1. Derinlik n.
- ⌊log₂ 1000⌋ + 1 = 10 tur; aranan yoksa da en çok 10 tur sonra left > right.
- 15 printf (2⁴ − 1); 15 çağrı (her çağrı tam bir printf yapar).
- abcd, abdc, acbd, acdb, adcb, adbc.
- 01 ve 10 (dışlayıcı veya).
- q = [0, 2, 4, 1, 3].
Özet
Bölüm Özeti
Taban durum + küçülen adım. Her çağrı bir yığın çerçevesi; kuyruk özyinelemesi döngüye çevrilebilir.
Sıralı dizide aralığı yarılar: O(log n). İndis döndürülür, −1 hata demektir.
h(n) = 2h(n−1) + 1 = 2ⁿ − 1. Çağrı ağacı = yığının tarihçesi.
Takas + özyineleme + geri takas: n! çıktı, O(n·n!).
Kısmi çözüm kurulur, kural bozulunca budanır ve geri alınır: SAT, n-vezir, alt küme.
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).