go
Veri Yapıları dersleri

Kuyruk (Queue) ve Deque

FIFO, dairesel tampon, çift uçlu kuyruk ve uygulamaları.

Ders 5 / 1425 dkBaşlangıç
Bu derste öğreneceklerin
  • FIFO prensibi
  • Slice tabanlı kuyruk ve bellek tuzakları
  • Dairesel tampon (ring buffer)
  • Çift uçlu kuyruk (deque)
  • Kanallar ile kuyruk
  • Kayan pencere maksimumu (monoton deque)

Markette kasaya ilk gelen ilk hizmet alır. Yazıcıya gönderilen ilk belge ilk basılır. Bir sunucuya gelen ilk istek ilk işlenir. Bu adalet fikri — ilk giren ilk çıkar (FIFO — First In, First Out) — kuyruk veri yapısının tanımıdır ve yığının tam tersidir.

Kuyruk, iş parçacıklarının birbirine iş devrettiği her yerde karşına çıkar: görev kuyrukları, mesaj kuyrukları, olay döngüleri, yazıcı kuyrukları, ağ tamponları. Algoritmalar dünyasında da vazgeçilmezdir: Genişlik öncelikli arama (BFS), bir grafı katman katman gezmek için kuyruk kullanır ve bu, en kısa yol problemlerinin temelini oluşturur.

İlk bakışta kuyruk yazmak kolay görünür — "dilimin başından al, sonuna ekle" — ama bu naif yaklaşımın ciddi bir bellek tuzağı vardır. Bu derste FIFO prensibini, dilim tabanlı kuyruğun tuzağını ve çözümünü, sabit bellekli dairesel tamponu, iki uçtan da çalışan deque yapısını, Go'nun kanallarıyla kuyruk kurmayı ve monoton deque ile kayan pencere maksimumu problemini öğreneceksin.

FIFO ve temel işlemler

                  kuyruk
  çıkış ◄── ┌────┬────┬────┬────┐ ◄── giriş
            │ 10 │ 20 │ 30 │ 40 │
            └────┴────┴────┴────┘
             ön                arka
             (front)          (back)

Enqueue(50) → arkaya ekler:  [10 20 30 40 50]
Dequeue()   → önden alır:    döndürür 10, kalır [20 30 40 50]
İşlemAnlamıHedef karmaşıklık
Enqueue(v)Arkaya ekleO(1)
Dequeue()Önden al ve silO(1)
Front()Öndekine bakO(1)
Len()Eleman sayısıO(1)
ZamanO(1)AlanO(n)

Aşağıdaki görselleştirmede kuyruk kipini seçerek elemanların bir uçtan girip diğerinden çıktığını izleyebilirsin:

Dilim tabanlı kuyruk ve bellek tuzağı

En akla yatkın uygulama şudur: Arkaya append ile ekle, önden q = q[1:] ile al. İkisi de O(1) görünür ve doğrudur — ama burada gizli bir sorun vardır.

başlangıç dizisi (kapasite 8):
[10][20][30][40][ ][ ][ ][ ]
 ▲ dilim buradan başlıyor

q = q[1:] üç kez:
[10][20][30][40][ ][ ][ ][ ]
              ▲ dilim artık buradan başlıyor

10, 20, 30 hâlâ bellekte duruyor ama erişilemez.
Dilim baştan kaydıkça altta yatan dizi asla küçülmez!

q[1:] işlemi yeni bir dizi ayırmaz; yalnızca başlangıç işaretçisini ilerletir. Bu, tek başına sorun değildir; asıl sorun, dilimin önündeki alanın bir daha kullanılamamasıdır. Uzun ömürlü bir kuyrukta milyonlarca eleman geçerse, altta yatan dizi sürekli büyür ve serbest bırakılmayan bir bellek yığını oluşur.

main.go
package main

import "fmt"

// naiveQueue: q = q[1:] kullanır — kapasite hiç geri kazanılmaz
type naiveQueue []int

func (q *naiveQueue) Enqueue(v int) { *q = append(*q, v) }

func (q *naiveQueue) Dequeue() (int, bool) {
	if len(*q) == 0 {
		return 0, false
	}
	v := (*q)[0]
	*q = (*q)[1:] // baş işaretçisi ilerler; öndeki bellek boşa gider
	return v, true
}

// Queue: ön indeksi tutar ve gerektiğinde sıkıştırır
type Queue[T any] struct {
	items []T
	head  int // ilk geçerli elemanın indeksi
}

func (q *Queue[T]) Enqueue(v T) {
	q.items = append(q.items, v)
}

func (q *Queue[T]) Dequeue() (T, bool) {
	var zero T
	if q.head >= len(q.items) {
		return zero, false
	}
	v := q.items[q.head]
	q.items[q.head] = zero // işaretçiyi serbest bırak
	q.head++

	// Ön yarıdan fazlası boşaldıysa sıkıştır: amortize O(1)
	if q.head > len(q.items)/2 {
		n := copy(q.items, q.items[q.head:])
		q.items = q.items[:n]
		q.head = 0
	}
	return v, true
}

func (q *Queue[T]) Len() int { return len(q.items) - q.head }

func main() {
	// Naif sürümün kapasitesi büyümeye devam eder
	var naive naiveQueue
	for i := range 1000 {
		naive.Enqueue(i)
		naive.Dequeue()
	}
	fmt.Println("naif kuyruk — uzunluk:", len(naive), "kapasite:", cap(naive))

	// Sıkıştıran sürüm kapasiteyi geri kazanır
	var q Queue[int]
	for i := range 1000 {
		q.Enqueue(i)
		q.Dequeue()
	}
	fmt.Println("sıkıştıran kuyruk — uzunluk:", q.Len(), "kapasite:", cap(q.items))

	// Doğruluk kontrolü: FIFO sırası korunuyor mu
	var fifo Queue[string]
	for _, s := range []string{"birinci", "ikinci", "üçüncü"} {
		fifo.Enqueue(s)
	}
	for fifo.Len() > 0 {
		v, _ := fifo.Dequeue()
		fmt.Print(v, " ")
	}
	fmt.Println()
	_, ok := fifo.Dequeue()
	fmt.Println("boş kuyruktan alınabildi mi:", ok)
}
Çıktı
naif kuyruk — uzunluk: 0 kapasite: 0
sıkıştıran kuyruk — uzunluk: 0 kapasite: 1
birinci ikinci üçüncü
boş kuyruktan alınabildi mi: false

Sıkıştırma stratejisi burada kritiktir: Her Dequeue sonrasında sıkıştırmak O(n) maliyet getirirdi. Yalnızca boş alan kuyruğun yarısını geçtiğinde sıkıştırmak, işi amortize O(1)'de tutar — dinamik dizideki katlayarak büyüme fikrinin aynısı, ters yönde.

Dairesel tampon (ring buffer)

Kuyruğun boyutu önceden biliniyorsa çok daha zarif bir çözüm vardır: Sabit bir diziyi halka gibi kullanmak. Sona ulaşan indeks başa döner, böylece ne kaydırma ne de yeniden ayırma gerekir.

kapasite 6, head=2, count=3

indeks:  0     1     2     3     4     5
       [ - ] [ - ] [ A ] [ B ] [ C ] [ - ]

                    head          tail = (head+count) % 6 = 5

Enqueue(D) → 5. konuma yaz, count=4
Enqueue(E) → (2+4)%6 = 0. konuma yaz (başa sardı!), count=5

       [ E ] [ - ] [ A ] [ B ] [ C ] [ D ]
               ▲ tail        head
main.go
package main

import (
	"errors"
	"fmt"
)

var ErrFull = errors.New("tampon dolu")

type RingBuffer[T any] struct {
	data  []T
	head  int // ilk elemanın indeksi
	count int // eleman sayısı
}

func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
	return &RingBuffer[T]{data: make([]T, max(1, capacity))}
}

func (r *RingBuffer[T]) Cap() int     { return len(r.data) }
func (r *RingBuffer[T]) Len() int     { return r.count }
func (r *RingBuffer[T]) IsFull() bool { return r.count == len(r.data) }

// Enqueue: O(1), tampon doluysa hata döner
func (r *RingBuffer[T]) Enqueue(v T) error {
	if r.IsFull() {
		return ErrFull
	}
	tail := (r.head + r.count) % len(r.data)
	r.data[tail] = v
	r.count++
	return nil
}

// Push: doluysa en eskiyi atarak yazar (kayan pencere günlükleri için)
func (r *RingBuffer[T]) Push(v T) (evicted T, didEvict bool) {
	if r.IsFull() {
		evicted, _ = r.Dequeue()
		didEvict = true
	}
	_ = r.Enqueue(v)
	return evicted, didEvict
}

// Dequeue: O(1)
func (r *RingBuffer[T]) Dequeue() (T, bool) {
	var zero T
	if r.count == 0 {
		return zero, false
	}
	v := r.data[r.head]
	r.data[r.head] = zero
	r.head = (r.head + 1) % len(r.data)
	r.count--
	return v, true
}

func (r *RingBuffer[T]) Slice() []T {
	out := make([]T, 0, r.count)
	for i := range r.count {
		out = append(out, r.data[(r.head+i)%len(r.data)])
	}
	return out
}

func main() {
	rb := NewRingBuffer[int](5)

	for i := 1; i <= 5; i++ {
		_ = rb.Enqueue(i)
	}
	fmt.Println("dolu tampon:", rb.Slice(), "kapasite:", rb.Cap())
	fmt.Println("bir daha eklemeyi dene:", rb.Enqueue(6))

	v, _ := rb.Dequeue()
	fmt.Println("çıkan:", v, "→", rb.Slice())

	_ = rb.Enqueue(6) // 6, dizinin başına sarar
	fmt.Println("6 eklendi:", rb.Slice())

	// Son N kaydı tutan günlük tamponu
	log := NewRingBuffer[string](3)
	for _, line := range []string{"başladı", "bağlandı", "sorgu", "yanıt", "kapandı"} {
		if old, did := log.Push(line); did {
			fmt.Printf("  %q atıldı\n", old)
		}
	}
	fmt.Println("son 3 kayıt:", log.Slice())
}
Çıktı
dolu tampon: [1 2 3 4 5] kapasite: 5
bir daha eklemeyi dene: tampon dolu
çıkan: 1 → [2 3 4 5]
6 eklendi: [2 3 4 5 6]
  "başladı" atıldı
  "bağlandı" atıldı
son 3 kayıt: [sorgu yanıt kapandı]

Dairesel tamponun en güzel yanı, bellek kullanımının sabit olmasıdır: Ne kadar veri geçerse geçsin, tek bir dizi kullanılır. Bu yüzden gömülü sistemlerde, ses/video tamponlarında, ağ paket kuyruklarında ve "son N kaydı tut" tarzı günlük yapılarında standart çözümdür.

Dikkat edilecek nokta, doluluk durumunun nasıl anlaşılacağıdır. Yalnızca head ve tail tutulursa, boş ve dolu durumlar aynı görünür (head == tail). Burada ayrı bir count alanı tutarak bu belirsizliği ortadan kaldırdık.

Çift uçlu kuyruk (deque)

Deque (double-ended queue), her iki uçtan da ekleme ve çıkarma yapabildiğin yapıdır. Böylece hem yığın hem kuyruk gibi davranabilir; ayrıca kayan pencere algoritmalarının temel aracıdır.

main.go
package main

import "fmt"

type Deque[T any] struct {
	items []T
	head  int
}

func (d *Deque[T]) Len() int { return len(d.items) - d.head }

func (d *Deque[T]) PushBack(v T) {
	d.items = append(d.items, v)
}

func (d *Deque[T]) PushFront(v T) {
	if d.head > 0 { // önde yer varsa kaydırma yapma
		d.head--
		d.items[d.head] = v
		return
	}
	d.items = append([]T{v}, d.items...) // yer yoksa yeniden ayır
}

func (d *Deque[T]) PopFront() (T, bool) {
	var zero T
	if d.Len() == 0 {
		return zero, false
	}
	v := d.items[d.head]
	d.items[d.head] = zero
	d.head++
	if d.head > len(d.items)/2 {
		n := copy(d.items, d.items[d.head:])
		d.items = d.items[:n]
		d.head = 0
	}
	return v, true
}

func (d *Deque[T]) PopBack() (T, bool) {
	var zero T
	if d.Len() == 0 {
		return zero, false
	}
	last := len(d.items) - 1
	v := d.items[last]
	d.items[last] = zero
	d.items = d.items[:last]
	return v, true
}

func (d *Deque[T]) Front() (T, bool) {
	var zero T
	if d.Len() == 0 {
		return zero, false
	}
	return d.items[d.head], true
}

func (d *Deque[T]) Back() (T, bool) {
	var zero T
	if d.Len() == 0 {
		return zero, false
	}
	return d.items[len(d.items)-1], true
}

func (d *Deque[T]) Slice() []T { return d.items[d.head:] }

func main() {
	var d Deque[string]

	d.PushBack("b")
	d.PushBack("c")
	d.PushFront("a")
	fmt.Println("deque:", d.Slice(), "uzunluk:", d.Len())

	front, _ := d.Front()
	back, _ := d.Back()
	fmt.Println("ön:", front, "arka:", back)

	v1, _ := d.PopFront()
	v2, _ := d.PopBack()
	fmt.Println("önden:", v1, "arkadan:", v2, "→", d.Slice())

	// Yığın gibi kullanım (tek uçtan)
	var stackLike Deque[int]
	for i := 1; i <= 3; i++ {
		stackLike.PushBack(i)
	}
	fmt.Print("yığın gibi: ")
	for stackLike.Len() > 0 {
		v, _ := stackLike.PopBack()
		fmt.Print(v, " ")
	}
	fmt.Println()

	// Kuyruk gibi kullanım
	var queueLike Deque[int]
	for i := 1; i <= 3; i++ {
		queueLike.PushBack(i)
	}
	fmt.Print("kuyruk gibi: ")
	for queueLike.Len() > 0 {
		v, _ := queueLike.PopFront()
		fmt.Print(v, " ")
	}
	fmt.Println()
}
Çıktı
deque: [a b c] uzunluk: 3
ön: a arka: c
önden: a arkadan: c → [b]
yığın gibi: 3 2 1
kuyruk gibi: 1 2 3

PushFront bu uygulamada ön tarafta yer varsa O(1), yoksa O(n)'dir. Gerçek bir deque, dairesel tampon üzerine kurularak her iki uçta da kesin O(1) sağlar; yukarıdaki sürüm okunabilirlik için sadeleştirilmiştir.

Kanallarla kuyruk

Go'da eşzamanlı bir kuyruğa ihtiyacın varsa, kendi kilitli yapını yazmadan önce kanalları düşün. Tamponlu bir kanal, tam olarak sınırlı kapasiteli, goroutine-güvenli bir FIFO kuyruğudur.

main.go
package main

import (
	"fmt"
	"sort"
	"sync"
)

type Job struct {
	ID    int
	Input int
}

func main() {
	// Tamponlu kanal = eşzamanlı, sınırlı kapasiteli FIFO kuyruk
	jobs := make(chan Job, 10)
	results := make(chan int, 10)

	var wg sync.WaitGroup
	for range 3 { // üç işçi aynı kuyruktan besleniyor
		wg.Go(func() {
			for job := range jobs { // kanal kapanınca döngü biter
				results <- job.Input * job.Input
			}
		})
	}

	for i := 1; i <= 8; i++ {
		jobs <- Job{ID: i, Input: i}
	}
	close(jobs)
	wg.Wait()
	close(results)

	var out []int
	for r := range results {
		out = append(out, r)
	}
	sort.Ints(out) // varış sırası belirsiz: deterministik çıktı için sırala
	fmt.Println("sonuçlar:", out)

	// Kanal kapasitesi ve doluluk
	buf := make(chan string, 3)
	buf <- "a"
	buf <- "b"
	fmt.Printf("kapasite=%d uzunluk=%d\n", cap(buf), len(buf))

	// Bloklanmadan deneme: select + default
	buf <- "c"
	select {
	case buf <- "d":
		fmt.Println("d eklendi")
	default:
		fmt.Println("kuyruk dolu, d eklenemedi")
	}

	fmt.Println("önden çıkan:", <-buf)
}
Çıktı
sonuçlar: [1 4 9 16 25 36 49 64]
kapasite=3 uzunluk=2
kuyruk dolu, d eklenemedi
önden çıkan: a

Kanal ile elle yazılmış kuyruk arasındaki seçim şuna bağlıdır: Kuyruğa birden çok goroutine erişecekse ve elemanlar bir tarafta üretilip diğer tarafta tüketilecekse kanal doğru araçtır — senkronizasyonu, engellemeyi ve kapanışı kendisi halleder. Tek bir goroutine içinde algoritmik bir kuyruk gerekiyorsa (BFS gibi) kanal gereksiz yere pahalıdır; dilim tabanlı kuyruk çok daha hızlıdır.

Kanal kuyruğunun bir sınırı da rastgele erişime izin vermemesidir: Yalnızca en öndeki elemanı alabilirsin, içine bakamaz ya da uzunluğuna göre karar veremezsin (aslında len çalışır ama yarış durumuna açıktır).

Monoton deque: kayan pencere maksimumu

Bir dizide k genişliğindeki her pencerenin maksimumunu bulmak isteniyor. Kaba kuvvet çözümü her pencereyi tarar: O(n·k). Monoton bir deque ile bu O(n)'e iner.

Fikir, yığındaki monoton desenin deque hâlidir: Deque'te azalan sırada indeksler tutarsın. Yeni gelen eleman, arkadaki daha küçük elemanları geçersiz kılar (onlar artık hiçbir pencerede maksimum olamaz), pencereden çıkan eleman da önden atılır.

dizi: [1, 3, -1, -3, 5, 3, 6, 7], k = 3

i=0: deque=[0]           (değer 1)
i=1: 3 > 1 → 0 atılır    deque=[1]           (değer 3)
i=2: -1 < 3 → eklenir    deque=[1,2]         pencere [1,3,-1] max=3
i=3: -3 < -1 → eklenir   deque=[1,2,3]       pencere [3,-1,-3] max=3
i=4: 5 hepsini siler     deque=[4]           pencere [-1,-3,5] max=5
...
main.go
package main

import "fmt"

// maxSlidingWindow, k genişliğindeki her pencerenin maksimumunu O(n) sürede bulur.
func maxSlidingWindow(nums []int, k int) []int {
	if k <= 0 || len(nums) < k {
		return nil
	}
	out := make([]int, 0, len(nums)-k+1)
	var deque []int // indeksler; karşılık gelen değerler AZALAN sırada

	for i, v := range nums {
		// 1) Pencereden çıkan indeksi önden at
		if len(deque) > 0 && deque[0] <= i-k {
			deque = deque[1:]
		}
		// 2) Arkada kalan küçük değerler artık maksimum olamaz
		for len(deque) > 0 && nums[deque[len(deque)-1]] <= v {
			deque = deque[:len(deque)-1]
		}
		deque = append(deque, i)

		// 3) Pencere tamamlandıysa cevabı yaz: her zaman deque'in önü
		if i >= k-1 {
			out = append(out, nums[deque[0]])
		}
	}
	return out
}

// bruteForce: doğrulama için O(n·k) sürüm
func bruteForce(nums []int, k int) []int {
	if k <= 0 || len(nums) < k {
		return nil
	}
	out := make([]int, 0, len(nums)-k+1)
	for i := 0; i+k <= len(nums); i++ {
		best := nums[i]
		for _, v := range nums[i : i+k] {
			best = max(best, v)
		}
		out = append(out, best)
	}
	return out
}

func main() {
	nums := []int{1, 3, -1, -3, 5, 3, 6, 7}

	for _, k := range []int{1, 3, 4, 8} {
		fast := maxSlidingWindow(nums, k)
		slow := bruteForce(nums, k)
		same := fmt.Sprint(fast) == fmt.Sprint(slow)
		fmt.Printf("k=%d%v (kaba kuvvetle aynı mı: %t)\n", k, fast, same)
	}

	fmt.Println("k dizi boyundan büyük:", maxSlidingWindow(nums, 20))
	fmt.Println("azalan dizi:", maxSlidingWindow([]int{9, 7, 5, 3}, 2))
}
Çıktı
k=1 → [1 3 -1 -3 5 3 6 7] (kaba kuvvetle aynı mı: true)
k=3 → [3 3 5 5 6 7] (kaba kuvvetle aynı mı: true)
k=4 → [3 5 5 6 7] (kaba kuvvetle aynı mı: true)
k=8 → [7] (kaba kuvvetle aynı mı: true)
k dizi boyundan büyük: []
azalan dizi: [9 7 5]
ZamanO(n)AlanO(k)

Karmaşıklık analizi yine amortizeye dayanır: İç döngü bazen çok eleman atsa da, her indeks deque'e bir kez girer ve en fazla bir kez çıkar. Bellek kullanımı O(k)'dır, çünkü deque'te aynı anda en fazla pencere boyu kadar indeks bulunur.

Bu deseni tanımanın işareti şudur: "Sabit genişlikte kayan bir pencerede en büyük/en küçük değeri sürekli takip et." Kayan pencere ailesinin diğer üyelerini İki İşaretçi ve Kayan Pencere dersinde göreceksin.

Kuyruğu ne zaman seçmeli?

Kuyruk, "sıraya girme" fikrinin doğal olduğu her yerde doğru araçtır. Tanıman gereken durumlar şunlardır.

Üretici-tüketici ilişkisi. Bir taraf iş üretiyor, diğer taraf tüketiyorsa kuyruk ikisini birbirinden ayırır. Üretici hızlandığında kuyruk şişer, tüketici yetiştiğinde boşalır; bu tampon etkisi sistemi ani yüklere karşı dayanıklı kılar. Go'da bu iş için kanallar vardır.

Katman katman gezinme. Bir ağacı seviye seviye, bir grafı mesafe sırasına göre gezmek istiyorsan kuyruk zorunludur. Bu, genişlik öncelikli aramanın tanımıdır ve ağırlıksız graflarda en kısa yolu bulmanın en basit yoludur.

Adil sıra. İsteklerin geliş sırasına göre işlenmesi gerekiyorsa kuyruk bunu doğal olarak sağlar. Öncelik gerekiyorsa öncelik kuyruğuna (heap) geçmen gerekir; onu Heap dersinde inceliyoruz.

Sabit bellekli tamponlama. Son N kaydı tutmak, ses örneklerini tamponlamak ya da ağ paketlerini biriktirmek gibi işlerde dairesel tampon hem hız hem öngörülebilir bellek sağlar.

Hız sınırlama ve geri basınç. Kuyruğun kapasitesini sınırlamak, sistemin kaldırabileceğinden fazla iş kabul etmesini engeller. Dolu bir kuyruk, üreticiye "yavaşla" sinyali verir; buna geri basınç (backpressure) denir ve dayanıklı sistemlerin temel tekniğidir.

Kuyruğun uygun olmadığı durum, elemanlara ortadan erişmen ya da onları yeniden sıralaman gereken hâllerdir. O zaman ya öncelik kuyruğuna ya da doğrudan bir dilime geçmelisin.

Sık yapılan hatalar

  • q = q[1:] ile kuyruk yazmak. Altta yatan dizi asla küçülmez; uzun ömürlü kuyruklarda bellek sürekli büyür.
  • Her Dequeuedan sonra sıkıştırmak. O(n) maliyet getirir; yalnızca boş alan belirli bir eşiği geçtiğinde sıkıştır.
  • Dairesel tamponda boş/dolu ayrımını yapamamak. Yalnızca iki indeksle bu iki durum aynı görünür; count alanı tut.
  • Mod işlemini unutmak. Dairesel tamponda indeksler % cap ile sarmalanmalıdır.
  • Kapatılan kanala yazmak. Panik oluşturur; kapatmayı yalnızca yazan taraf yapar.
  • Dilim kuyruğunu eşzamanlı kullanmak. Kilit yoksa yarış durumu oluşur; eşzamanlılık gerekiyorsa kanal kullan.
  • Monoton deque'te yanlış uçtan çıkarmak. Pencereden çıkan indeks önden, küçük değerler arkadan atılır.

Alıştırmalar

Alıştırma·İki yığınla kuyruk
Kolay

Yalnızca iki yığın kullanarak bir kuyruk yaz. Enqueue giriş yığınına eklesin; Dequeue çıkış yığını boşsa giriş yığınındaki her şeyi ona aktarsın. Amortize O(1) olduğunu açıkla.

İpucu

Bir yığından diğerine aktarma, sırayı ters çevirir; iki kez ters çevirmek özgün sırayı verir. Aktarmayı yalnızca çıkış yığını boşken yap.

Çözümü göster
main.go
package main

import "fmt"

type TwoStackQueue[T any] struct {
	in, out   []T
	transfers int
}

func (q *TwoStackQueue[T]) Enqueue(v T) {
	q.in = append(q.in, v)
}

func (q *TwoStackQueue[T]) Dequeue() (T, bool) {
	var zero T
	if len(q.out) == 0 {
		if len(q.in) == 0 {
			return zero, false
		}
		// Aktarma: sıra ters çevrilir, böylece out'un tepesi en eski elemandır
		for len(q.in) > 0 {
			last := len(q.in) - 1
			q.out = append(q.out, q.in[last])
			q.in[last] = zero
			q.in = q.in[:last]
		}
		q.transfers++
	}
	last := len(q.out) - 1
	v := q.out[last]
	q.out[last] = zero
	q.out = q.out[:last]
	return v, true
}

func (q *TwoStackQueue[T]) Len() int { return len(q.in) + len(q.out) }

func main() {
	var q TwoStackQueue[int]

	for i := 1; i <= 5; i++ {
		q.Enqueue(i)
	}

	fmt.Print("çıkış sırası: ")
	for range 3 {
		v, _ := q.Dequeue()
		fmt.Print(v, " ")
	}
	fmt.Println("| kalan:", q.Len())

	q.Enqueue(6)
	q.Enqueue(7)

	fmt.Print("devam: ")
	for q.Len() > 0 {
		v, _ := q.Dequeue()
		fmt.Print(v, " ")
	}
	fmt.Println()

	fmt.Println("toplam aktarma sayısı:", q.transfers)
	_, ok := q.Dequeue()
	fmt.Println("boş kuyruktan alınabildi mi:", ok)
}
Çıktı
çıkış sırası: 1 2 3 | kalan: 2
devam: 4 5 6 7
toplam aktarma sayısı: 2
boş kuyruktan alınabildi mi: false
ZamanO(1) amortizeAlanO(n)

Tek bir Dequeue en kötü durumda O(n) sürebilir (tüm giriş yığını aktarılır), ama her eleman hayatı boyunca en fazla iki kez taşınır: bir kez girişe, bir kez çıkışa. Dolayısıyla n işlem için toplam iş O(n)'dir ve işlem başına amortize maliyet sabittir. Bu, yığın dersinde gördüğün monoton yığın analizinin aynısıdır.

Alıştırma·Son N olayın ortalaması
Orta

Dairesel tampon kullanarak, gelen sayıların son N tanesinin ortalamasını sürekli veren bir yapı yaz. Ekleme ve ortalama sorgusu O(1) olsun; tamponu her seferinde toplamak yasak.

İpucu

Çalışan bir toplam (running sum) tut: Yeni eleman gelince topla, atılan eleman varsa çıkar.

Çözümü göster
main.go
package main

import "fmt"

type MovingAverage struct {
	data  []float64
	head  int
	count int
	sum   float64
}

func NewMovingAverage(window int) *MovingAverage {
	return &MovingAverage{data: make([]float64, max(1, window))}
}

// Add: O(1) — çalışan toplam güncellenir
func (m *MovingAverage) Add(v float64) float64 {
	if m.count == len(m.data) {
		m.sum -= m.data[m.head] // pencereden çıkan değeri düş
		m.head = (m.head + 1) % len(m.data)
		m.count--
	}
	tail := (m.head + m.count) % len(m.data)
	m.data[tail] = v
	m.sum += v
	m.count++
	return m.Average()
}

// Average: O(1)
func (m *MovingAverage) Average() float64 {
	if m.count == 0 {
		return 0
	}
	return m.sum / float64(m.count)
}

func (m *MovingAverage) Window() []float64 {
	out := make([]float64, 0, m.count)
	for i := range m.count {
		out = append(out, m.data[(m.head+i)%len(m.data)])
	}
	return out
}

func main() {
	ma := NewMovingAverage(3)

	for _, v := range []float64{10, 20, 30, 40, 50} {
		avg := ma.Add(v)
		fmt.Printf("ekle %-4.0f pencere=%-18v ortalama=%.2f\n", v, ma.Window(), avg)
	}

	fmt.Println()
	single := NewMovingAverage(1)
	fmt.Println("tek elemanlı pencere:", single.Add(5), single.Add(100))

	empty := NewMovingAverage(4)
	fmt.Println("boş ortalama:", empty.Average())
}
Çıktı
ekle 10   pencere=[10                ] ortalama=10.00
ekle 20   pencere=[10                 20                ] ortalama=15.00
ekle 30   pencere=[10                 20                 30                ] ortalama=20.00
ekle 40   pencere=[20                 30                 40                ] ortalama=30.00
ekle 50   pencere=[30                 40                 50                ] ortalama=40.00

tek elemanlı pencere: 5 100
boş ortalama: 0
ZamanO(1)AlanO(N)

Çalışan toplam tutmak, ortalamayı sabit sürede vermenin anahtarıdır; her sorguda pencereyi toplasaydın O(N) olurdu. Aynı teknik, kayan pencere toplamı, hareketli varyans ve hız hesaplarında kullanılır.

Küçük bir uyarı: Ondalık sayılarla çalışan toplam uzun süre güncellenirse yuvarlama hataları birikebilir. Hassasiyetin kritik olduğu yerlerde ya tamsayı cinsinden sakla ya da belirli aralıklarla toplamı yeniden hesapla.

Alıştırma·Görev zamanlayıcı benzetimi
Zor

Bir kuyruk kullanarak basit bir round-robin işlemci zamanlayıcısı yaz. Her görevin toplam çalışma süresi vardır; zamanlayıcı her göreve sabit bir zaman dilimi (quantum) verir, iş bitmezse görevi kuyruğun sonuna atar. Her görevin tamamlanma anını ve toplam bekleme süresini raporla.

İpucu

Kuyruğa (ad, kalan süre) çiftleri koy. Her turda quantum kadar ya da kalan süre kadar (hangisi küçükse) çalıştır, zamanı ilerlet.

Çözümü göster
main.go
package main

import "fmt"

type task struct {
	name      string
	total     int
	remaining int
}

type Queue[T any] struct {
	items []T
	head  int
}

func (q *Queue[T]) Enqueue(v T) { q.items = append(q.items, v) }

func (q *Queue[T]) Dequeue() (T, bool) {
	var zero T
	if q.head >= len(q.items) {
		return zero, false
	}
	v := q.items[q.head]
	q.items[q.head] = zero
	q.head++
	if q.head > len(q.items)/2 {
		n := copy(q.items, q.items[q.head:])
		q.items = q.items[:n]
		q.head = 0
	}
	return v, true
}

func (q *Queue[T]) Len() int { return len(q.items) - q.head }

func roundRobin(tasks []task, quantum int) {
	var q Queue[task]
	for _, t := range tasks {
		t.remaining = t.total
		q.Enqueue(t)
	}

	clock := 0
	fmt.Printf("%-6s %-8s %-10s %s\n", "zaman", "görev", "çalıştı", "durum")

	for q.Len() > 0 {
		t, _ := q.Dequeue()
		slice := min(quantum, t.remaining)
		start := clock
		clock += slice
		t.remaining -= slice

		status := "devam ediyor"
		if t.remaining == 0 {
			status = fmt.Sprintf("BİTTİ (bekleme: %d)", clock-t.total)
		} else {
			q.Enqueue(t)
		}
		fmt.Printf("%-6s %-8s %-10s %s\n",
			fmt.Sprintf("%d-%d", start, clock), t.name, fmt.Sprint(slice), status)
	}
	fmt.Println("toplam süre:", clock)
}

func main() {
	tasks := []task{
		{name: "derleme", total: 5},
		{name: "test", total: 3},
		{name: "paketle", total: 7},
	}

	fmt.Println("quantum = 2")
	roundRobin(tasks, 2)

	fmt.Println()
	fmt.Println("quantum = 10 (FIFO gibi davranır)")
	roundRobin(tasks, 10)
}
Çıktı
quantum = 2
zaman  görev    çalıştı    durum
0-2    derleme  2          devam ediyor
2-4    test     2          devam ediyor
4-6    paketle  2          devam ediyor
6-8    derleme  2          devam ediyor
8-9    test     1          BİTTİ (bekleme: 6)
9-11   paketle  2          devam ediyor
11-12  derleme  1          BİTTİ (bekleme: 7)
12-14  paketle  2          devam ediyor
14-15  paketle  1          BİTTİ (bekleme: 8)
toplam süre: 15

quantum = 10 (FIFO gibi davranır)
zaman  görev    çalıştı    durum
0-5    derleme  5          BİTTİ (bekleme: 0)
5-8    test     3          BİTTİ (bekleme: 5)
8-15   paketle  7          BİTTİ (bekleme: 8)
toplam süre: 15

Round-robin zamanlama, gerçek işletim sistemlerinin kullandığı en temel adil paylaşım stratejisidir ve kalbinde bir kuyruk vardır. Quantum seçimi klasik bir ödünleşmedir: Küçük quantum tepki süresini iyileştirir ama bağlam değiştirme maliyetini artırır; büyük quantum ise yapıyı FIFO'ya yaklaştırır — ikinci çıktıda tam olarak bunu görüyorsun.

Bekleme süresi hesabı, görevin bitiş anından kendi çalışma süresini çıkarmakla bulunur: Aradaki fark, başkalarının çalıştığı süredir. Görevlere öncelik eklemek istersen kuyruk yerine öncelik kuyruğu kullanman gerekir.

Kısa sınav

Kısa sınav

Dilim tabanlı bir kuyrukta q = q[1:] kullanmanın sakıncası nedir?

Dairesel tamponda bir sonraki yazma konumu nasıl hesaplanır?

Bir dairesel tamponda boş ve dolu durumları nasıl ayırt edilir?

Kayan pencere maksimumu probleminde deque'te ne saklanır?

Go'da eşzamanlı bir FIFO kuyruk gerektiğinde ilk tercih ne olmalıdır?

İki yığınla yapılan kuyruk uygulamasında Dequeue neden amortize O(1)'dir?

Özet

  • Kuyruk FIFO prensibiyle çalışır: Enqueue arkaya ekler, Dequeue önden alır.
  • Naif q = q[1:] uygulaması bellek sızdırır; baş indeksi tutup boş alan yarıyı geçince sıkıştırmak amortize O(1) verir.
  • Dairesel tampon sabit bellekle çalışır; indeksler % cap ile sarmalanır ve boş/dolu ayrımı için sayaç tutulur.
  • Deque her iki uçtan ekleme ve çıkarma sağlar; hem yığın hem kuyruk gibi kullanılabilir.
  • Tamponlu kanallar, Go'nun yerleşik eşzamanlı kuyruğudur; senkronizasyon ve geri basınç sağlar.
  • Monoton deque, kayan pencere maksimumunu O(n) sürede ve O(k) bellekle hesaplar.
  • Kuyruk; üretici-tüketici, BFS, adil sıra, sabit bellekli tamponlama ve geri basınç senaryolarının doğal aracıdır.
  • Önceliğe göre hizmet gerekiyorsa kuyruk yetmez; öncelik kuyruğuna (heap) geçilir.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.