Yığınlar - Stack Veri Yapısı

Stack, verilerin LIFO (Last In First Out) ilkesiyle yönetildiği soyut bir veri yapısıdır. Push, pop, peek gibi temel operasyonları dizi ve bağlı liste implementasyonlarıyla kapsar.

🗂️ Veri Yapıları 📅 21 February 2026 👁️ 13 görüntülenme

Stack Nedir?

Stack (yığın), elemanların yalnızca bir uçtan eklenip çıkarılabildiği ve "Son Giren İlk Çıkar" (LIFO — Last In First Out) prensibine göre çalışan soyut bir veri yapısıdır (Abstract Data Type — ADT). Soyut veri yapısı demek, stack'in ne yapılacağını tanımlayıp nasıl yapılacağını dikte etmemesi demektir. Push, pop, peek gibi operasyonlar arayüzü oluşturur; arka planda bir dizi veya bağlı liste kullanılması ise implementasyon tercihidir. Dışarıdan bakıldığında davranış tamamen aynıdır.

Stack'in en temel kuralı şudur: ekleme ve çıkarma işlemleri yalnızca tepeden (top) yapılabilir. Ortadan veya alttan eleman çekmek stack tanımına aykırıdır. Bu kısıtlama ilk bakışta dezavantaj gibi görünse de, aslında stack'i güçlü kılan tam da bu disiplindir. Fonksiyon çağrıları, geri alma (undo) sistemi, parantez dengesi kontrolü ve derinlik öncelikli arama gibi pek çok problem bu disipline doğal olarak uyar.

En bilindik gerçek hayat örneği üst üste dizilmiş tabaklardır: yeni tabak her zaman en üste konur ve kullanmak istediğinizde en üsttekini alırsınız. Alttaki tabağa ulaşmak için üsttekileri tek tek kaldırmanız gerekir. Başka bir örnek silah şarjörüdür: ilk doldurulan mermi en sona iner ve en son doldurulan mermi ilk ateşlenir.

Stack'in Yapısı — LIFO Prensibi
Stack Görünümü
← top (tepe)
10index 0 (bottom)
20index 1
30index 2
40index 3 ← top
bottom (taban)
Tabak Analojisi
🍽️ Tabak 1 (ilk konan)
🍽️ Tabak 2
🍽️ Tabak 3
🍽️ Tabak 4 ← al/koy
Ekleme (push) ve çıkarma (pop) yalnızca tepeden yapılır

LIFO vs FIFO

Stack'in LIFO (Last In, First Out) prensibini tam olarak kavramak için, karşıtı olan FIFO (First In, First Out) ile karşılaştırmak faydalıdır. FIFO, queue (kuyruk) veri yapısının prensibidir ve sıra mantığıyla çalışır: ilk gelen ilk çıkar. Stack bir tabak yığınıdır (son konan tabak ilk alınır), queue ise bir market kasası kuyruğudur (ilk gelen müşteri ilk çıkar).

LIFO (Stack)

Ekleme sırası: A, B, C, D
Çıkma sırası: D, C, B, A
Son giren ilk çıkar. Tabak yığını, geri alma, fonksiyon çağrıları.

FIFO (Queue)

Ekleme sırası: A, B, C, D
Çıkma sırası: A, B, C, D
İlk giren ilk çıkar. Kuyruk, yazıcı sırası, görev zamanlayıcı.

Temel Kavramlar ve Terminoloji

Top (Tepe)

Stack'in en üstündeki elemandır ve stack'te doğrudan erişilebilen tek elemandır. Push ile yeni eleman top'a eklenir, pop ile top'taki eleman çıkarılır, peek ile top okunur. Alttaki elemanlara doğrudan erişim yoktur; onlara ulaşmak için üsttekileri tek tek çıkarmak gerekir. Dizi tabanlı stack'te top bir indeks değişkenidir (0, 1, 2…). Linked list tabanlı stack'te ise top, listenin head pointer'ıdır.

Stack Operasyonları

OperasyonAçıklamaKarmaşıklık
push(x)x elemanını stack'in tepesine eklerO(1)
pop()Tepedeki elemanı çıkarır ve döndürürO(1)
peek() / top()Tepedeki elemanı döndürür ama çıkarmazO(1)
isEmpty()Stack boş mu? (true / false)O(1)
isFull()Stack dolu mu? (yalnızca dizi tabanlı)O(1)
size()Eleman sayısını döndürürO(1)
Her şey O(1): Stack'in tüm operasyonları sabit zamanda çalışır. Hiçbir operasyonda elemanlar üzerinde dolaşma gerekmez; her işlem sadece top pointer'ı veya top indeksini etkiler. Bu verimlilik, stack'i birçok algoritmanın bel kemiği yapar.

Overflow ve Underflow

Stack Overflow: Dolu bir stack'e push yapmaya çalışmaktır. Dizi tabanlı stack'lerde kapasite sınırı olduğu için bu durum oluşabilir. Linked list tabanlı stack'lerde teorik olarak bellek bitene kadar overflow olmaz. Bu kavramı bir de şöyle düşünün: bilgisayardaki fonksiyon çağrı yığını (call stack) da sonlu bir alandır. Sonsuz recursion, call stack'i taşırır ve programın "stack overflow" hatasıyla çökmesine yol açar — ünlü programcı sitesi stackoverflow.com de adını buradan alır.

Stack Underflow: Boş bir stack'ten pop veya peek yapmaya çalışmaktır. Her iki implementasyonda da olabilir. Kontrol edilmezse dizi tabanlıda geçersiz indeks erişimi, linked list tabanlıda NULL pointer dereference'a neden olur ve program çöker.

Overflow ve Underflow
Stack Overflow
push(60) → ❌ Kapasite dolu!
10
20
30
40
50← top (max)
Kapasite: 5 / 5
Stack Underflow
pop() → ❌ Eleman yok!
Kapasite: 0 / 3 | top = -1

Dizi Tabanlı Stack (Array-Based)

Stack'i gerçeklemenin en basit ve en yaygın yolu bir dizi kullanmaktır. Sabit boyutlu bir dizi elemanları saklar. Bir top değişkeni tepedeki elemanın indeksini takip eder. Stack boşken top = -1'dir; her push ile top bir artar, her pop ile bir azalır.

Bellekteki Görünüm

Dizi tabanlı stack'te elemanlar bellekte ardışık olarak yer alır. Bu ardışıklık, CPU cache'inin çok verimli çalışmasını sağlar ve stack'in hızlı olmasının en büyük sebebidir. Top indeksi mantıksal olarak "stack'in tepesi"ni gösterir; top'un üstündeki indeksler fiziksel olarak bellekte olsa da stack dışında kabul edilir.

Dizi Tabanlı Stack — push(10), push(20), push(30) sonrası
[0]
10
[1]
20
[2]←top
30
[3]
[4]
[5]
[6]
Sarı = dolu | Yeşil = top | Gri = boş (stack dışı)

Stack Yapısı ve Oluşturma

C dilinde stack yapısını bir struct ile tanımlarız. İki bileşen yeterlidir: elemanları saklayan sabit boyutlu bir dizi ve tepedeki elemanın indeksini tutan bir top değişkeni. stackOlustur fonksiyonu top'u -1 yaparak stack'i "boş" duruma getirir.

stack_struct.c
#include <stdio.h>
#include <stdlib.h>

#define MAX 100

typedef struct {
    int items[MAX];   // elemanları tutan dizi
    int top;           // tepedeki elemanın indeksi
} Stack;

// Stack oluşturma (initialize)
void stackOlustur(Stack* s) {
    s->top = -1;      // -1 = boş stack
}
Boş Stack — top = -1
items[0]
items[1]
items[2]
items[3]
items[4]
top = -1 → Stack boş, hiçbir eleman yok
Neden top = -1? Top, stack'teki son elemanın indeksini tutar. Boş stack'te hiçbir eleman yoktur, dolayısıyla geçerli bir indeks de yoktur. -1 değeri "geçersiz indeks" anlamına gelir ve top + 1 = 0 eleman sayısını doğru verir. Bazı implementasyonlar top'u 0'dan başlatıp "bir sonraki boş pozisyon" olarak kullanır, ancak -1 konvansiyonu daha yaygındır ve items[top] yazımı doğrudan tepedeki elemana erişim sağlar.

isEmpty ve isFull Kontrolleri

Her push ve pop operasyonundan önce stack'in durumunu kontrol etmek zorunludur. Kontrol yapmadan işlem yapmak, dizi sınırını aşma veya negatif indekse erişme gibi ciddi hatalara yol açar. Bu kontroller O(1) karmaşıklığındadır — sadece top değişkeninin değerine bakılır.

kontroller.c
// Stack boş mu?
int isEmpty(Stack* s) {
    return s->top == -1;
}
// top == -1 → hiçbir eleman yok → boş (return 1)
// top >= 0  → en az bir eleman var (return 0)

// Stack dolu mu?
int isFull(Stack* s) {
    return s->top == MAX - 1;
}
// top == MAX-1 → son indeks dolu → stack dolu (return 1)
// top <  MAX-1 → hâlâ yer var (return 0)

// Kullanım:
Stack s;
stackOlustur(&s);
printf("Boş: %d\n", isEmpty(&s));   // 1 (true)
printf("Dolu: %d\n", isFull(&s));   // 0 (false)
isEmpty ve isFull Durumları
isEmpty → true
top = -1
Normal durum
10
20← top=1
top = 1
isFull → true
10
20
30← top=MAX-1
top = 2 = MAX-1 (MAX=3)

Stack Boyutu (size)

Stack'teki eleman sayısını bulmak son derece basittir: top + 1. Top indeksi 0-tabanlı olduğu için eleman sayısı her zaman top'tan bir fazladır.

size.c
int size(Stack* s) {
    return s->top + 1;
}

// top = -1 → size = 0 (boş)
// top =  0 → size = 1 (tek eleman)
// top =  4 → size = 5
// top = MAX-1 → size = MAX (dolu)

Push İşlemi

Push, stack'in tepesine yeni eleman ekler. İşlem iki adımdan oluşur: önce top'u bir artır, sonra yeni elemanı items[top] pozisyonuna yaz. Push'tan önce isFull kontrolü yapılmalıdır; aksi halde dizinin sınırları dışına yazılır ve tanımsız davranış (undefined behavior) oluşur.

push(10), push(20), push(30) — Adım Adım
Boş stack
top = -1
[0]
[1]
[2]
[3]
push(10)
top: -1 → 0
10[0] ← top
push(20)
top: 0 → 1
10
20[1] ← top
push(30)
top: 1 → 2
10
20
30[2] ← top
push.c
void push(Stack* s, int deger) {
    // 1. Overflow kontrolü
    if (isFull(s)) {
        printf("Stack Overflow! Kapasite: %d\n", MAX);
        return;
    }

    // 2. top'u artır
    s->top++;

    // 3. Yeni elemanı yaz
    s->items[s->top] = deger;

    // Tek satırda yazılışı: s->items[++s->top] = deger;
}

// Kullanım:
Stack s;
stackOlustur(&s);
push(&s, 10);   // top: -1→0, items[0]=10
push(&s, 20);   // top:  0→1, items[1]=20
push(&s, 30);   // top:  1→2, items[2]=30
Pre-increment vs post-increment: Push'ta ++s->top (pre-increment) kullanılır çünkü top'u önce artırıp sonra o pozisyona yazmalıyız. Eğer s->top++ (post-increment) kullanırsanız, top'un eski değerine (bir alttaki pozisyona) yazar, sonra top artar — bu da mevcut bir elemanın üzerine yazılmasına yol açar. Bu fark mülakatlarda sıkça sorulur.

Pop İşlemi

Pop, tepedeki elemanı stack'ten çıkarır ve değerini döndürür. İşlem üç adımdan oluşur: top'taki elemanı bir değişkene oku, top'u bir azalt, okunan değeri döndür. Pop'tan önce isEmpty kontrolü yapılmalıdır.

Önemli bir detay: pop sırasında eleman bellekten fiziksel olarak silinmez. Sadece top indeksi geri çekilir ve o pozisyon artık "stack dışında" kabul edilir. Bir sonraki push o pozisyonun üzerine yazacaktır. Bu, performans için avantajlıdır çünkü silme maliyeti sıfırdır.

pop() → 40, pop() → 30, pop() → 20
Başlangıç
10
20
30
40← top=3
pop() → 40
top: 3 → 2
10
20
30← top=2
40
pop() → 30
top: 2 → 1
10
20← top=1
30
40
pop() → 20
top: 1 → 0
10← top=0
20
30
40
Gri değerler bellekte ama stack dışında (erişilemez)
pop.c
int pop(Stack* s) {
    // 1. Underflow kontrolü
    if (isEmpty(s)) {
        printf("Stack Underflow! Stack boş.\n");
        return -1;   // hata değeri
    }

    // 2. Tepedeki elemanı oku
    int deger = s->items[s->top];

    // 3. top'u geri çek
    s->top--;

    // 4. Okunan değeri döndür
    return deger;

    // Tek satırda: return s->items[s->top--];
}

// Kullanım: (stack: 10, 20, 30, 40)
int x = pop(&s);   // x = 40 (son eklenen ilk çıkar)
int y = pop(&s);   // y = 30
int z = pop(&s);   // z = 20
pop(&s);            // → 10
pop(&s);            // "Stack Underflow!" (boş)
Post-decrement neden doğru: Pop'ta s->top-- (post-decrement) kullanılır çünkü önce mevcut top'taki değer okunmalı, sonra top azaltılmalıdır. --s->top (pre-decrement) kullanılsaydı top önce azalır ve bir alttaki (yanlış) eleman döndürülürdü.

Peek (Top) İşlemi

Peek, tepedeki elemana bakar ama çıkarmaz. Pop'un "salt-okunur" versiyonudur. Stack'in durumunu hiçbir şekilde değiştirmez: ne top indeksi ne de elemanlar etkilenir. Ardı ardına 100 kez peek çağırsanız bile her seferinde aynı değer döner ve stack aynı kalır.

Peek, birçok algoritmada kritik öneme sahiptir. Örneğin parantez eşleştirmede yeni bir kapanış parantezi geldiğinde, stack'in tepesindeki açılış parantezinin eşleşip eşleşmediğini kontrol etmek için önce peek yapılır. Eşleşiyorsa pop yapılır, eşleşmiyorsa hata bildirilir. Bu karar mekanizması olmadan algoritma çalışamaz.

peek.c
int peek(Stack* s) {
    if (isEmpty(s)) {
        printf("Stack boş, peek yapılamaz!\n");
        return -1;
    }
    return s->items[s->top];   // top DEĞİŞMEZ!
}

// push(10), push(20), push(30) sonrası:
printf("%d\n", peek(&s));      // 30
printf("%d\n", peek(&s));      // 30 (hâlâ aynı, stack değişmedi)
printf("Boyut: %d\n", size(&s)); // 3 (boyut aynı)
pop(&s);                        // 30 çıktı
printf("%d\n", peek(&s));      // 20 (yeni top)
Pop vs Peek — Farkı Anlamak
Başlangıç
10
20
30← top
peek() → 30
10
20
30← top (aynı)
Stack değişmedi ✓
pop() → 30
10
20← yeni top
30
30 çıkarıldı, top geriledi

Stack'i Yazdırma

Gerçek bir stack'te sadece top'a erişim vardır; tüm elemanları görmek soyutlamayı kırar. Ancak debug ve eğitim amacıyla top'tan aşağıya doğru yazdırma fonksiyonu son derece kullanışlıdır.

yazdirma.c
void stackYazdir(Stack* s) {
    if (isEmpty(s)) {
        printf("[Stack boş]\n");
        return;
    }
    printf("Stack (top → bottom):\n");
    for (int i = s->top; i >= 0; i--) {
        if (i == s->top)
            printf("  → [%d] %d  ← TOP\n", i, s->items[i]);
        else
            printf("    [%d] %d\n", i, s->items[i]);
    }
    printf("  Boyut: %d / %d\n", size(s), MAX);
}

// Çıktı (push 10, 20, 30 sonrası):
// Stack (top → bottom):
//   → [2] 30  ← TOP
//     [1] 20
//     [0] 10
//   Boyut: 3 / 100

Tam Çalışan Program

Tüm operasyonları bir araya getiren eksiksiz bir program. Overflow ve underflow durumlarını, push/pop/peek sıralamasını ve size/isEmpty/isFull kontrollerini gösterir.

stack_demo.c
#include <stdio.h>

#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; }
int  size(Stack* s)         { return s->top + 1; }

void push(Stack* s, int val) {
    if (isFull(s)) { printf("  ⚠ Overflow!\n"); return; }
    s->items[++s->top] = val;
    printf("  push(%d) → top=%d, size=%d\n", val, s->top, s->top + 1);
}

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;
}

int peek(Stack* s) {
    if (isEmpty(s)) { printf("  ⚠ Boş!\n"); return -1; }
    return s->items[s->top];
}

void stackYazdir(Stack* s) {
    if (isEmpty(s)) { printf("  [Boş]\n"); return; }
    printf("  Stack: ");
    for (int i = s->top; i >= 0; i--)
        printf("%d ", s->items[i]);
    printf("(top→bottom)\n");
}

int main() {
    Stack s;
    stackOlustur(&s);

    printf("=== Push İşlemleri ===\n");
    push(&s, 10);  push(&s, 20);  push(&s, 30);
    push(&s, 40);  push(&s, 50);
    push(&s, 60);   // overflow!

    printf("\n=== Durum ===\n");
    printf("  Peek: %d\n", peek(&s));
    printf("  Boyut: %d/%d\n", size(&s), MAX);
    printf("  Boş mu: %d\n", isEmpty(&s));
    printf("  Dolu mu: %d\n", isFull(&s));
    stackYazdir(&s);

    printf("\n=== Pop İşlemleri ===\n");
    while (!isEmpty(&s))
        pop(&s);
    pop(&s);   // underflow!

    return 0;
}
program çıktısı
=== Push İşlemleri ===
  push(10) → top=0, size=1
  push(20) → top=1, size=2
  push(30) → top=2, size=3
  push(40) → top=3, size=4
  push(50) → top=4, size=5
  ⚠ Overflow!

=== Durum ===
  Peek: 50
  Boyut: 5/5
  Boş mu: 0
  Dolu mu: 1
  Stack: 50 40 30 20 10 (top→bottom)

=== Pop İşlemleri ===
  pop() → 50, top=3
  pop() → 40, top=2
  pop() → 30, top=1
  pop() → 20, top=0
  pop() → 10, top=-1
  ⚠ Underflow!
LIFO'yu gözlemleyin: Push sırası: 10, 20, 30, 40, 50. Pop sırası: 50, 40, 30, 20, 10. Son giren (50) ilk çıktı, ilk giren (10) son çıktı. Bu stack'in özüdür.

Dinamik Dizi Tabanlı Stack

Yukarıda sabit boyutlu (static) dizi tabanlı stack'i inceledik. Bu yaklaşımın en büyük sorunu kapasitenin derleme zamanında belirlenmesidir. #define MAX 100 yazdığınızda, stack en fazla 100 eleman tutabilir — ne eksik ne fazla. Kapasite küçükse overflow, büyükse bellek israfı yaşarsınız. Peki ya kaç eleman geleceğini bilmiyorsak?

Dinamik dizi tabanlı stack, bu sorunu çalışma zamanında çözer. Başlangıçta küçük bir kapasite ile başlanır (örneğin 4) ve stack dolduğunda realloc ile kapasite 2 katına çıkarılır. Bu strateji "doubling strategy" olarak bilinir.

Doubling Strategy Nasıl Çalışır?

Dinamik Büyüme — Kapasite 2 → 4 → 8
Başlangıç: kapasite = 2
[0]
10
[1]←top
20
Dolu! push(30) geldi → realloc tetiklenir
↓ realloc: kapasite × 2 = 4 ↓
Genişletildi: kapasite = 4
[0]
10
[1]
20
[2]←top
30
[3]
push(40) → [3]'e sığar, realloc gerekmez
↓ push(50): dolu → realloc: 4 × 2 = 8 ↓
Genişletildi: kapasite = 8
[0]
10
[1]
20
[2]
30
[3]
40
[4]←top
50
[5]
[6]
[7]

Amortized O(1) Analizi

Her realloc çağrısı O(n) sürer çünkü tüm elemanlar eski bellekten yeni belleğe kopyalanır. Bu ilk bakışta push'un O(n) olduğu izlenimini verebilir. Ancak realloc her push'ta değil, sadece dolu olunca yapılır. Kapasite her seferinde 2 katına çıktığı için geometrik seri oluşur:

n = 32 Push İçin Toplam Kopyalama Maliyeti
Kapasite 2→4   kopyalama: 2
Kapasite 4→8   kopyalama: 4
Kapasite 8→16  kopyalama: 8
Kapasite 16→32 kopyalama: 16
Toplam kopyalama = 2 + 4 + 8 + 16 = 30 ≈ 2n
32 push + 30 kopyalama = 62 işlem → push başına ≈ 2 işlem = O(1)
Amortized O(1): Tek bir push O(n) sürebilir (realloc anında), ama n push'un toplam maliyeti ≈ 3n'dir. Push başına ortalama maliyet O(1) kalır. Buna "amortize edilmiş sabit zaman" denir. C++'ın std::vector ve Java'nın ArrayList yapıları da tam olarak bu stratejiyi kullanır.
dynamic_stack.c
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int* items;       // dinamik dizi (heap'te)
    int top;           // tepedeki elemanın indeksi
    int kapasite;      // mevcut dizi boyutu
} DStack;

// Oluşturma
void dstackOlustur(DStack* s, int baslangicKap) {
    s->items = (int*)malloc(baslangicKap * sizeof(int));
    if (!s->items) {
        printf("Bellek ayrılamadı!\n");
        exit(1);
    }
    s->top = -1;
    s->kapasite = baslangicKap;
}

// Kontroller
int disEmpty(DStack* s) { return s->top == -1; }
int dsize(DStack* s)   { return s->top + 1; }

// Push — doluysa kapasiteyi 2 katına çıkar
void dpush(DStack* s, int deger) {
    if (s->top == s->kapasite - 1) {
        int yeniKap = s->kapasite * 2;
        int* yeni = (int*)realloc(s->items, yeniKap * sizeof(int));
        if (!yeni) {
            printf("realloc hatası!\n");
            return;
        }
        s->items = yeni;
        printf("  ↑ Kapasite %d → %d\n", s->kapasite, yeniKap);
        s->kapasite = yeniKap;
    }
    s->items[++s->top] = deger;
}

// Pop
int dpop(DStack* s) {
    if (disEmpty(s)) {
        printf("Underflow!\n");
        return -1;
    }
    return s->items[s->top--];
}

// Peek
int dpeek(DStack* s) {
    if (disEmpty(s)) {
        printf("Stack boş!\n");
        return -1;
    }
    return s->items[s->top];
}

// Belleği serbest bırak
void dstackSerbest(DStack* s) {
    free(s->items);
    s->items = NULL;
    s->top = -1;
    s->kapasite = 0;
}
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.

Shrink Stratejisi (Opsiyonel)

Dinamik stack'te sadece büyüme değil, küçülme de uygulanabilir. Pop sonrası eleman sayısı kapasitenin ¼'üne düştüğünde kapasiteyi yarıya indirmek bellekten tasarruf sağlar. Neden ½ değil de ¼? Eğer eşik ½ olursa, tam sınırda art arda push-pop yapıldığında sürekli genişle-küçül döngüsü oluşur (thrashing). ¼ eşiği bu sorunu önler.

shrink.c
int dpopShrink(DStack* s) {
    if (disEmpty(s)) { printf("Underflow!\n"); return -1; }

    int deger = s->items[s->top--];

    // Kapasite çok büyükse küçült (min kapasite: 4)
    if (s->top + 1 > 0 &&
        s->top + 1 == s->kapasite / 4 &&
        s->kapasite > 4)
    {
        s->kapasite /= 2;
        s->items = (int*)realloc(s->items,
                   s->kapasite * sizeof(int));
        printf("  ↓ Kapasite %d'ye küçültüldü\n", s->kapasite);
    }

    return deger;
}

Bağlı Liste Tabanlı Stack

Stack'i linked list ile gerçeklemek, kapasite sorununu tamamen ortadan kaldırır: her yeni push için bir düğüm oluşturulur, bellek bitene kadar overflow olmaz. İsFull kontrolüne gerek yoktur. Push işlemi linked list'in başına ekleme, pop işlemi baştan silme ile birebir eşleşir — her ikisi de zaten O(1) olan operasyonlardır.

Eşleşme: Stack ↔ Linked List

Stack OperasyonuLinked List KarşılığıNeden?
push(x)Başa eleman eklemeO(1) — sadece head pointer güncellenir
pop()Baştan eleman silmeO(1) — head silinir, next yeni head olur
peek()head->dataO(1) — sadece okuma
isEmpty()head == NULLO(1)
top pointerhead pointerListenin başı = stack'in tepesi
Neden başa ekleme? Sona ekleme O(n) sürer (tail'i bulmak için traverse gerekir). Başa ekleme O(1)'dir ve doğrudan top pointer güncellenir. Bu yüzden stack'in tepesi, linked list'in başıdır (head).

Push ve Pop Görsel

Linked List Stack — Push İşlemi
push(10):
top→
10
NULL
push(20): yeni düğüm → eski top'un önüne
top→
20
10
NULL
push(30): yeni düğüm → eski top'un önüne
top→
30
20
10
NULL
Linked List Stack — Pop İşlemi
Mevcut: top → 30 → 20 → 10 → NULL
top→
30
20
10
NULL
pop() → 30 çıktı, free(30), top = 20
top→
20
10
NULL
pop() → 20 çıktı, free(20), top = 10
top→
10
NULL

Tam Implementasyon

stack_linkedlist.c
#include <stdio.h>
#include <stdlib.h>

// ── Düğüm yapısı ──
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// ── Stack yapısı ──
typedef struct {
    Node* top;     // listenin başı = stack'in tepesi
    int boyut;     // eleman sayısı (O(1) size için)
} LLStack;

// ── Oluşturma ──
void llStackOlustur(LLStack* s) {
    s->top = NULL;
    s->boyut = 0;
}

// ── Kontroller ──
int llIsEmpty(LLStack* s) { return s->top == NULL; }
int llSize(LLStack* s)    { return s->boyut; }

// ── Push = başa ekleme ──
void llPush(LLStack* s, int deger) {
    Node* yeni = (Node*)malloc(sizeof(Node));
    if (!yeni) {
        printf("Bellek ayrılamadı!\n");
        return;
    }
    yeni->data = deger;       // veriyi ata
    yeni->next = s->top;      // yeninin next'i → eski top
    s->top = yeni;             // yeni düğüm artık top
    s->boyut++;

    printf("  push(%d) → boyut=%d\n", deger, s->boyut);
}

// ── Pop = baştan silme ──
int llPop(LLStack* s) {
    if (llIsEmpty(s)) {
        printf("  ⚠ Underflow!\n");
        return -1;
    }

    Node* silinecek = s->top;          // ①
    int deger = silinecek->data;        // ② değeri kaydet
    s->top = silinecek->next;           // ③ top → sonraki
    free(silinecek);                     // ④ eski top'u serbest bırak
    s->boyut--;

    printf("  pop() → %d, boyut=%d\n", deger, s->boyut);
    return deger;
}

// ── Peek ──
int llPeek(LLStack* s) {
    if (llIsEmpty(s)) {
        printf("  ⚠ Stack boş!\n");
        return -1;
    }
    return s->top->data;    // sadece oku, çıkarma
}

// ── Yazdırma ──
void llYazdir(LLStack* s) {
    if (llIsEmpty(s)) { printf("  [Boş]\n"); return; }
    printf("  top");
    Node* temp = s->top;
    while (temp) {
        printf(" → [%d]", temp->data);
        temp = temp->next;
    }
    printf(" → NULL\n");
}

// ── Tüm stack'i serbest bırak ──
void llStackSerbest(LLStack* s) {
    while (!llIsEmpty(s))
        llPop(s);
}

// ── Demo ──
int main() {
    LLStack s;
    llStackOlustur(&s);

    printf("=== Push ===\n");
    llPush(&s, 10);
    llPush(&s, 20);
    llPush(&s, 30);
    llPush(&s, 40);
    llYazdir(&s);

    printf("\nPeek: %d\n", llPeek(&s));

    printf("\n=== Pop ===\n");
    llPop(&s);
    llPop(&s);
    llYazdir(&s);

    llStackSerbest(&s);

    return 0;
}
program çıktısı
=== Push ===
  push(10) → boyut=1
  push(20) → boyut=2
  push(30) → boyut=3
  push(40) → boyut=4
  top → [40] → [30] → [20] → [10] → NULL

Peek: 40

=== Pop ===
  pop() → 40, boyut=3
  pop() → 30, boyut=2
  top → [20] → [10] → NULL
  pop() → 20, boyut=1
  pop() → 10, boyut=0

Üç Implementasyonun Karşılaştırması

ÖzellikSabit DiziDinamik DiziLinked List
KapasiteSabit (derleme zamanı)Dinamik (2x büyür)Sınırsız
PushO(1)O(1) amortizedO(1)
PopO(1)O(1)O(1)
PeekO(1)O(1)O(1)
Overflow riskiEvetÇok nadirHayır
isFull gerekli mi?EvetHayırHayır
Cache performansıMükemmelÇok iyiKötü
Bellek overheadBoş hücrelerBoş hücreler (az)Düğüm başı +8 byte
malloc/freeHiçNadiren (realloc)Her push/pop'ta
ImplementasyonEn basitOrtaPointer yönetimi
Shrink desteğiOpsiyonelOtomatik (free)
Pratikte hangisi? Standart kütüphaneler ve üretim kodu neredeyse her zaman dinamik dizi tabanlı stack tercih eder. Sebebi cache locality'dir: ardışık bellekte yer alan elemanlar CPU cache'i ile çok uyumludur. Linked list'te her düğüm belleğin farklı bir yerinde olduğu için CPU cache sürekli "miss" yapar ve aynı algoritmik karmaşıklığa rağmen 5-10 kat daha yavaş olabilir. Linked list stack genellikle eğitim amaçlı ve kapasitenin önceden bilinmediği özel durumlarda tercih edilir.

Stack Nerelerde Kullanılır?

1. Fonksiyon Çağrı Yığını (Call Stack)

Programlama dillerinin çalışma zamanı ortamı fonksiyon çağrılarını bir stack ile yönetir. Her fonksiyon çağrıldığında stack'e bir "stack frame" push edilir. Bu frame, fonksiyonun yerel değişkenlerini, parametrelerini ve dönüş adresini içerir. Fonksiyon bittiğinde frame pop edilir ve kontrol çağıran fonksiyona döner.

Call Stack — main() → A() → B() → C()
main()
main()
A() çağrıldı
main()
A()
B() çağrıldı
main()
A()
B()
C() çağrıldı
main()
A()
B()
C()
C() bitti
main()
A()
B()
Sonsuz recursion → frame'ler birikir → Stack Overflow!

Recursive fonksiyonlarda her çağrı yeni bir frame oluşturur. Eğer recursion çok derine giderse (örneğin base case'siz bir factorial fonksiyonu), call stack taşar ve program "Segmentation fault" veya "Stack Overflow" hatası ile çöker. Linux'ta varsayılan call stack boyutu genellikle 8 MB'dır.

2. Geri Alma (Undo) / Yeniden Yapma (Redo)

Metin editörlerinde her eylem undo stack'ine push edilir. Ctrl+Z basıldığında son eylem pop edilip geri alınır ve redo stack'ine push edilir. Ctrl+Y (veya Ctrl+Shift+Z) basıldığında redo stack'inden pop edilip tekrar uygulanır. İki stack birlikte çalışarak ileri-geri gezinme sağlar.

Undo/Redo — İki Stack Mekanizması
Undo Stack
Yaz: "Merhaba"
Kalın yap
Renk: kırmızı
Ctrl+Z → "Renk: kırmızı" pop edilir
Ctrl+Z / Ctrl+Y
Redo Stack
Renk: kırmızı
Pop edilen eylem buraya push

3. Parantez Eşleştirme

Derleyiciler ve kod editörleri, parantez dengesi kontrolünü stack ile yapar. Açılan her parantez push, kapanan her parantez peek + pop ile kontrol edilir. Bu algoritma () [] {} <> tüm eşleşen semboller için çalışır.

parantez_eslestirme.c
int parantezDengeli(const char* ifade) {
    Stack s;
    stackOlustur(&s);

    for (int i = 0; ifade[i] != '\0'; i++) {
        char c = ifade[i];

        // Açılış → push
        if (c == '(' || c == '[' || c == '{') {
            push(&s, c);
        }
        // Kapanış → peek + pop ile kontrol
        else if (c == ')' || c == ']' || c == '}') {
            if (isEmpty(&s)) return 0;  // eşi yok

            char top = pop(&s);
            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{'))
                return 0;  // yanlış eşleşme
        }
    }
    return isEmpty(&s);  // stack boşsa dengeli
}

parantezDengeli("{[()]}");   // 1 (dengeli ✓)
parantezDengeli("([)]");     // 0 (çapraz ✗)
parantezDengeli("(((");      // 0 (kapanmamış ✗)
parantezDengeli("()");       // 1 (dengeli ✓)
Parantez Eşleştirme — "{[()]}" Örneği
{ → push
{
[ → push
{
[
( → push
{
[
(
) → pop (
{
[
] → pop [
{
} → pop {
boş
Stack boş → Dengeli ✓

4. Postfix İfade Değerlendirme

Hesap makineleri ve derleyiciler, matematiksel ifadeleri stack ile değerlendirir. İnfix ifade (3 + 4 * 2) önce postfix'e (3 4 2 * +) çevrilir, sonra stack ile hesaplanır: sayılar push edilir, operatör geldiğinde iki sayı pop edilip işlem yapılıp sonuç push edilir.

5. Tarayıcı Geri/İleri Butonu

Ziyaret edilen her sayfa "back stack"e push edilir. Geri butonu son sayfayı pop edip "forward stack"e push eder. İleri butonu bunun tersini yapar. Undo/redo ile aynı iki-stack mekanizmasıdır.

6. DFS (Derinlik Öncelikli Arama)

Graf ve ağaç yapılarında derinlik öncelikli arama stack kullanır. Her adımda mevcut düğümün komşuları push edilir ve pop edilen düğüm ziyaret edilir. Recursive DFS ise aslında call stack'i dolaylı olarak kullanır — explicit stack ile iteratif DFS yazmak aynı sonucu verir.

7. String Tersine Çevirme

Her karakter sırayla push edilir, ardından tüm karakterler pop edilir. LIFO prensibi gereği son karakter ilk çıkar ve string otomatik olarak tersine döner.

Sık Yapılan Hatalar

1. Overflow / Underflow Kontrolü Yapmamak

En tehlikeli ve en yaygın hata. Her push'tan önce isFull (dizi tabanlıda), her pop/peek'ten önce isEmpty kontrolü zorunludur. Kontrol yapılmazsa programın çökmesi kaçınılmazdır.

hata1.c
// ❌ YANLIŞ: Kontrol yok
int x = s.items[s.top--];   // top=-1 ise → items[-1] → ÇÖKME
s.items[++s.top] = val;     // top=MAX-1 ise → dizi taşması

// ✅ DOĞRU: Her zaman kontrol et
if (!isEmpty(&s)) { int x = pop(&s); }
if (!isFull(&s))  { push(&s, val); }

2. Pop ve Peek Karıştırmak

Pop elemanı çıkarır, peek çıkarmaz. Sadece bakmak isteyip yanlışlıkla pop çağırmak veri kaybına yol açar. Özellikle döngü içinde koşul kontrolü yaparken peek kullanılmalıdır.

hata2.c
// ❌ YANLIŞ: Sadece bakmak istiyorduk ama pop yaptık
if (pop(&s) == hedef) {
    // eleman zaten çıktı, tekrar erişilemez!
}

// ✅ DOĞRU: Önce peek ile bak, gerekirse pop yap
if (peek(&s) == hedef) {
    int x = pop(&s);   // şimdi bilinçli çıkardık
}

3. Pre/Post Increment-Decrement Karışıklığı

hata3.c
// Push: ÖNCE artır, SONRA yaz → pre-increment
s->items[++s->top] = deger;   // ✅ doğru pozisyona yazar
s->items[s->top++] = deger;   // ❌ eski pozisyona yazar

// Pop: ÖNCE oku, SONRA azalt → post-decrement
return s->items[s->top--];    // ✅ doğru elemanı döndürür
return s->items[--s->top];    // ❌ bir alttakini döndürür

4. Linked List Stack'te free() Unutmak

Pop sırasında düğümü free() etmemek bellek sızıntısına (memory leak) neden olur. Her pop'ta eski düğüm free edilmelidir. Programın sonunda kalan düğümler için llStackSerbest() çağırmak iyi bir alışkanlıktır.

hata4.c
// ❌ YANLIŞ: free() yok → bellek sızıntısı
int llPop_hatali(LLStack* s) {
    int deger = s->top->data;
    s->top = s->top->next;   // eski düğüm kayıp!
    return deger;
}

// ✅ DOĞRU: Önce kaydet, sonra free
int llPop_dogru(LLStack* s) {
    Node* silinecek = s->top;   // ① pointer'ı kaydet
    int deger = silinecek->data; // ② değeri kaydet
    s->top = silinecek->next;    // ③ top'u ilerlet
    free(silinecek);              // ④ belleği serbest bırak
    return deger;
}

5. Stack'i Queue Gibi Kullanmak

Stack'in ortasından veya altından eleman çıkarmak tanım gereği mümkün değildir. Eğer FIFO davranışına ihtiyaç duyuyorsanız queue kullanmalısınız. İki stack kullanarak bir queue simüle etmek mümkündür — bu yaygın bir mülakat sorusudur ve stack uygulamaları bölümünde ele alınacaktır.

Zaman ve Alan Karmaşıklığı Özeti

OperasyonArray (sabit)Array (dinamik)Linked List
push()O(1)O(1) amortizedO(1)
pop()O(1)O(1)O(1)
peek()O(1)O(1)O(1)
isEmpty()O(1)O(1)O(1)
isFull()O(1)
size()O(1)O(1)O(1)*
AramaO(n)O(n)O(n)
AlanO(MAX)O(n)O(n)

* Linked list'te boyut sayacı tutuluyorsa O(1), yoksa traverse gerekir O(n).

Sonuç

Stack, programlamanın en temel yapı taşlarından biridir. LIFO prensibi basit görünse de bu prensibin uygulandığı alanlar son derece geniştir: fonksiyon çağrı yönetiminden parantez eşleştirmeye, undo mekanizmalarından DFS algoritmasına kadar her yerde stack karşımıza çıkar. Tüm temel operasyonlarının O(1) olması, stack'i kullanım alanında son derece verimli bir araç yapar.

Üç implementasyon yöntemini de bilmek önemlidir: sabit dizi en basiti, dinamik dizi en pratik olanı, linked list ise en esnek olanıdır. Gerçek dünya uygulamalarında genellikle dinamik dizi tabanlı tercih edilir (cache locality avantajı); ancak kapasite bilinmediğinde linked list güçlü bir alternatiftir.

Bir sonraki bölümde stack'in uygulamalarını detaylı olarak inceleyeceğiz: postfix ifade değerlendirme, infix→postfix dönüşüm (Shunting Yard algoritması), min stack, iki stack ile queue simülasyonu ve recursion'ın stack ile iteratif hale dönüştürülmesi. Ardından stack'in ikizi olan queue (kuyruk) veri yapısına geçeceğiz.

Genel Özet: Stack = LIFO. Push tepeye ekler, pop tepeden çıkarır, peek tepeden okur. Hepsi O(1). Üç implementasyon: sabit dizi (basit), dinamik dizi (pratik, amortized O(1)), linked list (esnek, sınırsız). Overflow ve underflow kontrolü her zaman zorunludur. Gerçek hayat: call stack, undo/redo, parantez kontrolü, DFS, postfix hesaplama.
← Veri Yapıları