Bölüm 6: Yığınlar
Giriş
Öğrenme Çıktıları
Yığını S = {a0, …, an−1} listesi olarak tanımlayıp alt ve üst öğeyi göstermek.
top indisiyle push/pop yazmak; taşma (overflow) ve alt taşmayı (underflow) denetlemek.
Düğüm ayırıp (malloc) bağlamak, çıkarılan düğümü free ile bırakmak.
Fonksiyon çağrılarının bir yığın gibi çalıştığını ve çağrı yığınının neden taştığını açıklamak.
Sabit dizi, büyüyen dizi ve bağlı liste arasında maliyete göre seçim yapmak.
Tek bir bellek dizisinde n yığını top[i] ve sinir[i] ile paylaştırmak.
Yığın ADT
Yığın (Stack): Son Giren İlk Çıkar
Yığın, ekleme (insertion) ve silmenin (deletion) yalnızca bir uçtan yapıldığı sıralı bir listedir: S = {a0, a1, …, an−1} için a0 alt, an−1 üst (top) öğedir. Son eklenen ilk çıkar: LIFO (last-in-first-out).
Tek arabalık çıkmaz sokağa park eden araçlar: son giren (D) ilk çıkmak zorunda.
Türkçe kaynaklarda stack için yığın ve yığıt ikisi de kullanılır; bu derste stack için “yığın”, heap veri yapısı için “heap” kullanılır.
Yığın ADT
Yığının Büyümesi ve Küçülmesi
A eklendi → üst öğe artık A; top = 0.
B eklendi → üst öğe artık B; top = 1.
C eklendi → üst öğe artık C; top = 2.
D eklendi → üst öğe artık D; top = 3.
E eklendi → üst öğe artık E; top = 4.
Silme yalnız üstten: ilk alınan öğe en son eklenen E olur. Üst öğe yeniden D, top = 3.
- Ekleme sırası: A, B, C, D, E.
- Silme sırası (tümü çıkarılsaydı): E, D, C, B, A: tam ters.
- Ortada bir öğeye (ör. B) erişmek için üstündekilerin hepsi alınmalı.
- Bu kısıt bir kusur değil, bir sözleşmedir: “en son açılan iş ilk kapanır”.
Yığın ADT
Soyut Veri Tipi: Yığın
| İşlem | Anlamı | Ön koşul / hata |
|---|---|---|
yigit_olustur(max) | En çok max öğe alan boş yığın | — |
yigit_dolu(S) | Öğe sayısı max ise doğru | — |
yigit_ekle(S, x) · push | x'i üste koyar | Dolu ise taşma (overflow) |
yigit_bos(S) | Öğe yoksa doğru | — |
yigit_al(S) · pop | Üstteki öğeyi çıkarıp döndürür | Boş ise alt taşma (underflow) |
yigit_ust(S) · peek | Üstteki öğeyi çıkarmadan okur | Boş ise hata |
ADT ne yapıldığını söyler, nasıl yapıldığını söylemez. Aynı arayüz sabit dizi, büyüyen dizi ya da bağlı listeyle gerçeklenebilir (Horowitz–Sahni, ADT 3.1).
Yığın ADT
Yığının Kullanıldığı Yerler
Her fonksiyon çağrısı dönüş adresi ve yerel değişkenleriyle bir çerçeve iter.
Derleyiciler infix → postfix çevirisi ve postfix hesaplamada yığın kullanır.
Labirentte çıkmaza girilince son yol ayrımına dönülür.
Her kapanan, en son açılanla eşleşmeli.
Düzenleyici her işlemi yığına koyar; Ctrl+Z en sonuncuyu geri alır.
DFS, ziyaret edilecek düğümleri yığında tutar.
Yığın ADT
push ve pop Yalnız Üstte
Dört kez push, ardından pop: çıkan sıra girenin tersidir. Boş yığında pop alt taşmadır.
Dizi ile Yığın
Dizi ile Yığın: top İndisi
Boş yığında top = −1. Bu hâlde öğe almak alt taşma (underflow) hatasıdır.
Bu hâlde öğe eklemek taşma (overflow) hatasıdır.
İkisini karıştırmak klasik “bir eksik/bir fazla” hatasıdır.
C dizi sınırını denetlemez: denetimsiz bir push komşu belleğin üzerine yazar.
Dizi ile Yığın
Dizi ile Yığın: C Tanımları
#include <stdio.h> #include <stdlib.h> #define MAX_STACK_SIZE 101 typedef struct { int deger; /* diğer alanlar */ } element; element yigit[MAX_STACK_SIZE]; void yigit_dolu(void) { printf("Yigit dolu!!!\n"); exit(-1); } void yigit_bos(void) { printf("Yigit bos!!!\n"); exit(-2); }
element: yığında saklanacak kayıt; şimdilik tek alandeger.- Yığın küresel (global) bir dizi;
topise çağırandan adresle gelir. - Hata yordamları mesaj basıp programı
exitile sonlandırır.
Dizi ile Yığın
Ekleme ve Silme Yordamları
void yigit_ekle(int *top, element item) { if (*top >= MAX_STACK_SIZE - 1) /* yer var mı? */ yigit_dolu(); /* uyar ve çık */ yigit[++*top] = item; /* önce işaretçiyi artır */ } /* sonra öğeyi yerleştir */ element yigit_al(int *top) { if (*top < 0) yigit_bos(); /* boşsa uyar ve çık */ return yigit[(*top)--]; /* döndür, sonra azalt */ }
yigit[++*top]Önce *top bir artar, sonra yeni indise yazılır: boş yığında −1 → 0.
yigit[(*top)--]Önce eski indisteki öğe okunur, sonra *top azalır. Parantez şart: *top-- işaretçinin kendisini azaltırdı.
Dizi ile Yığın
C'de Öncelik Tuzağı: * ile ++/--
| İfade | C bunu nasıl okur | Sonuç |
|---|---|---|
++*top | ++(*top): gösterilen tamsayıyı artır | Doğru (push) |
(*top)-- | gösterilen tamsayıyı kullan, sonra azalt | Doğru (pop) |
*top-- | *(top--): işaretçiyi azalt, eski adresi oku | Hatalı: top değişmez |
*top++ | *(top++): işaretçiyi ilerlet | Hatalı |
Son ek ++/--, tekli *'dan daha önceliklidir; ön ek ++ ile * aynı düzeydedir ve sağdan sola bağlanır.
top çağıranın değişkenidir; yordam onu değiştirebilsin diye &top geçirilir (C'de değerle çağrı vardır).
Dizi ile Yığın · Kodu adım adım çalıştır
Kod İzleme: Dizi ile Yığın
main'de top = −1 (boş). Sırayla ekle A, B, C; al; ekle D, E, F. MAX_STACK_SIZE = 4.
-1 ≥ 3? Hayır, yer var.
++*top → top = 0, sonra yigit[0] = A.
0 ≥ 3? Hayır, yer var.
++*top → top = 1, sonra yigit[1] = B.
1 ≥ 3? Hayır, yer var.
++*top → top = 2, sonra yigit[2] = C.
2 < 0? Hayır, yığın boş değil.
yigit[2] = C döndürülür, ardından top = 1. Hücrede eski değer durur ama artık yığının parçası değildir.
1 ≥ 3? Hayır, yer var.
++*top → top = 2, sonra yigit[2] = D.
2 ≥ 3? Hayır, yer var.
++*top → top = 3, sonra yigit[3] = E.
3 ≥ 3? Evet: yer yok!
yigit_dolu() mesajı basar ve exit(-1) ile tüm programı bitirir. Kütüphane kodunda bu kaba bir tepkidir; bool döndüren sürüm kararı çağırana bırakır.
void yigit_ekle(int *top, element item) { if (*top >= MAX_STACK_SIZE - 1) yigit_dolu(); /* "Yigit dolu!!!", exit */ yigit[++*top] = item; } element yigit_al(int *top) { if (*top < 0) yigit_bos(); /* "Yigit bos!!!", exit */ return yigit[(*top)--]; }
Dizi ile Yığın
Daha İyi Tasarım: Yığın Bir Nesne Gibi
#include <stdbool.h> #define KAP 100 typedef struct { int veri[KAP]; int ust; /* boşken -1 */ } Yigit; void yigit_baslat(Yigit *y) { y->ust = -1; } bool yigit_bos(const Yigit *y) { return y->ust < 0; } bool yigit_dolu(const Yigit *y) { return y->ust == KAP - 1; } int yigit_boyut(const Yigit *y) { return y->ust + 1; } bool yigit_ust(const Yigit *y, int *x) { if (yigit_bos(y)) return false; *x = y->veri[y->ust]; return true; }
- Yığın ve
ustaynı yapıda: iki yığın aynı programda karışmadan yaşar. - Küresel değişken yok; her yordam yığını parametre olarak alır.
- Hata durumunda
exityerinefalsedöner; kararı çağıran verir.
Dizi ile Yığın · Kodu adım adım çalıştır
Kod İzleme: bool Döndüren push / pop
yigit_baslat(&y) sonrası ust = −1. Sırayla push 5, 8, 2, 9; sonra dört kez pop. KAP = 3.
-1 == 2? Hayır, yer var.
++y->ust → 0; veri[0] = 5; true döner.
0 == 2? Hayır, yer var.
++y->ust → 1; veri[1] = 8; true döner.
1 == 2? Hayır, yer var.
++y->ust → 2; veri[2] = 2; true döner.
ust == KAP − 1 (2 == 2) → return false. Program sürer; çağıran taşmayı öğrenir.
Boş değil; *x = veri[2] = 2, sonra ust = 1.
Boş değil; *x = veri[1] = 8, sonra ust = 0.
Boş değil; *x = veri[0] = 5, sonra ust = -1.
ust < 0 → return false: alt taşma, *x'e dokunulmadı.
bool push(Yigit *y, int x) { if (y->ust == KAP - 1) return false; /* taşma */ y->veri[++y->ust] = x; return true; } bool pop(Yigit *y, int *x) { if (y->ust < 0) return false; /* alt taşma */ *x = y->veri[y->ust--]; return true; }
Dizi ile Yığın
Bileşen Gibi Yaz: yigit.h ve yigit.c
/* yigit.h: kullanıcı yalnız bunu görür */ #ifndef YIGIT_H #define YIGIT_H #include <stdbool.h> typedef struct Yigit Yigit; /* opak */ Yigit *yigit_olustur(int kapasite); void yigit_yok_et(Yigit *y); bool yigit_ekle(Yigit *y, int x); bool yigit_al(Yigit *y, int *x); bool yigit_bos(const Yigit *y); #endif
Kullanan kod struct'ın içini göremez; yalnız işlemleri çağırır. Gerçekleme diziden bağlı listeye geçse bile kullanan kod değişmez.
/* yigit.c: gerçekleme */ struct Yigit { int *veri; int ust, kap; }; Yigit *yigit_olustur(int kap) { Yigit *y = malloc(sizeof *y); if (y == NULL) return NULL; y->veri = malloc(kap * sizeof *y->veri); if (y->veri == NULL) { free(y); return NULL; } y->ust = -1; y->kap = kap; return y; } void yigit_yok_et(Yigit *y) { if (y) { free(y->veri); free(y); } }
Dizi ile Yığın · Kodu adım adım çalıştır
Kod İzleme: Yığınla Dizge Ters Çevirme
Girdi "YIGIT"; boş yığın kuruldu.
s[0] = 'Y' ≠ '\0' → push('Y').
s[1] = 'I' ≠ '\0' → push('I').
s[2] = 'G' ≠ '\0' → push('G').
s[3] = 'I' ≠ '\0' → push('I').
s[4] = 'T' ≠ '\0' → push('T').
pop → 'T', s[0] = 'T'.
pop → 'I', s[1] = 'I'.
pop → 'G', s[2] = 'G'.
pop → 'I', s[3] = 'I'.
pop → 'Y', s[4] = 'Y'.
Yığın boş: pop false döner, döngü biter. Sonuç "TIGIY": n push + n pop → O(n).
void ters_cevir(char *s) { Yigit y; int c, i; yigit_baslat(&y); for (i = 0; s[i] != '\0'; i++) push(&y, s[i]); /* KAP yeterli varsayıldı */ for (i = 0; pop(&y, &c); i++) s[i] = (char)c; }
Bağlı Yığın
Bağlı Liste Kullanarak Yığın
A, B, C sırasıyla eklendi: top liste başını gösterir.
- Her öğe bir düğüm (node):
deger+ sonraki düğümebag. - Ekleme ve silme liste başında: ikisi de O(1), kaydırma yok.
- Kapasite sınırı yok: yalnız
mallocbaşarısız olursa taşma olur.
Bağlı Yığın · Kodu adım adım çalıştır
Kod İzleme: Bağlı push
Başlangıç: dugum *top = NULL; (boş yığın). Sırayla push 10, 20, 30.
malloc yeni bir düğüm ayırır; NULL değilse devam. Alanları henüz belirsiz.
yeni->deger = 10.
yeni->bag = *top: yeni düğüm eski üstü (NULL) gösterir. Sıra önemli: önce bağla, sonra *top'u değiştir.
*top = yeni: çağıranın top'u artık 10'u gösterir. Yığın: 10.
malloc yeni bir düğüm ayırır; NULL değilse devam. Alanları henüz belirsiz.
yeni->deger = 20.
yeni->bag = *top: yeni düğüm eski üstü (düğüm(10)) gösterir. Sıra önemli: önce bağla, sonra *top'u değiştir.
*top = yeni: çağıranın top'u artık 20'u gösterir. Yığın: 20 → 10.
malloc yeni bir düğüm ayırır; NULL değilse devam. Alanları henüz belirsiz.
yeni->deger = 30.
yeni->bag = *top: yeni düğüm eski üstü (düğüm(20)) gösterir. Sıra önemli: önce bağla, sonra *top'u değiştir.
*top = yeni: çağıranın top'u artık 30'u gösterir. Yığın: 30 → 20 → 10.
Üç push sonrası: 30 → 20 → 10 → NULL. Her push sabit sayıda iş: O(1).
bool push(dugum **top, int x) { dugum *yeni = malloc(sizeof *yeni); if (yeni == NULL) return false; /* bellek yok */ yeni->deger = x; yeni->bag = *top; /* eski üstü göster */ *top = yeni; /* yeni üst */ return true; }
Bağlı Yığın · Kodu adım adım çalıştır
Kod İzleme: Bağlı pop
Yığın: 30 → 20 → 10. Dört kez pop çağrılacak; her başarılı sonucu main yazdırır.
eski = *top: yalnızca bir işaretçi kopyası; yeni bellek ayrılmaz.
*x = 30 (değer, düğüm silinmeden önce alınmalı).
*top = eski->bag → düğüm(20).
free(eski): 30 düğümünün belleği sisteme döner. main 30 yazar.
eski = *top: yalnızca bir işaretçi kopyası; yeni bellek ayrılmaz.
*x = 20 (değer, düğüm silinmeden önce alınmalı).
*top = eski->bag → düğüm(10).
free(eski): 20 düğümünün belleği sisteme döner. main 20 yazar.
eski = *top: yalnızca bir işaretçi kopyası; yeni bellek ayrılmaz.
*x = 10 (değer, düğüm silinmeden önce alınmalı).
*top = eski->bag → NULL.
free(eski): 10 düğümünün belleği sisteme döner. main 10 yazar.
eski = NULL → yığın boş, return false. Döngü burada durur.
bool pop(dugum **top, int *x) { dugum *eski = *top; /* malloc yok! */ if (eski == NULL) return false; /* boş */ *x = eski->deger; *top = eski->bag; /* üstü ilerlet */ free(eski); /* belleği bırak */ return true; }
Bağlı Yığın
Bağlı Yığında Bellek Sorumluluğu
typedef struct dugum { int deger; struct dugum *bag; } dugum; void yigit_temizle(dugum **top) { while (*top != NULL) { dugum *eski = *top; *top = eski->bag; free(eski); } } /* sonunda *top == NULL */
eskibir işaretçi değişkenidir, yeni düğüm değil: onamallocgerekmez.- Önce bağ kurtarılır (
*top = eski->bag), sonrafreeçağrılır: tersi serbest bırakılmış belleği okur. - Program bitmeden
yigit_temizleçağrılır: hermalloc'un birfree'si olmalı.
Bağlı Yığın
Dizi Tabanlı mı, Bağlı mı?
| Ölçüt | Sabit dizi | Büyüyen dizi | Bağlı liste |
|---|---|---|---|
| Kapasite | Derleme anında sabit | Gerektikçe ×2 | Bellek bitene kadar |
| push / pop | O(1) | O(1) amortize | O(1), her push'ta malloc |
| Öğe başına ek bellek | Yok | Yok (boş kapasite hariç) | Bir işaretçi (8 bayt) |
| Önbellek (cache) | Çok iyi | Çok iyi | Zayıf (düğümler dağınık) |
| Taşma | Mümkün | Yalnız realloc başarısızsa | Yalnız malloc başarısızsa |
| Hata riski | Sınır denetimi | realloc sonucu | free unutmak, sarkan işaretçi |
Üst sınır biliniyorsa sabit dizi; bilinmiyorsa büyüyen dizi pratikte en hızlısıdır. Bağlı yığın, düğümlerin zaten başka yapılarda yaşadığı ya da tek tek yaratılıp silindiği durumlar için uygundur.
Çağrı Yığını
Çağrı Yığını da Taşar
Çağrı yığını bir yığındır: her çağrı dönüş adresi, parametreler ve yerellerden oluşan bir çerçeveyi push eder, return onu pop eder; en son çağrılan fonksiyon ilk biter.
Taban durumu olmayan ya da ona ulaşmayan özyineleme her çağrıda çerçeve iter; sistem yığını (tipik olarak birkaç MB) dolunca program stack overflow ile çöker.
int a[1000000]; gibi yerel bir dizi çerçevenin içindedir; tek başına yığını taşırabilir. Büyük veriler için malloc ile heap bellek kullanılır.
return &yerel; hatalıdır: çerçeve pop edilince o bellek başka çağrılarca yeniden kullanılır (sarkan işaretçi).
Her özyinelemeli algoritma, kendi yığınını tutan bir döngüye çevrilebilir.
Çoklu Yığın
Çoklu Yığın: Önce İki Yığın
- Yığın 1 0'dan yukarı, yığın 2 m−1'den aşağı büyür.
- Boş:
top1 = −1,top2 = m. - Dolu:
top1 + 1 == top2: ikisi ortada buluşunca.
Çoklu Yığın · Kodu adım adım çalıştır
Kod İzleme: Tek Bellekte n Yığın
MEM = 12, n = 3: her yığına 4 hücre. Başta top[i] = sinir[i] = (m/n)·i − 1, sinir[n] = m − 1.
Yer var; bellek[0] = 5, top[0] = 0.
Yer var; bellek[4] = 7, top[1] = 4.
Yer var; bellek[1] = 9, top[0] = 1.
Yer var; bellek[8] = 4, top[2] = 8.
Yer var; bellek[2] = 2, top[0] = 2.
Yer var; bellek[3] = 8, top[0] = 3.
top[0] == sinir[1] (3 == 3) → yığın 0 dolu, oysa bellekte 6 boş hücre var! Horowitz'in stack_full'u burada yığınları kaydırıp yer açar.
top[1] ≠ sinir[1] → boş değil; *x = 7, top[1] = 3.
/* yığın i: bellek[sinir[i]+1 .. sinir[i+1]] */ bool ekle(int i, int x) { if (top[i] == sinir[i + 1]) return false; /* dolu */ bellek[++top[i]] = x; return true; } bool al(int i, int *x) { if (top[i] == sinir[i]) return false; /* boş */ *x = bellek[top[i]--]; return true; }
Çoklu Yığın
n Yığının Paylaşımı: Artılar ve Eksiler
Tek boyutlu bir dizi (memory[MEMORY_SIZE]) n kesime (segment) bölünerek n yığın kurulabilir; yığınların top ve sınır değerleri iki yardımcı dizide tutulur.
Artılar
- Tek
malloc, tek dizi: yönetimi kolay. - push/pop yine O(1).
- Aynı kuyruk fikri de uygulanabilir (n kuyruk).
Eksiler
- Bir kesim dolunca, bellekte yer olsa bile “dolu” hatası.
- Yer açmak için yığınları kaydırmak O(m): kaydırma pahalıdır.
- Kesim boyutlarını önceden kestirmek zor.
stack_full(i), önce sağdaki, yoksa soldaki boş yeri bulur ve arada kalan yığınları bir hücre kaydırır. En kötü durumda her ekleme O(m) olabilir.Özet
Hangi Çıkış Sırası Mümkün?
1, 2, 3, 4 bu sırayla push edilecek; aralara istenen sayıda pop konabilir. Aşağıdaki çıkış sıralarından hangileri elde edilebilir?
| Çıkış sırası | Mümkün mü? |
|---|---|
1 2 3 4 | gösterEvet: her push'tan hemen sonra pop. |
4 3 2 1 | gösterEvet: dört push, dört pop. |
2 4 3 1 | gösterEvet: push1 push2 pop(2) push3 push4 pop(4) pop(3) pop(1). |
3 1 4 2 | gösterHayır: 3 çıktığında 1 ve 2 yığındadır, 2 üsttedir; 1, 2'den önce çıkamaz. |
Genel kural: çıkışta bir x'ten sonra, x'ten önce itilmiş ve henüz çıkmamış öğeler ters sırada çıkmak zorundadır. n öğe için olası sıra sayısı Catalan sayısıdır (n = 4 için 14).
Özet
Karmaşıklık Özeti
| İşlem | Sabit dizi | Büyüyen dizi | Bağlı |
|---|---|---|---|
| push | O(1) | O(1) amortize, en kötü O(n) | O(1) |
| pop | O(1) | O(1) | O(1) |
| peek / bos / dolu | O(1) | O(1) | O(1) |
| n öğelik bellek | MAX (sabit) | ≤ 2n | n · (öğe + işaretçi) |
| Temizleme | O(1) | O(1) (tek free) | O(n) |
| fakt(n) çağrı yığını | — | — | O(n) çerçeve |
| Çoklu yığın ekle | O(1); yer açma O(m) | — | — |
Özet
Sık Yapılan Hatalar
*top-- yazmak: işaretçiyi azaltır; doğrusu (*top)--.top değerini öğe sayısıyla karıştırmak (sayı = top + 1).*top = yeni yazıp eski listeyi kaybetmek.free'den sonra okumak ya da düğümü hiç bırakmamak.malloc yapıp bellek sızdırmak.Özet
Alıştırmalar
- Dizi yığınında
yigit_ekle'yiexityerinebooldöndürecek biçimde yeniden yaz. - Tek dizide iki yığın için
ekle(k, x)veal(k)yordamlarını yaz (k = 1 ya da 2). - Bağlı yığını,
topile birlikteboyutalanı da tutan birstructiçine taşı. - Bir dizgeyi yığın kullanarak ters çeviren
void ters(char *s)yordamını yaz. - Yalnız push/pop kullanarak bir yığının en alttaki öğesini döndüren yordamı yaz (yardımcı yığın serbest).
fakt(5)çağrısında yığında en çok kaç çerçeve olur?maindahil çiz.
İpuçları / cevaplar
- Taşma varsa
return false;, yoksayigit[++*top] = item; return true;. - Boş: top1 = −1, top2 = m. Dolu: top1 + 1 == top2. k = 2 için ekleme
--top2, almatop2++. typedef struct { dugum *top; int boyut; } BYigit;; push'ta ++boyut, başarılı pop'ta --boyut.- Karakterleri sırayla push et, sonra pop ederek aynı diziye geri yaz: O(n) zaman, O(n) ek bellek.
- Tümünü yardımcı yığına aktar, sonuncuyu sakla, sonra geri aktar: O(n).
- fakt(5), fakt(4), …, fakt(1) + main = 6 çerçeve.
Özet
Bölüm Özeti
Ekleme ve silme yalnız üstte; son giren ilk çıkar.
top = −1 boş, top = MAX − 1 dolu; ++*top ve (*top)--.
exit yerine durum döndür; taşma/alt taşma çağıranın kararı.
top = liste başı; push/pop O(1); her çıkan düğüme free.
Her çağrı bir çerçeve: dönüş adresi, yereller, önceki çerçeve.
Tek bellek, top[i] ve sinir[i]; kesim dolarsa kaydırma gerekir.
Özet
Kaynaklar ve Devam
- E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, 2. bs., Bölüm 3.1–3.2, 3.7 ve 4.3.
- T. H. Cormen vd., Introduction to Algorithms, 3. bs., Bölüm 10.1 (yığınlar) ve 17 (amortize analiz).
- R. Sedgewick, Algorithms in C, Kısım 1–4, Bölüm 4.