🔄 3.2: Normal Queue Problemi ve Circular Queue

Neden Dairesel Kuyruk Gerekli?

🤔 Önce Normal (Linear) Queue'yu Anlayalım

Bir diziyi queue olarak kullandığımızda head (baş) ve tail (kuyruk) pointer'ları kullanırız:

Normal (Linear) Queue - Problem Gösterimi
import numpy as np

class LinearQueue:
    """
    Normal (Dairesel Olmayan) Queue
    Problem: tail sona ulaştığında, başta yer olsa bile ekleme yapılamaz!
    """
    def __init__(self, capacity):
        self.capacity = capacity
        self.data = np.zeros(capacity, dtype=int)  # Sabit boyutlu numpy dizisi
        self.head = 0   # Çıkış noktası (dequeue buradan)
        self.tail = 0   # Giriş noktası (enqueue buraya)
        self.size = 0   # Mevcut eleman sayısı

    def enqueue(self, val):
        # ❌ PROBLEM: tail dizinin sonuna ulaştıysa,
        # baş tarafta boş yer olsa bile ekleme yapamayız!
        if self.tail == self.capacity:
            print(f"❌ Dolu! tail={self.tail} (Dizinin sonuna ulaşıldı)")
            print(f"   Başta {self.head} boş hücre var ama kullanamıyoruz!")
            return False

        self.data[self.tail] = val
        self.tail += 1  # Sadece artırıyoruz, modülo YOK
        self.size += 1
        print(f"✅ Enqueue({val}) -> Dizi: {self.data}, head={self.head}, tail={self.tail}")
        return True

    def dequeue(self):
        if self.size == 0:
            print("❌ Boş!")
            return None

        val = self.data[self.head]
        self.data[self.head] = 0  # Görsel temizlik (zorunlu değil)
        self.head += 1  # Sadece artırıyoruz, modülo YOK
        self.size -= 1
        print(f"🔻 Dequeue() -> {val}, Dizi: {self.data}, head={self.head}, tail={self.tail}")
        return val


# ===== PROBLEMİ GÖSTEREN SENARYO =====
print("=" * 60)
print("📌 NORMAL QUEUE PROBLEMİ")
print("=" * 60)

q = LinearQueue(5)

# Adım 1: 3 eleman ekle
print("\n1️⃣ Üç eleman ekliyoruz:")
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)

# Adım 2: 2 eleman çıkar
print("\n2️⃣ İki eleman çıkarıyoruz:")
q.dequeue()  # 10 çıkar
q.dequeue()  # 20 çıkar

# Adım 3: 2 eleman daha ekle
print("\n3️⃣ İki eleman daha ekliyoruz:")
q.enqueue(40)
q.enqueue(50)

# Adım 4: Bir eleman daha eklemeye çalış
print("\n4️⃣ Bir eleman daha eklemeye çalışıyoruz:")
q.enqueue(60)  # ❌ BAŞARISIZ! Başta yer var ama kullanamıyoruz!

print("\n" + "=" * 60)
print("📊 SONUÇ:")
print(f"   Dizide sadece {q.size} eleman var")
print(f"   Ama {q.head} hücre boşa gitti (kullanılamıyor)!")
print("   Bu hafıza israfıdır ve 'false overflow' denir.")
print("=" * 60)
Çıktı bekleniyor...

🎯 Problemi Görselleştirelim

❌ Normal Queue Problemi: "False Overflow"

Başlangıç durumu (3 eleman eklendi):

10
20
30
-
-
head=0, tail=3

2 eleman çıkarıldıktan sonra:

💀
💀
30
40
50
head=2, tail=5 (dizinin sonu!)

⚠️ tail=5 oldu ama dizi boyutu 5. Başta 2 hücre boş olmasına rağmen yeni eleman ekleyemiyoruz!

✅ Çözüm: Circular Queue (Modülo ile)

Pointer'ları % capacity ile sarmalayarak dizinin başına dönebiliriz:

tail = (tail + 1) % capacity  # 5 % 5 = 0 (başa döner!)
head = (head + 1) % capacity
            

Bu sayede dizi dairesel olarak kullanılır ve hafıza israfı önlenir.

🔄 Circular Queue Görsel Simülasyonu

Head: 0, Tail: 0

💻 Circular Queue - Numpy Implementasyonu

Python Kodu
import numpy as np

class CircularQueue:
    def __init__(self, capacity):
        self.capacity = capacity
        self.data = np.zeros(capacity, dtype=int)
        self.head = 0
        self.tail = 0
        self.size = 0
        
    def enqueue(self, val):
        if self.size == self.capacity:
            print("❌ Gerçekten dolu! (Tüm hücreler kullanımda)")
            return False
        self.data[self.tail] = val
        # 🔑 MODÜLO İLE SARMALAMA - dizinin başına döner!
        self.tail = (self.tail + 1) % self.capacity
        self.size += 1
        print(f"✅ Enqueue({val}) -> Dizi: {self.data}, head={self.head}, tail={self.tail}")
        return True

    def dequeue(self):
        if self.size == 0:
            print("❌ Boş!")
            return None
        val = self.data[self.head]
        self.data[self.head] = 0
        # 🔑 MODÜLO İLE SARMALAMA
        self.head = (self.head + 1) % self.capacity
        self.size -= 1
        print(f"🔻 Dequeue() -> {val}, Dizi: {self.data}, head={self.head}, tail={self.tail}")
        return val

# ===== CIRCULAR QUEUE'NUN AVANTAJINI GÖSTER =====
print("=" * 60)
print("📌 CIRCULAR QUEUE - AYNI SENARYO")
print("=" * 60)

q = CircularQueue(5)

print("\n1️⃣ Üç eleman ekliyoruz:")
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)

print("\n2️⃣ İki eleman çıkarıyoruz:")
q.dequeue()
q.dequeue()

print("\n3️⃣ İki eleman daha ekliyoruz:")
q.enqueue(40)
q.enqueue(50)

print("\n4️⃣ Şimdi bir eleman daha eklemeye çalışalım:")
q.enqueue(60)  # ✅ BAŞARILI! tail=0'a döndü!

print("\n5️⃣ Hatta bir tane daha ekleyelim:")
q.enqueue(70)  # ✅ BAŞARILI! tail=1'e geçti!

print("\n" + "=" * 60)
print("📊 SONUÇ:")
print(f"   Circular Queue ile tüm {q.capacity} hücre kullanılabilir!")
print(f"   Mevcut eleman sayısı: {q.size}")
print("   Hafıza israfı YOK!")
print("=" * 60)
Çıktı bekleniyor...

📊 Linear vs Circular Queue Karşılaştırması

Özellik Linear Queue Circular Queue
Hafıza Kullanımı ❌ İsraf var (false overflow) ✅ Tam verimli
Pointer Güncelleme tail++ tail = (tail+1) % cap
Doluluk Kontrolü tail == capacity size == capacity
Zaman Karmaşıklığı O(1) O(1)

🔢 Modülo (%) Operatörü Nasıl Çalışır?

Modülo, bölme işleminin kalanını verir ve circular yapılar için mükemmeldir:

# Kapasite = 5 iken:
0 % 5 = 0  # Başlangıç
1 % 5 = 1
2 % 5 = 2
3 % 5 = 3
4 % 5 = 4
5 % 5 = 0  # ← Tekrar başa döndü!
6 % 5 = 1
7 % 5 = 2
...

# Bu sayede dizi hiçbir zaman "taşmaz",
# sadece başa döner (wrap-around)

Gerçek hayat analojisi: Saat kadranı! 12'den sonra 1'e döner (12 % 12 = 0, sonra 1, 2, 3...).