Stack Nerede Kullanılır?
Stack'in LIFO prensibi "en son açılan en önce kapanmalı" kalıbına uyan her problemi zarif bir şekilde çözer. Bu bölümde dört klasik uygulamayı inceleyeceğiz:
Bu Bölümün Haritası
🔗
Parantez Dengeleme
( ) [ ] { } eşleşme
🔄
Infix → Postfix/Prefix
Shunting Yard algoritması
📊
Postfix Değerlendirme
Hesap makinesi motoru
↩️
Undo Mekanizması
Geri alma / yineleme
Her uygulama aynı temel prensibi kullanır: bir şeyi geçici olarak stack'e koy, uygun koşul oluştuğunda çıkar ve işle. Fark yalnızca ne koyulduğu, ne zaman çıkarıldığı ve çıkarılanla ne yapıldığıdır.
Parantez Dengeleme Kontrolü
Bir ifadenin parantezlerinin doğru eşleşip eşleşmediğini kontrol etmek, compiler'ların, IDE'lerin ve metin editörlerinin en temel işlevlerinden biridir. Yanlış eşleşme derleme hatasına, sonsuz döngüye veya mantık hatasına yol açabilir. Stack bu problemi tek geçişte (single pass) ve O(n) zamanda çözer.
Problem Tanımı
Verilen bir string içindeki üç tür parantez çifti kontrol edilir: ( ), [ ] ve { }. String "dengeli" (balanced) kabul edilir eğer:
✓ Dengeli (Balanced)
✓ {[()]}
✓ a*(b+c)-{d/[e+f]}
✓ ((()))
✓ boş string ""
✗ Dengesiz (Unbalanced)
✗ {[()} — ] eksik
✗ )( — sıra yanlış
✗ (()] — tip uyuşmuyor
✗ (() — kapanmamış
Algoritma
Algoritma tek bir kurala dayanır: "en son açılan parantez, en önce kapanmalıdır" — bu tam olarak LIFO'dur.
Algoritma Adımları:
1. String'i soldan sağa tara.
2. Karakter bir açma parantezi ise → stack'e push et.
3. Karakter bir kapama parantezi ise → stack'ten pop et.
• Stack boşsa → dengesiz (fazla kapama).
• Pop edilen eşleşmiyorsa → dengesiz (tip uyuşmazlığı).
4. Karakter parantez değilse → atla (skip).
5. String bitti ve stack boşsa → dengeli ✓
Stack doluysa → dengesiz (kapanmamış parantezler).
Adım Adım Görsel — Dengeli: {[()]}
"{[()]}" — 6 Adımlık Takip
4
)
Kapama → pop → ( eşleşti ✓
{ [
( ↔ ) ✓
5
]
Kapama → pop → [ eşleşti ✓
{
[ ↔ ] ✓
6
}
Kapama → pop → { eşleşti ✓
boş
{ ↔ } ✓
String bitti, stack boş → Dengeli ✓
Adım Adım Görsel — Dengesiz: {[(])}
"{[(])}" — Tip Uyuşmazlığı Tespiti
4
]
Kapama → pop → ( ≠ ] ✗
—
( ↔ ] ✗ HATA
⛔ Adım 4'te duruldu: pop edilen "(" ile gelen "]" eşleşmiyor → Dengesiz
Adım Adım Görsel — Dengesiz: (()
"(()" — Kapanmamış Parantez
3
)
Kapama → pop → ( eşleşti ✓
(
( ↔ ) ✓
⛔ String bitti ama stack'te hâlâ "(" kaldı → Dengesiz (kapanmamış parantez)
Eşleştirme Mantığı
Bir kapama parantezi geldiğinde, stack'ten pop edilen açma parantezinin aynı türden olup olmadığını kontrol etmemiz gerekir. Bu eşleştirme basit bir yardımcı fonksiyon ile yapılır:
Eşleşme Tablosu
Açma push edilir, kapama gelince pop edilen ile karşılaştırılır
Tam C Implementasyonu
#include <stdio.h>
#include <string.h>
#define MAX 256
typedef struct {
char items[MAX];
int top;
} CharStack;
void olustur(CharStack* s) { s->top = -1; }
int isEmpty(CharStack* s) { return s->top == -1; }
void push(CharStack* s, char c) { s->items[++s->top] = c; }
char pop(CharStack* s) { return s->items[s->top--]; }
/* ── Açma parantezi mi? ── */
int acmaMi(char c) {
return c == '(' || c == '[' || c == '{';
}
/* ── Kapama parantezi mi? ── */
int kapamaMi(char c) {
return c == ')' || c == ']' || c == '}';
}
/* ── Eşleşme kontrolü ── */
int eslesiyorMu(char acma, char kapama) {
return (acma == '(' && kapama == ')') ||
(acma == '[' && kapama == ']') ||
(acma == '{' && kapama == '}');
}
/* ════════════════════════════════
ANA FONKSİYON: Parantez Dengeli mi?
════════════════════════════════ */
int dengeliMi(const char* ifade) {
CharStack s;
olustur(&s);
for (int i = 0; ifade[i] != '\0'; i++) {
char c = ifade[i];
if (acmaMi(c)) {
push(&s, c); // Kural 2: açma → push
}
else if (kapamaMi(c)) {
// Kural 3a: stack boşsa → fazla kapama
if (isEmpty(&s)) {
printf(" Hata: pozisyon %d — '%c' için eşleşme yok\n",
i, c);
return 0;
}
char acma = pop(&s);
// Kural 3b: tip uyuşmazlığı
if (!eslesiyorMu(acma, c)) {
printf(" Hata: pozisyon %d — '%c' ile '%c' eşleşmiyor\n",
i, acma, c);
return 0;
}
}
// Kural 4: parantez değilse → atla
}
// Kural 5: string bitti, stack boş mu?
if (!isEmpty(&s)) {
printf(" Hata: %d adet kapanmamış parantez\n", s.top + 1);
return 0;
}
return 1; // Dengeli ✓
}Demo Program
int main() {
const char* testler[] = {
"{[()]}", // dengeli
"a*(b+c)-{d/[e+f]}", // dengeli (parantez dışı karakterler)
"((()))", // dengeli
"", // dengeli (boş string)
"{[(])}", // dengesiz: tip uyuşmazlığı
")(", // dengesiz: fazla kapama
"(()", // dengesiz: kapanmamış
"([{((()))}])", // dengeli (derin iç içe)
};
int n = sizeof(testler) / sizeof(testler[0]);
for (int i = 0; i < n; i++) {
printf("Test: \"%s\"\n", testler[i]);
if (dengeliMi(testler[i]))
printf(" ✓ Dengeli\n\n");
else
printf(" ✗ Dengesiz\n\n");
}
return 0;
}Test: "{[()]}"
✓ Dengeli
Test: "a*(b+c)-{d/[e+f]}"
✓ Dengeli
Test: "((()))"
✓ Dengeli
Test: ""
✓ Dengeli
Test: "{[(])}"
Hata: pozisyon 3 — '(' ile ']' eşleşmiyor
✗ Dengesiz
Test: ")("
Hata: pozisyon 0 — ')' için eşleşme yok
✗ Dengesiz
Test: "(()"
Hata: 1 adet kapanmamış parantez
✗ Dengesiz
Test: "([{((()))}])"
✓ DengeliKarmaşıklık Analizi
| Ölçüt | Değer | Neden? |
|---|
| Zaman | O(n) | Her karakter tam bir kez ziyaret edilir |
| Push/Pop | En fazla n/2 | Sadece parantez karakterleri stack'e girer |
| Bellek | O(n) | En kötü: tamamı açma parantezi → "(((((" |
| Pratik bellek | Genellikle O(d) | d = maksimum iç içe derinlik (depth) |
Edge Case'ler ve Dikkat Noktaları
Üç Hata Türü
① Fazla Kapama
)(
Kapama geldi ama stack boş → eşleşecek açma yok
② Tip Uyuşmazlığı
(]
Pop edilen açma ile kapama farklı tür → ( ≠ ]
③ Kapanmamış
(()
String bitti ama stack'te açma parantezi kaldı
String'deki diğer karakterler: Parantez olmayan karakterler (harfler, rakamlar, operatörler) algoritmayı etkilemez — sadece atlanır. Bu sayede a*(b+c)-{d/[e+f]} gibi matematiksel ifadeler de doğru kontrol edilir. Bazı uygulamalar (JSON parser, HTML tag kontrolü) farklı "açma-kapama" çiftleri kullanır ama temel mantık aynıdır.
Gerçek Dünya Kullanım Alanları
Parantez dengeleme algoritması sadece akademik bir alıştırma değildir. Endüstride yaygın olarak kullanılan uygulamaları şunlardır:
Compiler / Interpreter
Kaynak koddaki parantez, süslü parantez ve köşeli parantez hatalarını derleme öncesi tespit eder. GCC, Clang gibi derleyicilerin lexer/parser aşamasının temel parçasıdır.
IDE ve Editörler
VS Code, IntelliJ gibi editörler imleç bir paranteze geldiğinde eşini vurgular. Bu "bracket matching" özelliği tam olarak bu algoritmanın gerçek zamanlı versiyonudur.
JSON / XML Doğrulama
JSON'daki { } ve [ ], XML/HTML'deki <tag> </tag> çiftleri aynı dengeleme prensibiyle kontrol edilir. Hatalı iç içe yapı → geçersiz dosya.
Matematik Motoru
Hesap makineleri ve CAS sistemleri kullanıcının girdiği ifadeyi değerlendirmeden önce parantez kontrolü yapar. Dengesizse hata mesajı verir.
Bir sonraki uygulama: Parantez dengeleme stack'in en basit uygulamasıdır. Bir sonraki bölümde daha karmaşık bir probleme geçeceğiz: matematiksel ifadeleri infix notasyonundan postfix'e (ters Polonya notasyonu) dönüştürmek — Dijkstra'nın ünlü Shunting Yard algoritması ile.
Infix → Postfix Dönüşümü
Matematiksel ifadeleri günlük hayatta infix notasyonuyla yazarız: operatör iki operandın arasındadır (a + b). Ancak bilgisayarlar ve hesap makineleri ifadeleri değerlendirmek için genellikle postfix (Ters Polonya Notasyonu — Reverse Polish Notation, RPN) kullanır çünkü postfix'te parantez gerekmez ve operatör önceliği belirsizliği yoktur.
Üç Notasyon Biçimi
Aynı İfade — Üç Farklı Notasyon
Infix: operatör arada | Postfix: operatör sonra | Prefix: operatör önce
Infix
(a + b) * c
a + b * c
((a + b) * (c - d)) / e
İnsan dostu ama parantez ve öncelik kuralları gerektirir
Postfix (RPN)
a b + c *
a b c * +
a b + c d - * e /
Parantez yok, belirsizlik yok, soldan sağa taranır
Prefix (PN)
* + a b c
+ a * b c
/ * + a b - c d e
Lisp/Scheme'de doğal gösterim
Neden postfix? Postfix ifadeyi değerlendirmek son derece basittir: soldan sağa oku, operand görürsen stack'e koy, operatör görürsen iki operand pop et, sonucu push et. Paranteze, önceliğe veya ilişkilendirme yönüne (associativity) bakmanıza gerek yoktur. Bu yüzden HP hesap makineleri, Forth dili ve JVM bytecode hep postfix kullanır.
Operatör Öncelik Tablosu
Shunting Yard algoritması, infix ifadeyi postfix'e dönüştürürken hangi operatörün önce çıkacağına karar vermek için bir öncelik tablosu kullanır. Yüksek öncelikli operatör, düşük öncelikliden önce çıktıya yazılır.
| Operatör | Öncelik | İlişkilendirme | Açıklama |
|---|
^ | 3 (en yüksek) | Sağdan sola →← | Üs alma: 2^3^2 = 2^(3^2) = 512 |
* / | 2 | Soldan sağa ←→ | Çarpma, bölme |
+ - | 1 (en düşük) | Soldan sağa ←→ | Toplama, çıkarma |
( | 0 | — | Stack'te engel görevi görür |
İlişkilendirme (associativity) neden önemli? Aynı öncelikli iki operatör yan yana geldiğinde hangi yöne gruplanacağını belirler. a - b - c soldan sağa → (a-b)-c. Ama 2^3^2 sağdan sola → 2^(3^2) = 512, yoksa (2^3)^2 = 64 olurdu. Algoritma: sol ilişkilendirmede ≥ ile pop, sağ ilişkilendirmede sadece > ile pop.
Shunting Yard Algoritması
Edsger Dijkstra'nın 1961'de geliştirdiği bu algoritma, adını tren istasyonlarındaki manevra alanından alır: trenler (operatörler) yan hatlarda (stack) bekletilir, uygun sıra gelince ana hatta (output) yönlendirilir.
Shunting Yard — Kurallar:
1. Operand (sayı/değişken) → doğrudan çıktıya yaz.
2. Açma parantezi ( → stack'e push et.
3. Kapama parantezi ) → stack'ten ( görene kadar pop et, hepsini çıktıya yaz. ('i at.
4. Operatör → stack'in tepesindeki operatör daha yüksek öncelikli (veya aynı öncelikli + sol ilişkilendirmeli) olduğu sürece pop edip çıktıya yaz. Sonra gelen operatörü push et.
5. String bitince stack'te kalan tüm operatörleri çıktıya yaz.
Adım Adım — a + b * c
"a + b * c" → Postfix: "a b c * +"
1
a
→ çıktı
boş
a
Operand → doğrudan çıktı
3
b
→ çıktı
+
a b
Operand → doğrudan çıktı
4
*
push
+ *
a b
* (önc=2) > + (önc=1) → push
5
c
→ çıktı
+ *
a b c
Operand → doğrudan çıktı
6
SON
pop all
boş
a b c * +
Kalan operatörleri çıktıya
Sonuç: a b c * + → önce b*c hesaplanır, sonra a ile toplanır ✓
Adım Adım — (a + b) * c - d / e
"(a + b) * c - d / e" → Postfix: "a b + c * d e / -"
3
+
push
( +
a
Top = ( → engel, push
5
)
pop→(
boş
a b +
Kural 3: ( bulana kadar pop
6
*
push
*
a b +
Stack boş → push
8
-
pop *, push -
-
a b + c *
- (1) ≤ * (2) → pop *, push -
9
d
→ çıktı
-
a b + c * d
Operand
10
/
push
- /
a b + c * d
/ (2) > - (1) → push
11
e
→ çıktı
- /
a b + c * d e
Operand
12
SON
pop all
boş
a b + c * d e / -
Kalan: / sonra -
Sonuç: a b + c * d e / - → ((a+b)*c) - (d/e) ✓
Adım Adım — a ^ b ^ c (Sağ ilişkilendirme)
"a ^ b ^ c" → Postfix: "a b c ^ ^" (sağdan sola!)
4
^
push
^ ^
a b
^ sağ ilişk. → aynı önc. pop etmez
6
SON
pop all
boş
a b c ^ ^
İlk ^ sonra ikinci ^
Sonuç: a b c ^ ^ → a ^ (b ^ c) — sağdan sola doğru ✓
Eğer sol ilişkilendirmeli olsaydı: a b ^ c ^ → (a ^ b) ^ c olurdu
Tam C Implementasyonu
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAX 256
/* ── Stack ── */
typedef struct { char items[MAX]; int top; } CharStack;
void olustur(CharStack* s) { s->top = -1; }
int isEmpty(CharStack* s) { return s->top == -1; }
void push(CharStack* s, char c) { s->items[++s->top] = c; }
char pop(CharStack* s) { return s->items[s->top--]; }
char peek(CharStack* s) { return s->items[s->top]; }
/* ── Öncelik ── */
int oncelik(char op) {
switch (op) {
case '+': case '-': return 1;
case '*': case '/': return 2;
case '^': return 3;
default: return 0;
}
}
/* ── Sağ ilişkilendirmeli mi? ── */
int sagIliskilendirme(char op) {
return op == '^';
}
/* ════════════════════════════════
INFIX → POSTFIX (Shunting Yard)
════════════════════════════════ */
void infixToPostfix(const char* infix, char* postfix) {
CharStack s;
olustur(&s);
int j = 0; // postfix indeksi
for (int i = 0; infix[i] != '\0'; i++) {
char c = infix[i];
// Boşlukları atla
if (c == ' ') continue;
// Kural 1: Operand → çıktıya
if (isalnum(c)) {
postfix[j++] = c;
}
// Kural 2: ( → push
else if (c == '(') {
push(&s, c);
}
// Kural 3: ) → ( bulana kadar pop
else if (c == ')') {
while (!isEmpty(&s) && peek(&s) != '(')
postfix[j++] = pop(&s);
if (!isEmpty(&s)) pop(&s); // ( 'yi at
}
// Kural 4: Operatör
else {
while (!isEmpty(&s) && peek(&s) != '(' &&
(oncelik(peek(&s)) > oncelik(c) ||
(oncelik(peek(&s)) == oncelik(c) &&
!sagIliskilendirme(c))))
{
postfix[j++] = pop(&s);
}
push(&s, c);
}
}
// Kural 5: Kalan operatörleri boşalt
while (!isEmpty(&s))
postfix[j++] = pop(&s);
postfix[j] = '\0';
}Demo Program
int main() {
const char* testler[] = {
"a+b*c",
"(a+b)*c",
"(a+b)*c-d/e",
"a^b^c",
"a+b*(c^d-e)^(f+g*h)-i",
};
int n = sizeof(testler) / sizeof(testler[0]);
for (int i = 0; i < n; i++) {
char sonuc[MAX];
infixToPostfix(testler[i], sonuc);
printf("Infix: %s\n", testler[i]);
printf("Postfix: %s\n\n", sonuc);
}
return 0;
}Infix: a+b*c
Postfix: abc*+
Infix: (a+b)*c
Postfix: ab+c*
Infix: (a+b)*c-d/e
Postfix: ab+c*de/-
Infix: a^b^c
Postfix: abc^^
Infix: a+b*(c^d-e)^(f+g*h)-i
Postfix: abcd^e-fgh*+^*+i-
Karmaşıklık Analizi — Shunting Yard
| Ölçüt | Değer | Neden? |
|---|
| Zaman | O(n) | Her token tam bir kez okunur, her operatör en fazla bir kez push + bir kez pop |
| Bellek | O(n) | En kötü: tüm tokenler operatör → hepsi stack'te |
| Çıktı boyutu | O(n) | Parantezler hariç tüm tokenler çıktıya geçer |
Infix → Prefix Dönüşümü
Prefix notasyonu (Polonya Notasyonu), Lisp ve Scheme gibi dillerin doğal gösterim biçimidir. Infix'ten prefix'e dönüşüm, postfix dönüşümünün "ayna" versiyonudur: ifadeyi sağdan sola tarayarak aynı algoritmayı uygularsınız, ardından sonucu ters çevirirsiniz.
Algoritma
Infix → Prefix — 4 Adım:
1. Infix ifadeyi ters çevir. Çevirirken ( ile )'yi, [ ile ]'yi takas et.
2. Ters çevrilmiş ifadeye Shunting Yard uygula (postfix'e dönüştür).
• Tek fark: ilişkilendirme koşulunu ters çevir (sol ilişkilendirmede > kullan, ≥ değil).
3. Elde edilen postfix sonucu ters çevir.
4. Sonuç = Prefix ifade.
Adım Adım — (a + b) * c
"(a + b) * c" → Prefix: "* + a b c"
② Ters çevir + parantez takas:
( → ), ) → ( takası yapıldı
③ Shunting Yard → Postfix:
Tam C Implementasyonu
/* ── String'i ters çevir ── */
void tersCevir(char* str) {
int n = strlen(str);
for (int i = 0; i < n / 2; i++) {
char tmp = str[i];
str[i] = str[n - 1 - i];
str[n - 1 - i] = tmp;
}
}
/* ── Parantezleri takas et ── */
void parantezTakas(char* str) {
for (int i = 0; str[i]; i++) {
if (str[i] == '(') str[i] = ')';
else if (str[i] == ')') str[i] = '(';
}
}
/* ════════════════════════════════
INFIX → PREFIX
════════════════════════════════ */
void infixToPrefix(const char* infix, char* prefix) {
char temp[MAX];
strcpy(temp, infix);
// 1. Ters çevir
tersCevir(temp);
// 2. Parantezleri takas et
parantezTakas(temp);
// 3. Shunting Yard uygula (postfix'e dönüştür)
char postfix[MAX];
infixToPostfix(temp, postfix);
// 4. Sonucu ters çevir → prefix
tersCevir(postfix);
strcpy(prefix, postfix);
}
// Kullanım:
char sonuc[MAX];
infixToPrefix("(a+b)*c", sonuc);
printf("%s\n", sonuc); // *+abcÜç Notasyon — Hızlı Referans
| Infix | Postfix | Prefix |
|---|
a + b | a b + | + a b |
a + b * c | a b c * + | + a * b c |
(a + b) * c | a b + c * | * + a b c |
a + b * c - d | a b c * + d - | - + a * b c d |
a * (b + c) / d | a b c + * d / | / * a + b c d |
a ^ b ^ c | a b c ^ ^ | ^ a ^ b c |
Bir sonraki adım: Artık infix'i postfix'e dönüştürebiliyoruz. Sonraki bölümde postfix ifadeyi stack ile değerlendireceğiz (evaluate) — yani gerçek bir hesap makinesi motoru inşa edeceğiz. Ardından stack'in günlük hayattaki en görünür uygulamasını inceleyeceğiz: Undo/Redo mekanizması.
Postfix İfade Değerlendirme
Önceki bölümde infix ifadeyi postfix'e dönüştürdük. Şimdi sıra postfix ifadeyi gerçekten hesaplamakta. Postfix değerlendirme, stack'in en zarif uygulamalarından biridir: parantez yok, öncelik kuralı yok, sadece soldan sağa okuyup stack'e koy veya hesapla.
Algoritma
Postfix Değerlendirme — Kurallar:
1. Postfix ifadeyi soldan sağa tara.
2. Token bir operand (sayı) ise → stack'e push et.
3. Token bir operatör ise →
• İki operand pop et: önce sağ (b), sonra sol (a).
• Hesapla: sonuç = a operatör b
• Sonucu stack'e push et.
4. İfade bittiğinde stack'te kalan tek eleman = sonuç.
Pop sırası kritik! Çıkarma ve bölmede sıra önemlidir: a b - demek a - b demektir. Önce pop edilen b (sağ operand), sonra pop edilen a (sol operand). Yani sonuç = ikinci_pop OP ilk_pop. Sırayı karıştırırsanız b - a hesaplarsınız.
Adım Adım — 3 4 + 2 * 7 -
Bu ifade infix'te (3 + 4) * 2 - 7 ifadesine karşılık gelir. Beklenen sonuç: (3+4)×2−7 = 7.
"3 4 + 2 * 7 -" → Sonuç: 7
3
+
pop 4,3 → hesapla
7
3 + 4 = 7 → push
5
*
pop 2,7 → hesapla
14
7 * 2 = 14 → push
7
-
pop 7,14 → hesapla
7
14 - 7 = 7 → push
Stack'te tek eleman kaldı: 7 = Sonuç ✓
Adım Adım — 5 1 2 + 4 * + 3 -
Infix karşılığı: 5 + (1 + 2) * 4 - 3. Beklenen sonuç: 5 + 3×4 − 3 = 14.
"5 1 2 + 4 * + 3 -" → Sonuç: 14
Sonuç: 14 ✓
Tam C Implementasyonu
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#define MAX 256
/* ── Stack (double) ── */
typedef struct { double items[MAX]; int top; } NumStack;
void nOlustur(NumStack* s) { s->top = -1; }
int nIsEmpty(NumStack* s) { return s->top == -1; }
void nPush(NumStack* s, double v) { s->items[++s->top] = v; }
double nPop(NumStack* s) { return s->items[s->top--]; }
/* ── Operatör mü? ── */
int operatorMu(char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '^';
}
/* ── İşlemi uygula ── */
double hesapla(double a, double b, char op) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if (b == 0) { fprintf(stderr, "Sıfıra bölme!\n"); exit(1); }
return a / b;
case '^': return pow(a, b);
default: return 0;
}
}
/* ════════════════════════════════
POSTFİX DEĞERLENDİRME
════════════════════════════════ */
double postfixDegerlendir(const char* ifade) {
NumStack s;
nOlustur(&s);
for (int i = 0; ifade[i] != '\0'; i++) {
char c = ifade[i];
// Boşlukları atla
if (c == ' ') continue;
// Sayı → stack'e push
if (isdigit(c)) {
double sayi = 0;
while (isdigit(ifade[i])) {
sayi = sayi * 10 + (ifade[i] - '0');
i++;
}
i--; // for döngüsü de artıracak
nPush(&s, sayi);
}
// Operatör → iki operand pop et, hesapla, push et
else if (operatorMu(c)) {
double b = nPop(&s); // ← önce sağ operand
double a = nPop(&s); // ← sonra sol operand
double sonuc = hesapla(a, b, c);
nPush(&s, sonuc);
}
}
return nPop(&s); // son eleman = sonuç
}Demo Program
int main() {
const char* testler[] = {
"3 4 + 2 * 7 -", // (3+4)*2-7 = 7
"5 1 2 + 4 * + 3 -", // 5+(1+2)*4-3 = 14
"2 3 ^ 4 +", // 2^3+4 = 12
"15 7 1 1 + - / 3 * 2 1 1 + + -", // karmaşık = 5
};
int n = sizeof(testler) / sizeof(testler[0]);
for (int i = 0; i < n; i++) {
double sonuc = postfixDegerlendir(testler[i]);
printf("Postfix: %-35s → %.2f\n", testler[i], sonuc);
}
return 0;
}Postfix: 3 4 + 2 * 7 - → 7.00
Postfix: 5 1 2 + 4 * + 3 - → 14.00
Postfix: 2 3 ^ 4 + → 12.00
Postfix: 15 7 1 1 + - / 3 * 2 1 1 + + - → 5.00
Tam Boru Hattı: Infix → Postfix → Sonuç
Önceki iki bölümü birleştirerek tam bir hesap makinesi motoru oluşturabiliriz. İnsan tarafından girilen infix ifade, önce postfix'e dönüştürülür, ardından değerlendirilir:
Hesap Makinesi Pipeline'ı
(3+4)*2-7
→
Shunting Yard
infixToPostfix()
→
3 4 + 2 * 7 -
→
Stack Eval
postfixDegerlendir()
→
7
Karmaşıklık Analizi
| Ölçüt | Değer | Neden? |
|---|
| Zaman | O(n) | Her token bir kez okunur, her sayı en fazla bir kez push + pop |
| Bellek | O(n) | En kötü: tüm tokenler operand → hepsi stack'te |
| Pratik bellek | Genellikle O(n/2) | Operandlar yaklaşık tokenların yarısı |
Undo / Redo Mekanizması
Stack'in günlük hayatta en çok karşılaştığımız uygulaması: geri alma (undo) ve yineleme (redo). Metin editörlerinden grafik programlarına, tarayıcılardan dosya yöneticilerine kadar neredeyse her interaktif uygulama bu mekanizmayı kullanır. Temel prensip: iki stack, biri geçmişi, diğeri "geleceği" tutar.
Çift Stack Modeli
Undo / Redo — İki Stack Mimarisi
Undo Stack (geçmiş)
Yaz "M"
Yaz "e"
Yaz "r"
Yaz "h"
← en son yapılan işlem tepede
Redo Stack (gelecek)
Undo yapılınca buraya taşınır
Kurallar:
1. Yeni işlem → Undo stack'e push et. Redo stack'i temizle (yeni dal açıldı).
2. Undo → Undo stack'ten pop et, işlemi ters çevir, Redo stack'e push et.
3. Redo → Redo stack'ten pop et, işlemi tekrar uygula, Undo stack'e push et.
4. Undo stack boşsa → geri alınacak bir şey yok.
5. Redo stack boşsa → yinelenecek bir şey yok.
Adım Adım Senaryo
Bir metin editöründe "Mer" yazıp, undo ile geri alıp, redo ile tekrar yaptığımızı düşünelim:
Yazma → Undo → Undo → Redo Senaryosu
③ Yaz "r" — yeni işlem
Undo: [M, e, r]
Redo: [boş]
Ekran: Mer
⑥ REDO — "e" tekrar uygulandı
⑦ Yaz "s" — yeni işlem (redo stack temizlenir!)
Undo: [M, e, s]
Redo: [boş] ← temizlendi!
Ekran: Mes
Yeni işlem yapıldığında redo geçmişi silinir — "r" artık geri getirilemez
Tam C Implementasyonu
#include <stdio.h>
#include <string.h>
#define MAX 100
/* ── Karakter Stack ── */
typedef struct { char items[MAX]; int top; } CStack;
void csOlustur(CStack* s) { s->top = -1; }
int csIsEmpty(CStack* s) { return s->top == -1; }
void csPush(CStack* s, char c) { s->items[++s->top] = c; }
char csPop(CStack* s) { return s->items[s->top--]; }
void csTemizle(CStack* s) { s->top = -1; }
/* ── Editör Durumu ── */
typedef struct {
char metin[MAX]; // mevcut metin
int uzunluk; // metin uzunluğu
CStack undo; // geri alma yığını
CStack redo; // yineleme yığını
} Editor;
void editorOlustur(Editor* e) {
e->metin[0] = '\0';
e->uzunluk = 0;
csOlustur(&e->undo);
csOlustur(&e->redo);
}
/* ── Karakter yaz ── */
void yaz(Editor* e, char c) {
e->metin[e->uzunluk++] = c;
e->metin[e->uzunluk] = '\0';
csPush(&e->undo, c);
csTemizle(&e->redo); // yeni dal → redo sıfırla
printf(" Yaz '%c' → \"%s\"\n", c, e->metin);
}
/* ── Geri al ── */
void geriAl(Editor* e) {
if (csIsEmpty(&e->undo)) {
printf(" Undo: geri alınacak işlem yok\n");
return;
}
char c = csPop(&e->undo);
e->metin[--e->uzunluk] = '\0';
csPush(&e->redo, c);
printf(" Undo '%c' → \"%s\"\n", c, e->metin);
}
/* ── Yinele ── */
void yinele(Editor* e) {
if (csIsEmpty(&e->redo)) {
printf(" Redo: yinelenecek işlem yok\n");
return;
}
char c = csPop(&e->redo);
e->metin[e->uzunluk++] = c;
e->metin[e->uzunluk] = '\0';
csPush(&e->undo, c);
printf(" Redo '%c' → \"%s\"\n", c, e->metin);
}Demo Program
int main() {
Editor e;
editorOlustur(&e);
printf("=== Yazma ===\n");
yaz(&e, 'M');
yaz(&e, 'e');
yaz(&e, 'r');
printf("\n=== Undo ===\n");
geriAl(&e);
geriAl(&e);
printf("\n=== Redo ===\n");
yinele(&e);
printf("\n=== Yeni yazma (redo silinir) ===\n");
yaz(&e, 's');
yinele(&e); // redo boş
return 0;
}=== Yazma ===
Yaz 'M' → "M"
Yaz 'e' → "Me"
Yaz 'r' → "Mer"
=== Undo ===
Undo 'r' → "Me"
Undo 'e' → "M"
=== Redo ===
Redo 'e' → "Me"
=== Yeni yazma (redo silinir) ===
Yaz 's' → "Mes"
Redo: yinelenecek işlem yok
Gerçek uygulamalarda: Yukarıdaki örnek basitleştirilmiş bir versiyondur — her işlem tek bir karakter ekleme/silme. Gerçek editörlerde (VS Code, Photoshop vb.) her "işlem" bir komut nesnesidir (Command Pattern) ve sadece metni değil silme, biçimlendirme, taşıma gibi karmaşık işlemleri de kapsar. Ama temel yapı aynıdır: iki stack, birinden çıkan diğerine girer.
Dört Uygulama — Özet Tablosu
| Uygulama | Stack'te ne var? | Push ne zaman? | Pop ne zaman? | Karmaşıklık |
|---|
| Parantez Dengeleme | Açma parantezleri | ( [ { geldiğinde | ) ] } geldiğinde | O(n) zaman, O(n) bellek |
| Infix → Postfix | Operatörler ve ( | Operatör veya ( geldiğinde | Daha yüksek/eşit öncelikli operatör geldiğinde | O(n) zaman, O(n) bellek |
| Postfix Değerlendirme | Operandlar (sayılar) | Sayı okunduğunda | Operatör okunduğunda (2 pop) | O(n) zaman, O(n) bellek |
| Undo / Redo | İşlemler (komutlar) | Yeni işlem yapıldığında | Ctrl+Z (undo) veya Ctrl+Y (redo) | O(1) per işlem |
Ortak kalıp: Dört uygulamanın hepsinde aynı yapı tekrar eder — bir şeyi geçici olarak stack'e koy, uygun koşul oluştuğunda çıkar ve işle. Fark sadece ne koyulduğu (parantez, operatör, sayı, komut), ne zaman çıkarıldığı (eşleşme, öncelik, operatör, kullanıcı isteği) ve çıkarılanla ne yapıldığıdır (karşılaştırma, çıktıya yazma, hesaplama, geri alma).
Sonuç
Stack basit bir veri yapısıdır — sadece push ve pop. Ama bu iki operasyon, LIFO prensibi ile birleştiğinde şaşırtıcı derecede güçlü çözümler üretir. Parantez dengeleme derleyicilerin temelini oluşturur. Shunting Yard algoritması ifade ayrıştırma motorlarının kalbindedir. Postfix değerlendirme hesap makinelerini ve JVM'i çalıştırır. Undo mekanizması her interaktif uygulamanın vazgeçilmezidir.
Bu dört uygulamanın ötesinde, stack call stack (fonksiyon çağrı yığını), DFS (derinlik öncelikli arama), string ters çevirme, tarayıcı geri butonu ve recursion'ın iteratif hale dönüştürülmesinde de kritik rol oynar. Stack'i anlamak, bilgisayar biliminin temel yapı taşlarından birini kavramak demektir.
Bir sonraki bölümde stack'in kardeş veri yapısına geçeceğiz: Queue (Kuyruk) — FIFO prensibi, circular queue, priority queue ve BFS uygulamaları.
Bölüm 4.3 tamamlandı. Stack uygulamalarını öğrendik: parantez dengeleme (compiler), infix↔postfix↔prefix dönüşümleri (Shunting Yard), postfix değerlendirme (hesap makinesi motoru) ve undo/redo (çift stack). Tüm uygulamaların ortak kalıbı: stack'e koy → koşul oluşunca çıkar → işle.