Bölüm 1: Giriş ve Algoritma Analizi
Giriş
Öğrenme Çıktıları
Sistem yaşam döngüsünün beş aşamasını ve her aşamada veri yapısı kararlarının yerini açıklamak.
Algoritma ve soyut veri tipi (ADT) kavramlarını tanımlamak; C'de başlık dosyası ile arayüz kurmak.
Bir C yordamının bellek gereksinimini ve adım sayısını çıkarmak (sum, rsum, add).
Adım sayısını O, Ω ve Θ ile ifade etmek; en iyi, ortalama ve en kötü durumu ayırmak.
Alışılmış karmaşıklık sınıflarının n büyüdükçe nasıl ayrıştığını yorumlamak.
clock() ile işletim süresini ölçmek ve ölçüm tuzaklarını (saat vuruşu, çıktı) tanımak.
Derse Giriş · kitaplarda yazmaz!
Ders Kitapları ve Kaynaklar
Kernighan ve Ritchie, The C Programming Language. C'nin tasarımcısının kitabı; her an el altında bulunması gereken bir başvuru kaynağı.
Horowitz, Sahni ve Anderson-Freed, Fundamentals of Data Structures in C.
Aynı kitabın üç sürümü: içerik hemen hemen aynı, yalnızca kodların dili değişiyor.
Ek kaynaklar: Cormen ve diğ. (CLRS) Introduction to Algorithms; Sedgewick, Algorithms in C.
Derse Giriş · Okuma parçası
Okuma Parçası: Meslek Sevgisi
Bugün yaşıyor ama aklı 5500 yıl öncesinde: “Firavunun karısı oraya bu sebeple gömülmüş olamaz” diyebiliyor.
Kara deliği ispatlamak için yıllarca fotoğraf çekiyor; bir yıldızın bir piksel oynamasına seviniyor.
8 saat yol, 150 TL bilet, taş ve değnek riski… Ama konferans için aynı yolu gideni “aptal” sayıyoruz.
“Bir ömür mutlu olmanın yolu, işini sevmektir.” — Çin atasözü
Temeller
Sistem Yaşam Döngüsü (System Life Cycle)
Yazılımlar, aşamaları aşağıdaki gibi olan bir süreçten geçer:
Gereksinimler: Proje, amacını belirten şartlarla başlar: programcıya hangi girdiler verilecek, hangi çıktılar üretilecek?
Çözümleme: Problem parçalara ayrılır. İki yol var: aşağıdan yukarı (bottom-up) ya da yukarıdan aşağı (top-down).
Tasarım: Veri nesneleri (→ ADT) ve işlemler (→ algoritma) tanımlanır; ikisi de dilden bağımsızdır. Tasarıma ayrılan zaman arttıkça hata azalır.
İnceltme ve kodlama: Seçilen veri yapıları üzerinde algoritmalar kodlanır. Veri gösterimi, algoritmanın etkinliğini belirler.
Doğrulama: Doğruluk ispatı, geniş test verisiyle sınama ve hata giderme. Her biri başlı başına bir araştırma konusu.
Horowitz–Sahni, Bölüm 1
Temeller
Çözümleme: Aşağıdan Yukarı mı, Yukarıdan Aşağı mı?
Aşağıdan yukarı (bottom-up)
- Problem parçalara bölünür, her parçaya ayrı odaklanılır.
- Modüller doğrudan görülür; problem modül modül çözülür.
- Proje konusunda tecrübeliysek tercih edilir.
Yukarıdan aşağı (top-down)
- Önce bir ana plan: “Amaç ne?”, “Son ürün ne olacak?”
- Problem alt parçalara bölünür; daha çok zaman harcanır.
- Geri dönüş ve hata olasılığı daha azdır; tecrübemiz yoksa tercih edilir.
Temeller
Kodlama ve Doğrulama
Matematiksel tekniklerle program doğruluğu ispatlanır; büyük projede baştan sona ispat neredeyse imkânsız. Doğruluğu ispatlanmış algoritmaları kullanmak hatayı azaltır.
Çalışan kod ve tüm senaryoları kapsayan test verisi gerekir. Sözdizimi hatası vermeden çalışan program doğru demek değildir.
İspat ve testler hatayı gösterirse, tasarım ve kodlama kararlarına göre hata ayıklanır.
Hatasız ama yavaş çalışan bir programın da fazla değeri yoktur: doğruluk ile verimlilik ayrı sorulardır.
Temeller
Algoritma Nedir?
Algoritma, belirli bir görevi yerine getiren sonlu sayıda komut dizisidir. Her algoritma şu ölçütleri sağlamalıdır:
| Ölçüt | Anlamı |
|---|---|
| Girdi | Dışarıdan sıfır ya da daha fazla nicelik alınır. |
| Çıktı | En az bir nicelik üretilir. |
| Kesinlik (definiteness) | Her komut açık ve tek anlamlıdır: “x’e 6 ya da 7 ekle” olmaz. |
| Sonluluk (finiteness) | Her durumda, sonlu adım sonra sona erer. |
| Etkinlik (effectiveness) | Her komut, kâğıt kalemle yapılabilecek kadar temel ve uygulanabilirdir. |
Horowitz–Sahni, Bölüm 1
Temeller
Soyut Veri Tipi (Abstract Data Type)
Veri tipi = nesneler + işlemler. ADT: nesnelerin ve işlemlerin belirtiminin (specification), gösterimden ve gerçeklemeden (implementation) ayrıldığı veri tipi.
İşlem adları, parametre türleri, sonuç türü ve işlemin ne yaptığı. Kullanıcı yalnızca bunu bilir.
Verinin bellekte gösterimi (struct, dizi…) ve işlemleri yapan kod. Kullanıcıdan gizlenir.
ADT Kesir
nesneler: pay/payda, payda ≠ 0
işlemler:
olustur(p, q) → Kesir /* kurucu */
topla(a, b) → Kesir /* dönüştürücü */
esit(a, b) → Boolean /* gözlemci */
yaz(a) /* raporlayıcı */
sil(a)Horowitz–Sahni, Bölüm 1.3
Temeller
C’de Soyutlama: Başlık Dosyası + struct
/* kesir.h : arayuz (NE?) */ #ifndef KESIR_H #define KESIR_H typedef struct kesir Kesir; /* opak */ Kesir *kesir_olustur(int pay, int payda); Kesir *kesir_topla(const Kesir *a, const Kesir *b); int kesir_esit(const Kesir *a, const Kesir *b); void kesir_yaz(const Kesir *k); void kesir_sil(Kesir *k); #endif
/* kesir.c : gercekleme (NASIL?) */ #include <stdio.h> #include <stdlib.h> #include "kesir.h" struct kesir { int pay, payda; }; Kesir *kesir_olustur(int pay, int payda) { Kesir *k; if (payda == 0) return NULL; k = malloc(sizeof *k); if (k == NULL) return NULL; k->pay = pay; k->payda = payda; return k; }
typedef struct kesir Kesir; eksik (opak) tip bildirir: kesir.h’yi kullanan dosya yapının alanlarını göremez, yalnızca Kesir * tutar. Alanlar kesir.c’de gizlidir → ADT’nin C’deki karşılığı.Temeller
Gerçeklemenin Kalanı ve Kullanımı
Kesir *kesir_topla(const Kesir *a, const Kesir *b) { return kesir_olustur( a->pay * b->payda + b->pay * a->payda, a->payda * b->payda); } int kesir_esit(const Kesir *a, const Kesir *b) { return a->pay * b->payda == b->pay * a->payda; } void kesir_yaz(const Kesir *k) { printf("%d/%d\n", k->pay, k->payda); } void kesir_sil(Kesir *k) { free(k); }
/* main.c : yalnizca arayuzu bilir */ #include "kesir.h" #include <stddef.h> /* NULL */ int main(void) { Kesir *a = kesir_olustur(1, 2); Kesir *b = kesir_olustur(1, 3); Kesir *t = NULL; if (a && b) t = kesir_topla(a, b); if (t) kesir_yaz(t); /* 5/6 */ kesir_sil(a); kesir_sil(b); kesir_sil(t); /* free(NULL) zararsiz */ return 0; }
kesir_olustur payda 0 ise ya da bellek yoksa NULL döner; çağıran bunu denetler.
Her olustur/topla sonucu çağırana aittir; kesir_sil ile bırakılır.
Seçimli Sıralama
Seçimli Sıralama (Selection Sort)
Sıralı olmayan bir diziyi küçükten büyüğe sıralamak istiyoruz. Her turda kalan kısmın en küçüğü bulunur ve başa (i. konuma) taşınır:
for (i = 0; i < n; i++) {
list[i] .. list[n-1] arasini incele;
en kucuk tamsayi list[min] olsun;
list[i] ile list[min]'i takas et;
}Seçimli Sıralama
Seçimli Sıralama: C Gerçeklemesi
#include <stdio.h> #include <stdlib.h> #define MAX_SIZE 101 #define SWAP(x, y, t) ((t) = (x), (x) = (y), (y) = (t)) void sort(int list[], int n); int main(void) { int i, n, list[MAX_SIZE]; printf("Dizi boyunu giriniz: "); if (scanf("%d", &n) != 1 || n < 1 || n > MAX_SIZE) { printf("Hatali n degeri!\n"); return 1; } for (i = 0; i < n; i++) list[i] = rand() % 100; sort(list, n); for (i = 0; i < n; i++) printf("%d ", list[i]); return 0; }
void sort(int list[], int n) { int i, j, min, temp; for (i = 0; i < n - 1; i++) { min = i; for (j = i + 1; j < n; j++) if (list[j] < list[min]) min = j; SWAP(list[i], list[min], temp); } }
Seçimli Sıralama · Sık yapılan hata
Sık Görülen Hatalı sort() Nerede Ayrılıyor?
void sort(int list[], int n) { int i, j, min, temp; for (i = 0; i < n; i++) { min = i; for (j = 0; j < n; j++) if (list[j] > list[min]) { min = j; SWAP(list[i], list[min], temp); } } }
| Girdi | Sürüm | Karş. | Takas* |
|---|---|---|---|
| {29,10,14,37,13} | hatalı | 25 | 7 |
| seçimli | 10 | 3 | |
| 100 rastgele | hatalı | 10000 | 2561 |
| seçimli | 4950 | 94 | |
| 500…1 (azalan) | hatalı | 250000 | 124750 |
| seçimli | 124750 | 250 |
j = 0’dan başlar, takas döngünün içinde yapılır. Doğru sürüm (j = i + 1, takas iç döngüden sonra) bu bölümde kullanılan sürümdür. *Takas: iki farklı hücrenin yer değiştirdiği SWAP’lar (i ≠ min).Seçimli Sıralama
SWAP: Makro mu, Fonksiyon mu?
Makro
#define SWAP(x, y, t) \ ((t) = (x), (x) = (y), (y) = (t)) int temp; SWAP(list[i], list[min], temp);
- Çağrı maliyeti yok: önişlemci metni yerine koyar.
- Geçici değişken
tdışarıdan verilir.
Fonksiyon
void swap(int *x, int *y) { int temp = *x; *x = *y; *y = temp; } swap(&list[i], &list[min]);
- Tür denetimi var;
tempyerel olur. - Değişkenlerin adresleri gönderilmelidir.
SWAP(a[i++], a[j], t) i’yi iki kez artırır! Fonksiyona da SWAP adı verilirse makroyla çakışır; bu yüzden fonksiyonun adı swap.Seçimli Sıralama · Kodu adım adım çalıştır
Kod İzleme: swap(&a, &b)
main’in çerçevesinde a = 3, b = 8.
Çağrı: değerler değil adresler kopyalanır. x → a, y → b.
temp = *x: x’in gösterdiği yerdeki değer (3) okunur.
*x = *y: y’nin gösterdiği 8, x’in gösterdiği yere, yani a’ya yazılır.
*y = temp: b’ye 3 yazılır. Takas main’in değişkenlerinde gerçekleşti.
swap’ın çerçevesi yok oldu (x, y, temp gitti) ama etkisi kaldı.
void swap(int *x, int *y) { int temp = *x; *x = *y; *y = temp; } int main(void) { int a = 3, b = 8; swap(&a, &b); printf("%d %d\n", a, b); }
Performans Analizi
Bir Programı Nasıl Değerlendiririz?
- İsteneni karşılıyor mu?
- Doğru çalışıyor mu?
- Nasıl işletileceği ve kullanılacağı belgelenmiş mi (açıklama satırları dâhil)?
- Mantıksal sonuç üretmek için fonksiyonları etkili kullanıyor mu?
- Kod okunabilir mi?
- Ana ve ikincil belleği etkili kullanıyor mu?
- İşletim zamanı kabul edilebilir mi?
Makineden bağımsız: zaman ve bellek için tahmin (karmaşıklık).
Makineye bağlı: gerçek işletim süresi ölçülür.
Performans Analizi
Alan Karmaşıklığı (Space Complexity)
Programın işini tamamlaması için gereken bellek miktarı: S(P) = c + SP(I)
Girdi/çıktı büyüklüğünden bağımsız: kodun kendisi, basit değişkenler, sabitler.
I girdisinin özelliklerine (çoğunlukla boyutuna) bağlı kısım. Çözümlemede bununla ilgileniriz.
float abc(float a, float b, float c) { return a + b + b * c + (a + b - c) / (a + b) + 4.00; } /* yalniz sabit bellek: S_abc(I) = 0 */
Performans Analizi
sum ve rsum: Değişken Bellek
float sum(float list[], int n) { float tempsum = 0; int i; for (i = 0; i < n; i++) tempsum += list[i]; return tempsum; } float rsum(float list[], int n) { if (n) return rsum(list, n - 1) + list[n - 1]; return 0; }
sum’a dizinin ilk öğesinin adresi gider → Ssum(n) = 0. Öğeler değerle kopyalansaydı Ssum(n) = n olurdu.rsum(list, n)toplam n+1 kez çağrılır; her çağrı yığında bir çerçeve açar → Srsum(n) ∝ n.
Performans Analizi
Zaman Karmaşıklığı (Time Complexity)
T(P) = derleme zamanı + işletim zamanı. Derleme bir kez yapılır; biz işletim zamanı tP ile ilgileniriz.
Her işlemin süresi: derleyiciye ve makineye bağlı, önceden bilmek zor.
Bunun yerine program adımlarını sayarız: makineden bağımsız, anlamlı bir birim.
Performans Analizi
Adım Sayma Tablosu: sum()
| Deyim | s/e | Sıklık | Toplam |
|---|---|---|---|
float sum(float list[], int n) { | 0 | 0 | 0 |
float tempsum = 0; | 1 | 1 | 1 |
int i; | 0 | 0 | 0 |
for (i = 0; i < n; i++) | 1 | n + 1 | n + 1 |
tempsum += list[i]; | 1 | n | n |
return tempsum; | 1 | 1 | 1 |
| Toplam | 2n + 3 |
for testi n+1 kez yapılır: n kez “devam”, bir kez de “bitti”. Bildirimler adım sayılmaz.Performans Analizi · Kodu adım adım çalıştır
Kod İzleme: count ile sum(list, 3)
Global count = 0. Her program adımında bir artacak.
Atama bir adım → count = 1.
for testi 0 < 3 doğru → bir adım (count = 2).
tempsum += list[0] → 3; count = 3.
for testi 1 < 3 doğru → bir adım (count = 4).
tempsum += list[1] → 4; count = 5.
for testi 2 < 3 doğru → bir adım (count = 6).
tempsum += list[2] → 8; count = 7.
Son test 3 < 3 yanlış; o da bir adım → count = 8.
return: count = 9 = 2·3 + 3. Tablodaki 2n + 3 doğrulandı.
float sum(float list[], int n) { float tempsum = 0; count++; int i; for (i = 0; i < n; i++) { count++; /* for testi */ tempsum += list[i]; count++; } count++; /* for'un son testi */ count++; return tempsum; }
Performans Analizi
Adım Sayma Tablosu: rsum()
| Deyim | s/e | Sıklık | Toplam |
|---|---|---|---|
float rsum(float list[], int n) { | 0 | 0 | 0 |
if (n) | 1 | n + 1 | n + 1 |
return rsum(list, n-1) + list[n-1]; | 1 | n | n |
return 0; | 1 | 1 | 1 |
| Toplam | 2n + 2 |
rsum (2n+2) sum’dan (2n+3) “daha az adım” görünür; ama her özyinelemeli çağrının (çerçeve açma, dönüş) maliyeti bir döngü turundan fazladır.
Önemli olan n büyüdükçe adım sayısının nasıl büyüdüğüdür: ikisi de doğrusal.
Performans Analizi · Kodu adım adım çalıştır
Kod İzleme: rsum(list, 3) ve çağrı yığını
rsum(list, 3): if testi (count = 1); n = 3 ≠ 0.
count = 2; önce rsum(list, 2) hesaplanmalı → yeni çerçeve.
rsum(list, 2): if testi (count = 3); n = 2 ≠ 0.
count = 4; önce rsum(list, 1) hesaplanmalı → yeni çerçeve.
rsum(list, 1): if testi (count = 5); n = 1 ≠ 0.
count = 6; önce rsum(list, 0) hesaplanmalı → yeni çerçeve.
rsum(list, 0): if testi (count = 7); n = 0 → taban durum.
count = 8; return 0. Yığın en derin hâlinde: 4 çerçeve.
Dönen değere list[0] = 3 eklenir → rsum(list, 1) = 3.
Dönen değere list[1] = 1 eklenir → rsum(list, 2) = 4.
Dönen değere list[2] = 4 eklenir → rsum(list, 3) = 8. count = 8 = 2·3 + 2.
float rsum(float list[], int n) { count++; /* if testi */ if (n) { count++; /* toplama + return */ return rsum(list, n - 1) + list[n - 1]; } count++; return 0; }
Performans Analizi
Adım Sayma: Matris Toplama add()
| Deyim | Sıklık |
|---|---|
void add(int a[][MAX_SIZE], int b[][MAX_SIZE], int c[][MAX_SIZE], int rows, int cols) { | 0 |
int i, j; | 0 |
for (i = 0; i < rows; i++) | rows + 1 |
for (j = 0; j < cols; j++) | rows · (cols + 1) |
c[i][j] = a[i][j] + b[i][j]; | rows · cols |
| Toplam | 2·rows·cols + 2·rows + 1 |
“O(rows, cols)” yazmak yanlıştır; doğrusu çarpım: O(rows·cols).
Performans Analizi
En İyi, Ortalama ve En Kötü Durum
Aynı n için adım sayısı girdinin kendisine de bağlı olabilir. Doğrusal aramada:
| Durum | Ne zaman? | Karşılaştırma |
|---|---|---|
| En iyi | Aranan ilk hücrede | 1 → Θ(1) |
| Ortalama | Aranan var ve her konum eşit olasılıklı | (n + 1) / 2 → Θ(n) |
| En kötü | Aranan son hücrede ya da hiç yok | n → Θ(n) |
CLRS Bölüm 2.2
Asimptotik Gösterim
Büyük O (Big-O) Gösterimi
Tanım: Tüm n ≥ n0 için f(n) ≤ c·g(n) olacak biçimde c > 0 ve n0 sabitleri varsa f(n) = O(g(n)) yazılır: “algoritmanın karmaşıklığı g(n) derecesindedir”.
- Tsum(n) = 2n + 3 = O(n)
- Trsum(n) = 2n + 2 = O(n)
- Tadd = 2·rows·cols + 2·rows + 1 = O(rows·cols) → O(n²)
O bir üst sınırdır; “=” burada “elemanıdır” anlamında tek yönlü okunur.
Asimptotik Gösterim
Örnek: f(n) = 3n³ + 6n² + 7n = O(n³)
Baskın olmayan 7n terimini n² ile sınırlarız: n ≥ 7 olduğunda 7n ≤ n².
6n² + n² = 7n².
Aynı hile bir kez daha: n ≥ 7 için 7n² ≤ n³.
c = 4 ve n₀ = 7 bulundu: tanım sağlandı.
İşletim zamanı hiçbir zaman 4n³ adımı aşmaz (n ≥ 7).
100 sayı 2 sn’de sıralanıyorsa 300 sayı en fazla 2·3³ = 54 sn sürer (6 sn değil!).
Asimptotik Gösterim
Alt Sınır Ω ve Sıkı Sınır Θ
| Gösterim | Tanım (tüm n ≥ n₀ için) | Anlamı |
|---|---|---|
| f(n) = O(g(n)) | f(n) ≤ c·g(n) | g, f’nin üst sınırı |
| f(n) = Ω(g(n)) | f(n) ≥ c·g(n) | g, f’nin alt sınırı |
| f(n) = Θ(g(n)) | c₁·g(n) ≤ f(n) ≤ c₂·g(n) | hem O hem Ω: sıkı sınır |
- 3n + 2 = Θ(n): n ≥ 2 için 3n ≤ 3n + 2 ≤ 4n.
- 3n + 2 = O(n²) de doğrudur ama bilgi vermez; mümkün olan en küçük g(n)’yi yazarız.
- Seçimli sıralama her girdide n(n−1)/2 karşılaştırma yapar → Θ(n²).
Asimptotik Gösterim
Alışılmış Karmaşıklıklar (Practical Complexities)
| Karmaşıklık | Adı | n=1 | n=2 | n=4 | n=8 | n=16 | n=32 |
|---|---|---|---|---|---|---|---|
| 1 | Sabit (constant) | 1 | 1 | 1 | 1 | 1 | 1 |
| lg n | Logaritmik | 0 | 1 | 2 | 3 | 4 | 5 |
| n | Doğrusal (linear) | 1 | 2 | 4 | 8 | 16 | 32 |
| n·lg n | Log-doğrusal | 0 | 2 | 8 | 24 | 64 | 160 |
| n² | Karesel (quadratic) | 1 | 4 | 16 | 64 | 256 | 1.024 |
| n³ | Kübik (cubic) | 1 | 8 | 64 | 512 | 4.096 | 32.768 |
| 2ⁿ | Üstel (exponential) | 2 | 4 | 16 | 256 | 65.536 | 4,29·109 |
| n! | Faktöriyel | 1 | 2 | 24 | 40.320 | 2,09·1013 | 2,63·1035 |
Asimptotik Gösterim
n Büyüdükçe: Büyüme Grafiği
Dikey ölçek 0–150: lg n, n ve n·lg n birbirinden ayrışıyor; n² hızla tırmanıyor, n³ ve 2ⁿ çoktan dışarıda.
Ölçek 0–4500: n³ = 1728 (n = 12), 2ⁿ = 4096 (n = 12); n! ise n = 7’de 5040 ile grafikten çıkar. Doğrusal fonksiyonlar tabana yapışmış görünür.
Asimptotik Gösterim
Saniyede 1 Milyar Komut İşleyen Makinede Süreler
| n | n | lg n | n·lg n | n² | n³ | n⁴ | n¹⁰ | 2ⁿ |
|---|---|---|---|---|---|---|---|---|
| 10 | 0,01 μs | 0,00332 μs | 0,0332 μs | 0,1 μs | 1 μs | 10,0 μs | 10,0 sn | 1,02 μs |
| 20 | 0,02 μs | 0,00432 μs | 0,0864 μs | 0,4 μs | 8 μs | 160 μs | 2,84 saat | 1,05 ms |
| 30 | 0,03 μs | 0,00491 μs | 0,147 μs | 0,9 μs | 27,0 μs | 810 μs | 6,83 gün | 1,07 sn |
| 40 | 0,04 μs | 0,00532 μs | 0,213 μs | 1,6 μs | 64,0 μs | 2,56 ms | 4,05 ay | 18,3 dk |
| 50 | 0,05 μs | 0,00564 μs | 0,282 μs | 2,5 μs | 125 μs | 6,25 ms | 3,1 yıl | 13,0 gün |
| 100 | 0,1 μs | 0,00664 μs | 0,664 μs | 10,0 μs | 1 ms | 100 ms | 31,7 asır | 4,02·10¹¹ asır |
| 1000 | 1 μs | 0,00997 μs | 9,97 μs | 1 ms | 1 sn | 16,7 dk | 3,17·10¹¹ asır | 3,40·10²⁸² asır |
| 10000 | 10,0 μs | 0,0133 μs | 133 μs | 100 ms | 16,7 dk | 3,86 ay | 3,17·10²¹ asır | 6,33·10²⁹⁹¹ asır |
Her adım 1 ns kabul edildi; 1 yıl = 365 gün, 1 ay = 30 gün. n = 10000 için 2ⁿ yaklaşık 102992 asır.
Asimptotik Gösterim
Amortize Analize Kısa Giriş
Bazı işlemler bazen pahalı, çoğunlukla ucuzdur. Amortize analiz, n işlemlik bir dizinin toplam maliyetini n’e böler. Örnek: doluysa kapasitesi ikiye katlanan dizi (realloc); bu dizinin adım adım izlemesi ve kopya sayımı dinamik dizilerle birlikte işlenir.
CLRS 3. baskı, Bölüm 17 (Amortized Analysis)
Sihirli Kare
Sihirli Kare (Magic Square) Problemi
Her satır, sütun ve köşegen: 65
1’den n²’ye kadar sayılar n×n kareye, her satır, sütun ve iki ana köşegenin toplamı eşit olacak biçimde yerleştirilir: toplam n(n²+1)/2 (5×5 için 65).
Coxeter kuralı (n tek; Siyam ya da de la Loubère yöntemi olarak da bilinir): İlk satırın ortasına 1 yazılır. Hep sol üst çapraza gidilir; kare doluysa bulunulan karenin altına inilir. Tablonun dışına taşılırsa karşı kenardan dönülür (sarmal). n² adımda biter → O(n²).
Sihirli Kare
“Bilgisayar Hızlı, Hemen Çözer!” mi?
Sık duyulan bir itiraz: 10 GHz’lik yeni bir bilgisayar bu problemi hemen çözer; algoritmayla uğraşmaya gerek yok.
4,9 milyon asır → yaklaşık 490 bin asır. Hâlâ umutsuz.
Coxeter kuralı: 25 adım, mikrosaniyenin altında. Algoritma, donanımdan önce gelir.
Hesap: 25! / (10¹⁰ · 3,15·10⁹) ≈ 4,9·10⁵ asır.
Sihirli Kare · Kodu adım adım çalıştır
Kod İzleme: sihirli(3, k)
İlk satırın ortası: i = 0, j = 1 → 1 yazılır.
s = 2: sol üst çapraz (2, 0) boş (satır taştı → en alt satır) → oraya geçilir.
s = 3: sol üst çapraz (1, 2) boş (sütun taştı → en sağ sütun) → oraya geçilir.
s = 4: sol üst çapraz (0, 1) dolu (orada 1 var) → bir aşağı: (2, 2).
s = 5: sol üst çapraz (1, 1) boş → oraya geçilir.
s = 6: sol üst çapraz (0, 0) boş → oraya geçilir.
s = 7: sol üst çapraz (2, 2) dolu (orada 4 var) → bir aşağı: (1, 0).
s = 8: sol üst çapraz (0, 2) boş (sütun taştı → en sağ sütun) → oraya geçilir.
s = 9: sol üst çapraz (2, 1) boş (satır taştı → en alt satır) → oraya geçilir.
s = 10 > 9: döngü biter. Her satır, sütun ve köşegenin toplamı 15 = 3·(9+1)/2.
void sihirli(int n, int k[][MAX]) { int i = 0, j = (n - 1) / 2, s, r, c; k[i][j] = 1; /* k sifirlanmis olmali */ for (s = 2; s <= n * n; s++) { r = (i - 1 < 0) ? n - 1 : i - 1; /* yukari */ c = (j - 1 < 0) ? n - 1 : j - 1; /* sol */ if (k[r][c]) i = (i + 1) % n; /* in */ else { i = r; j = c; } k[i][j] = s; } }
Performans Ölçümü
Performans Ölçümü: İki Yöntem
| 1 · İşleyici zamanı (clock) | 2 · Takvim zamanı (time) | |
|---|---|---|
| Başlangıç | start = clock(); | start = time(NULL); |
| Bitiş | stop = clock(); | stop = time(NULL); |
| Tür | clock_t | time_t |
| Sonuç (sn) | (double)(stop - start) / CLOCKS_PER_SEC | difftime(stop, start) |
Program için harcanan işlemci saat vuruşu (clock tick) sayısı. Çözünürlüğü sistemin vuruş aralığıyla sınırlıdır.
Duvar saati; saniye çözünürlüğü. Uzun işler için uygun, kısa işler için kaba.
CLK_TCK eski, standart dışı bir addır; standart C’de CLOCKS_PER_SEC kullanılır. Ölçerken çevre birimleriyle (ekran, disk, yazıcı) işlem yavaştır: printf’ler ölçülen bölgenin dışında tutulmalıdır.Performans Ölçümü
Uygulama 1: Seçimli Sıralamayı Ölçmek
#include <stdio.h> #include <time.h> #define MAX_SIZE 5001 #define ITERATIONS 20 void sort(int list[], int n); /* secmeli siralama */ int main(void) { int i, j, list[MAX_SIZE]; int sizelist[] = { 0, 100, 300, 600, 900, 1200, 1500, 2000, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, 5000 }; clock_t start, stop; double ms; for (i = 0; i < ITERATIONS; i++) { for (j = 0; j < sizelist[i]; j++) list[j] = sizelist[i] - j; /* azalan dizi */ start = clock(); sort(list, sizelist[i]); stop = clock(); ms = 1000.0 * (stop - start) / CLOCKS_PER_SEC; printf("%4d eleman: %6.1f ms\n", sizelist[i], ms); } return 0; }
- 20 farklı n; her biri için büyükten küçüğe dizilmiş dizi.
- Yalnızca
sortçağrısı ikiclock()arasında. (int)(1000*(stop-start)/CLK_TCK)yazılırsa tamsayı bölmesi küsuratı atar;1000.0ile double aritmetik daha doğru.
Performans Ölçümü
Ölçüm Sonuçları ve Tutarsızlıklar
Noktalar: örnek ölçüm · kesikli: c·n² eğrisi
- 600 öğeye kadar hepsi 0 ms?
- 3000 öğe 110 ms, 3100 ise 93 ms?
- 3500, 3600, 3700 hepsi 125 ms?
stop − start = 0. Vuruş aralığı ~15,6 ms olan bir sistemde sonuçlar 15–16 ms’nin katlarına yuvarlanır.Performans Ölçümü
Uygulama 2: Doğrusal Arama (Bekçi ile)
int seqsearch(int list[], int searchnum, int n) { int i; list[n] = searchnum; /* bekci */ for (i = 0; list[i] != searchnum; i++) ; return (i < n) ? i : -1; }
start = clock(); for (j = 0; j < runtimes[i]; j++) pos = seqsearch(list, -1, sizelist[i]); stop = clock(); total = 1000.0 * (stop - start) / CLOCKS_PER_SEC; ort = 1000.0 * total / runtimes[i]; /* mikro sn */ list[sizelist[i]] = sizelist[i]; /* bekciyi sil */
- Bekçi (sentinel): aranan değer
list[n]’e konur; döngü hep durur, her turdai < ntestine gerek kalmaz. - Dizide n + 1 yer olmalı (MAX_SIZE = 5001).
- −1 aranır: hiç bulunmaz → en kötü durum ölçülür.
- Her n için arama
runtimes[i]kez (500000 … 5000) tekrarlanır.
Performans Ölçümü · Kodu adım adım çalıştır
Kod İzleme: seqsearch({4, 8, 2}, 5, 3)
Dizide 3 öğe var, ama list[3] için yer ayrılmış. 5 aranıyor.
Bekçi: list[3] = 5. Artık döngü en geç burada duracak.
list[0] = 4 ≠ 5 → i++. (Tek test: sınır denetimi yok.)
list[1] = 8 ≠ 5 → i++. (Tek test: sınır denetimi yok.)
list[2] = 2 ≠ 5 → i++. (Tek test: sınır denetimi yok.)
list[3] = 5 → döngü durdu, ama bu bekçi.
i = 3 < n değil → return −1: bulunamadı. n + 1 = 4 karşılaştırma.
int seqsearch(int list[], int searchnum, int n) { int i; list[n] = searchnum; /* bekci */ for (i = 0; list[i] != searchnum; i++) ; return (i < n) ? i : -1; }
Performans Ölçümü
Tekrarlı Ölçüm: Ortalama Arama Süresi
Noktalar: ölçülen ortalamalar · kesikli: doğrusal eğilim
- Ortalamalar artık 0 değil ve n ile doğrusal büyüyor: O(n) beklentisiyle uyumlu.
- 3000’den sonraki dalgalanma: tekrar sayısı (runtimes) azaldıkça ölçüm yine kabalaşıyor.
- Ölçümün yapıldığı makinede saniyede 1000 vuruş: 1 vuruş = 1 ms.
Ödevler
Ödev Soruları (Özyineleme)
- n! değerini bulan fonksiyonu özyinelemeli olarak yazınız.
- Fibonacci serisi f₀ = 0, f₁ = 1, fᵢ = fᵢ₋₁ + fᵢ₋₂ (i > 1) ise fᵢ’yi bulan özyinelemeli yordamı yazınız.
- C(n, m) = C(n−1, m) + C(n−1, m−1) verildiğine göre C(n, m) = n! / (m!·(n−m)!) değerini hesaplayan özyinelemeli yordamı yazınız.
İpuçları
- Taban: n ≤ 1 → 1. Adım: n · fakt(n−1).
- Taban: n ≤ 1 → n. Adım: fibo(n−1) + fibo(n−2).
- Taban: m == 0 ya da m == n → 1.
Ödevler
Ödev Cevapları ve Sık Hatalar
long fakt(long n) { if (n <= 1) return 1; return n * fakt(n - 1); } long fibo(long n) { if (n <= 1) return n; /* f0 = 0, f1 = 1 */ return fibo(n - 1) + fibo(n - 2); } long komb(int n, int m) { if (m == 0 || m == n) return 1; return komb(n - 1, m) + komb(n - 1, m - 1); }
Sık yapılan hata: fibo(deger-1) * fibo(deger-2) yazmak; çarpım her zaman 0 ya da 1 verir. Tanım gereği toplam olmalı.
Sık yapılan hata: long komb() parametresiz yazmak; n ve m parametre olmalı.
fakt O(n); fibo ve komb bu hâliyle üstel: aynı alt problemler tekrar tekrar çözülür.
Özet
Karmaşıklık Özeti
| Yordam | Zaman | Ek bellek | Not |
|---|---|---|---|
sort (seçimli sıralama) | Θ(n²) | O(1) | n(n−1)/2 karşılaştırma, ≤ n−1 takas |
sum | Θ(n) | O(1) | 2n + 3 adım |
rsum | Θ(n) | O(n) | 2n + 2 adım, n+1 çerçeve |
add (matris toplama) | Θ(rows·cols) | O(1) | kare matriste Θ(n²) |
seqsearch | O(n) | O(1) | en iyi Θ(1), en kötü n + 1 |
sihirli (Coxeter) | Θ(n²) | O(1) | kaba kuvvet: (n²)! |
| Dinamik dizi ekleme | O(1) amortize | O(n) | tek ekleme en kötü O(n) |
fibo (özyinelemeli) | üstel | O(n) | yığın derinliği n |
Özet
Sık Yapılan Hatalar
clock() ile ölçmek: sonuç 0 ya da vuruş katı çıkar.printf bırakmak: G/Ç süresi algoritmayı gölgeler.SWAP(a[i++], …).Özet
Alıştırmalar
for (i = 0; i < n; i++) for (j = 0; j < i; j++) x++;parçasındax++kaç kez işletilir? O(?)- 10n² + 100n + 1000 = O(n²) olduğunu c ve n₀ vererek gösteriniz.
- Seçimli sıralama zaten sıralı bir dizide kaç karşılaştırma yapar? En iyi durumu var mı?
- O(n log n) bir algoritma n = 1000’de 10 ms sürüyorsa n = 10⁶’da yaklaşık ne kadar sürer?
rsum’u n = 10⁶ ile çağırmak neden tehlikelidir?- 7×7 sihirli karede ilk satırı Coxeter kuralıyla yazınız (toplam kaç olmalı?).
Cevapları / ipuçlarını göster
- n(n−1)/2 → Θ(n²).
- n ≥ 1 için 10n² + 100n + 1000 ≤ 1110n²: c = 1110, n₀ = 1 (başka seçimler de olur).
- Yine n(n−1)/2; karşılaştırma sayısı girdiden bağımsızdır (yalnız takas sayısı değişir).
- Oran (10⁶·log 10⁶)/(10³·log 10³) = 2000 → ≈ 20 sn.
- 10⁶ çerçeve yığını taşırabilir (stack overflow); döngülü sum güvenlidir.
- Toplam 7·50/2 = 175; ilk satır: 28 19 10 1 48 39 30.
Özet
Bölüm Özeti
Gereksinim → çözümleme → tasarım → kodlama → doğrulama. Gerçeklemeyi ertelemek daha iyi seçim sağlar.
ADT = belirtim ⟂ gerçekleme. C’de: başlık dosyası + opak struct.
Bellek: c + SP(I). Zaman: program adımları (sum 2n+3, rsum 2n+2).
O üst, Ω alt, Θ sıkı sınır. En kötü durum garantidir; amortize analiz dizinin toplamına bakar.
Küçük n yanıltır; 2ⁿ ve n! kısa sürede umutsuzlaşır. Algoritma donanımdan önce gelir.
clock() + tekrar + ortalama; G/Ç ölçüm dışında.
Özet
Kaynaklar
- Horowitz, Sahni, Anderson-Freed, Fundamentals of Data Structures in C, 2. baskı, Bölüm 1.
- Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms, 3. baskı: Bölüm 2–3 (analiz, asimptotik), Bölüm 17 (amortize).
- Kernighan, Ritchie, The C Programming Language, 2. baskı.