Neden Birden Fazla Gerçekleme?
Stack bir soyut veri tipidir (Abstract Data Type — ADT). Push, pop, peek gibi operasyonları tanımlar ama bu operasyonların nasıl gerçekleneceğini dikte etmez. İki farklı ekip biri dizi ile, biri bağlı liste ile bir stack yazabilir; dışarıdan kullanan kod hiçbir fark görmez. Her ikisi de aynı davranışı (LIFO) sergiler.
O halde neden tek bir gerçeklemeyle yetinmeyiz? Çünkü her yaklaşım farklı bir trade-off dengesi sunar. Dizi tabanlı stack sabit bellekte, ardışık erişimde ve basitlikte öne çıkar. Bağlı liste tabanlı stack esneklik, sınırsız kapasite ve parçalı bellek kullanımında avantaj sağlar. Hangi gerçeklemenin seçileceği projenin gereksinimlerine bağlıdır.
Bu bölümde her iki yaklaşımı detaylı olarak inceleyecek, tam çalışan C kodlarını gösterecek, bellek düzenlerini görselleştirecek ve avantaj-dezavantajlarını somut örneklerle karşılaştıracağız.
Aynı ADT — Farklı Gerçeklemeler
Stack ADT (Arayüz)
push(x)
pop() → x
peek() → x
isEmpty() → bool
size() → int
Aynı operasyonlar, aynı sonuçlar — farklı bellek düzeni
Dizi Tabanlı Stack (Array-Based)
Dizi tabanlı stack, sabit veya dinamik boyutlu bir dizinin üzerine stack davranışını inşa eder. Bir top değişkeni tepedeki elemanın indeksini tutar. Boş stack'te top = -1'dir; push ile top artar, pop ile azalır. İndeks 0 stack'in tabanı, indeks top ise stack'in tepesidir.
Bellek Düzeni
Dizi tabanlı stack'te elemanlar bitişik (contiguous) bellek adreslerinde tutulur. Bu bitişiklik, modern işlemcilerin cache mekanizmasıyla son derece uyumludur ve stack'in hızlı olmasının en büyük sebebidir.
Dizi Tabanlı Stack — Bellekteki Görünüm
items dizisi (bitişik bellek adresleri):
Adres farkı: items[i] = base + i × sizeof(int) → her eleman ardışık 4 byte
Bitişik bellek = CPU cache ile mükemmel uyum
Cache locality neden önemli? CPU belleğe doğrudan erişmez; RAM'den verileri "cache line" adı verilen 64-byte'lık bloklar halinde çeker. Dizi tabanlı stack'te elemanlar ardışık adreslerde durduğu için bir elemana eriştiğinizde komşu elemanlar da cache'e yüklenir (cache hit). Linked list'te düğümler belleğin farklı bölgelerine dağılmış olduğundan her erişim cache miss yaratma riski taşır. Pratikte bu fark, aynı algoritmik karmaşıklığa rağmen 5-10× performans farkı yaratabilir.
Sabit Boyutlu Dizi Gerçeklemesi
En temel form: kapasite derleme zamanında #define MAX ile belirlenir. Basittir, hızlıdır, ancak kapasite önceden bilinmelidir. Dizinin tamamı stack frame'de (lokal değişken olarak) yer alır, malloc gerekmez.
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
typedef struct {
int items[MAX]; // sabit boyutlu dizi
int top; // tepedeki elemanın indeksi
} Stack;
/* ── Temel Operasyonlar ── */
void stackOlustur(Stack* s) { s->top = -1; }
int isEmpty(Stack* s) { return s->top == -1; }
int isFull(Stack* s) { return s->top == MAX - 1; }
int size(Stack* s) { return s->top + 1; }
void push(Stack* s, int val) {
if (isFull(s)) {
fprintf(stderr, "Stack Overflow!\n");
return;
}
s->items[++s->top] = val;
}
int pop(Stack* s) {
if (isEmpty(s)) {
fprintf(stderr, "Stack Underflow!\n");
return -1;
}
return s->items[s->top--];
}
int peek(Stack* s) {
if (isEmpty(s)) {
fprintf(stderr, "Stack boş!\n");
return -1;
}
return s->items[s->top];
}
void stackYazdir(Stack* s) {
printf("[");
for (int i = s->top; i >= 0; i--)
printf("%d%s", s->items[i], i > 0 ? ", " : "");
printf("] (top→bottom, boyut=%d)\n", size(s));
}Dinamik Dizi Gerçeklemesi (realloc)
Sabit boyutun sınırlamalarını aşmak için heap'te dinamik bir dizi ayrılır ve gerektiğinde realloc ile büyütülür. Kapasite her seferinde 2 katına çıkarılır (doubling strategy). Opsiyonel olarak, eleman sayısı kapasitenin ¼'ünün altına düştüğünde kapasite yarıya indirilebilir (shrink). Eşiğin ½ değil ¼ olmasının sebebi: ½ olursa tam sınırda push-pop döngüsünde sürekli grow-shrink tetiklenir (thrashing) ve amortize avantajı yok olur.
#define BASLANGIC_KAP 4
typedef struct {
int* items; // heap'te dinamik dizi
int top;
int kapasite;
} DStack;
void dstackOlustur(DStack* s) {
s->items = (int*)malloc(BASLANGIC_KAP * sizeof(int));
if (!s->items) { perror("malloc"); exit(1); }
s->top = -1;
s->kapasite = BASLANGIC_KAP;
}
/* ── Büyütme ── */
static void buyut(DStack* s) {
int yeniKap = s->kapasite * 2;
int* yeni = (int*)realloc(s->items, yeniKap * sizeof(int));
if (!yeni) { perror("realloc"); exit(1); }
s->items = yeni;
s->kapasite = yeniKap;
}
/* ── Küçültme (opsiyonel) ── */
static void kucult(DStack* s) {
if (s->kapasite <= BASLANGIC_KAP) return;
if (s->top + 1 > s->kapasite / 4) return;
s->kapasite /= 2;
s->items = (int*)realloc(s->items, s->kapasite * sizeof(int));
}
void dpush(DStack* s, int val) {
if (s->top == s->kapasite - 1) buyut(s);
s->items[++s->top] = val;
}
int dpop(DStack* s) {
if (s->top == -1) { fprintf(stderr, "Underflow!\n"); return -1; }
int val = s->items[s->top--];
kucult(s);
return val;
}
int dpeek(DStack* s) {
if (s->top == -1) { fprintf(stderr, "Boş!\n"); return -1; }
return s->items[s->top];
}
void dstackSerbest(DStack* s) {
free(s->items);
s->items = NULL; s->top = -1; s->kapasite = 0;
}Dinamik Büyüme Görselleştirmesi
realloc ile Kapasite Genişleme — 4 → 8 → 16
① Kapasite = 4, dolu → push(50) geldi
top == kapasite-1 → buyut() tetiklenir
↓ realloc: 4 × 2 = 8 ↓ tüm elemanlar kopyalandı
② Kapasite = 8, push(50) eklendi
Sonraki 3 push realloc gerektirmez
↓ push(60), push(70), push(80) → dolu → realloc: 8 × 2 = 16 ↓
③ Kapasite = 16, push(90) eklendi
+ 7 boş hücre (gösterilmedi) | Sonraki 7 push realloc gerektirmez
Amortized O(1) ispatı: n push sonunda toplam kopyalama maliyeti n + n/2 + n/4 + ... ≈ 2n'dir. n push + 2n kopyalama = 3n → push başına ortalama maliyet 3 = O(1). Kapasite her seferinde 2× büyüdüğü için realloc giderek daha seyrek tetiklenir: 10 milyon elemanlık bir stack'te sadece ~23 kez realloc yapılır (log₂(10M) ≈ 23).
Güvenli realloc kullanımı: Yukarıdaki kodda realloc sonucunu önce geçici bir pointer'a (yeni) atadık. Eğer doğrudan s->items = realloc(s->items, ...) yazarsanız ve realloc başarısız olursa NULL atanır — hem orijinal bellek adresi kaybolur hem de programınız çöker. Geçici pointer kullanmak bu riski ortadan kaldırır.
Dizi Tabanlı Stack — Avantajlar ve Dezavantajlar
✓ Avantajlar
- Cache locality: Elemanlar ardışık bellekte → CPU cache ile mükemmel uyum → 5-10× daha hızlı
- Basit implementasyon: Pointer yönetimi yok, sadece bir dizi ve bir indeks
- O(1) random access: İndeks ile herhangi bir elemana doğrudan erişim (debug amaçlı)
- Düşük bellek overhead: Eleman başına yalnızca sizeof(int) yer kaplar, ekstra pointer yok
- malloc/free yok (sabit dizide): Stack bölgesinde ayrılır, heap yönetimi gerekmez
- Öngörülebilir performans: Her push/pop garanti O(1) — sabit dizide en kötü durum bile sabit
✗ Dezavantajlar
- Sabit kapasite (static): MAX derleme zamanında belirlenir, çalışma zamanında değiştirilemez
- Bellek israfı: MAX=1000 ama 10 eleman → 990 hücre boş, bellekte yer kaplar
- Overflow riski: Push sayısı MAX'ı aşarsa veri kaybı veya çökme
- realloc maliyeti (dinamik): Büyütme sırasında tüm elemanlar kopyalanır → tek push O(n)
- Shrink yapılmazsa şişer: Pop sonrası kapasite küçülmezse bellek israfı artar
- Büyük MAX ≈ stack taşması:
int items[1000000] program stack frame'ini taşırabilir
Bellek İsrafı Sorunu — MAX=8 ama sadece 3 eleman
5 hücre boş = 5 × 4 byte = 20 byte israf
Dinamik dizide kapasite = 4 olurdu (en fazla 1 boş hücre)
Sabit Dizi vs Dinamik Dizi
| Özellik | Sabit Dizi | Dinamik Dizi |
|---|
| Kapasite | Derleme zamanında sabit | Çalışma zamanında büyür/küçülür |
| Push | O(1) her zaman | O(1) amortized |
| Overflow | Evet (MAX aşılırsa) | Çok nadir (bellek biterse) |
| Bellek bölgesi | Stack (local variable) | Heap (malloc/realloc) |
| Bellek israfı | Yüksek olabilir | Düşük (2× katsayısı) |
free gerekli mi? | Hayır (otomatik) | Evet (dstackSerbest) |
| Tercih | Boyut biliniyorsa, basitlik önemliyse | Boyut bilinmiyorsa, esneklik gerekirse |
Generic (void*) Dizi Stack
Sadece int değil, herhangi bir veri tipi saklamak istiyorsanız void* dizisi kullanabilirsiniz. Bu yaklaşım stack'i veri tipinden bağımsız hale getirir — aynı fonksiyonlarla int, float, string veya struct saklayabilirsiniz.
typedef struct {
void** items; // void* dizisi: her slot bir pointer tutar
int top;
int kapasite;
} GenericStack;
void gsOlustur(GenericStack* s, int kap) {
s->items = (void**)malloc(kap * sizeof(void*));
s->top = -1;
s->kapasite = kap;
}
void gsPush(GenericStack* s, void* veri) {
if (s->top == s->kapasite - 1) {
s->kapasite *= 2;
s->items = (void**)realloc(s->items,
s->kapasite * sizeof(void*));
}
s->items[++s->top] = veri;
}
void* gsPop(GenericStack* s) {
if (s->top == -1) return NULL;
return s->items[s->top--];
}
// Kullanım:
GenericStack gs;
gsOlustur(&gs, 4);
int a = 42; float b = 3.14; char* c = "Merhaba";
gsPush(&gs, &a); // int pointer push
gsPush(&gs, &b); // float pointer push
gsPush(&gs, c); // string pointer push
char* str = (char*)gsPop(&gs); // "Merhaba"
float* fp = (float*)gsPop(&gs); // → 3.14void* dikkat noktası: Type safety (tip güvenliği) yoktur. Yanlış cast yaparsanız compiler uyarmaz; çalışma zamanında crash veya yanlış veri elde edersiniz. C++'ta template, Java'da generics ile bu sorun dil düzeyinde çözülür. C'de ise disiplinli kullanım veya her tip için ayrı stack yazmak gerekir.
Push ve Pop — Adım Adım Bellek Takibi
Dizi tabanlı stack'te push ve pop işlemlerinin bellekteki etkisini adım adım gözlemleyelim. Anahtar detay: pop edilen eleman bellekten fiziksel olarak silinmez, sadece top indeksi geri çekilir. O hücre artık "stack dışı" kabul edilir ve bir sonraki push üzerine yazar.
push(10), push(20), push(30), pop(), pop() — Bellek Takibi
⑤ pop() → 30 döndü, top: 2 → 1
⑥ pop() → 20 döndü, top: 1 → 0
Gri hücreler: bellekte duruyor ama stack dışında (erişilemez)
Tam Çalışan Demo — Sabit ve Dinamik
#include <stdio.h>
#include <stdlib.h>
/* ═══ Sabit Dizi Stack ═══ */
#define MAX 5
typedef struct {
int items[MAX];
int top;
} Stack;
void stackOlustur(Stack* s) { s->top = -1; }
int isEmpty(Stack* s) { return s->top == -1; }
int isFull(Stack* s) { return s->top == MAX - 1; }
void push(Stack* s, int val) {
if (isFull(s)) { printf(" ⚠ Overflow!\n"); return; }
s->items[++s->top] = val;
printf(" push(%d) → top=%d\n", val, s->top);
}
int pop(Stack* s) {
if (isEmpty(s)) { printf(" ⚠ Underflow!\n"); return -1; }
int val = s->items[s->top--];
printf(" pop() → %d, top=%d\n", val, s->top);
return val;
}
/* ═══ Dinamik Dizi Stack ═══ */
typedef struct {
int* items;
int top, kapasite;
} DStack;
void dOlustur(DStack* s, int kap) {
s->items = (int*)malloc(kap * sizeof(int));
s->top = -1; s->kapasite = kap;
}
void dPush(DStack* s, int val) {
if (s->top == s->kapasite - 1) {
s->kapasite *= 2;
s->items = (int*)realloc(s->items,
s->kapasite * sizeof(int));
printf(" ↑ kapasite → %d\n", s->kapasite);
}
s->items[++s->top] = val;
printf(" dPush(%d) → top=%d\n", val, s->top);
}
int main() {
/* ── Sabit dizi demo ── */
printf("=== Sabit Dizi Stack (MAX=%d) ===\n", MAX);
Stack s;
stackOlustur(&s);
for (int i = 1; i <= 6; i++)
push(&s, i * 10); // 6. → overflow
printf("\n");
while (!isEmpty(&s)) pop(&s);
pop(&s); // underflow
/* ── Dinamik dizi demo ── */
printf("\n=== Dinamik Dizi Stack (başlangıç=2) ===\n");
DStack ds;
dOlustur(&ds, 2);
for (int i = 1; i <= 10; i++)
dPush(&ds, i * 10);
printf(" Son kapasite: %d\n", ds.kapasite);
free(ds.items);
return 0;
}=== Sabit Dizi Stack (MAX=5) ===
push(10) → top=0
push(20) → top=1
push(30) → top=2
push(40) → top=3
push(50) → top=4
⚠ Overflow!
pop() → 50, top=3
pop() → 40, top=2
pop() → 30, top=1
pop() → 20, top=0
pop() → 10, top=-1
⚠ Underflow!
=== Dinamik Dizi Stack (başlangıç=2) ===
dPush(10) → top=0
dPush(20) → top=1
↑ kapasite → 4
dPush(30) → top=2
dPush(40) → top=3
↑ kapasite → 8
dPush(50) → top=4
dPush(60) → top=5
dPush(70) → top=6
dPush(80) → top=7
↑ kapasite → 16
dPush(90) → top=8
dPush(100) → top=9
Son kapasite: 16
Dikkat edin: Sabit dizide 6. push reddedildi (overflow). Dinamik dizide ise 10 push sorunsuz yapıldı — kapasite otomatik olarak 2 → 4 → 8 → 16 şeklinde büyüdü. 10 eleman için sadece 3 realloc gerçekleşti (log₂(10) ≈ 3.3).
Dizi Tabanlı Stack Ne Zaman Tercih Edilmeli?
Dizi tabanlı stack şu durumlarda ilk tercihtir: eleman sayısı makul bir üst sınırla tahmin edilebiliyorsa, performansın kritik olduğu sistemlerde (gömülü sistemler, oyun motorları, işletim sistemi çekirdekleri), cache-friendly erişimin önemli olduğu yoğun döngülerde ve basit, debug edilmesi kolay bir yapı istendiğinde. Standart kütüphanelerdeki stack implementasyonları (C++ std::stack, Java java.util.Stack, Python'un list) hepsi varsayılan olarak dinamik dizi tabanlıdır — bu tesadüf değildir.