go
Algoritmalar dersleri
Algoritmalar/Temel Teknikler

Verimli Sıralama Algoritmaları

Merge sort, quick sort, heap sort ve O(n log n) alt sınırı.

Ders 4 / 1835 dkOrta
Bu derste öğreneceklerin
  • Birleştirmeli sıralama (merge sort)
  • Hızlı sıralama (quick sort): Lomuto ve Hoare bölümlemesi
  • Pivot seçimi ve en kötü durum
  • Heap sıralaması (heap sort)
  • Karşılaştırma tabanlı sıralamanın alt sınırı
  • Go'nun slices.Sort'u: pdqsort
  • sort.Slice, slices.SortFunc ve cmp.Compare

Önceki derste gördüğün üç algoritma da O(n²)'ydi ve bunun sebebi ortaktı: Her biri, bir elemanı doğru yerine koymak için diğerleriyle tek tek karşılaştırıyordu. Bir milyon elemanlı bir dizide bu, bir trilyon işlem demektir — modern bir bilgisayarda bile saatler sürer.

Bu derste göreceğin üç algoritma aynı işi O(n log n) sürede yapar: bir milyon eleman için yirmi milyon işlem, yani saniyenin altında. Bu muazzam farkın kaynağı tek bir fikirdir: Bir karşılaştırmadan olabildiğince çok bilgi çıkarmak. Eklemeli sıralamada bir karşılaştırma yalnızca iki eleman hakkında bilgi verir; birleştirmeli sıralamada ise koca bir alt dizi hakkında.

Üçünü de öğrendikten sonra daha derin bir soruya geleceğiz: Daha da hızlı olabilir mi? Cevap, karşılaştırma tabanlı sıralama için hayır — ve bunun matematiksel bir kanıtı var. Son olarak Go'nun slices.Sort fonksiyonunun içinde ne olduğunu inceleyeceğiz; modern bir sıralama uygulamasının bu üç algoritmayı nasıl birleştirdiğini görmek, teoriyle pratiğin buluştuğu noktayı gösterir.

Birleştirmeli sıralama

Fikir, böl ve fethet stratejisinin en temiz örneğidir: Diziyi ikiye böl, her yarıyı ayrı ayrı sırala, sonra iki sıralı yarıyı birleştir.

        [5, 2, 9, 1, 7, 3]
              böl
      [5, 2, 9]   [1, 7, 3]
         böl          böl
    [5] [2,9]     [1] [7,3]
         böl           böl
        [2] [9]       [7] [3]
        birleştir     birleştir
        [2, 9]        [3, 7]
      birleştir     birleştir
     [2, 5, 9]      [1, 3, 7]
           birleştir
      [1, 2, 3, 5, 7, 9]

Bölme: log n seviye
Her seviyede birleştirme: O(n)
Toplam: O(n log n)

Aşağıdaki görselleştirmede birleştirmeli sıralamanın adımlarını izleyebilirsin; alt dizilerin nasıl birleştiğine dikkat et:

Birleştirme adımı algoritmanın kalbidir ve şaşırtıcı derecede basittir: İki sıralı listenin başlarına bak, küçüğünü al, ilerle.

main.go
package main

import (
	"fmt"
	"slices"
)

var merges, comparisons int

// merge: iki sıralı dilimi birleştirir — O(n)
func merge(left, right []int) []int {
	out := make([]int, 0, len(left)+len(right))
	i, j := 0, 0

	for i < len(left) && j < len(right) {
		comparisons++
		if left[i] <= right[j] { // <= kullanıldığı için KARARLI
			out = append(out, left[i])
			i++
		} else {
			out = append(out, right[j])
			j++
		}
	}
	out = append(out, left[i:]...)
	out = append(out, right[j:]...)
	merges++
	return out
}

// mergeSort: böl, fethet, birleştir
func mergeSort(data []int) []int {
	if len(data) <= 1 {
		return data
	}
	mid := len(data) / 2
	left := mergeSort(slices.Clone(data[:mid]))
	right := mergeSort(slices.Clone(data[mid:]))
	return merge(left, right)
}

// mergeSortInPlace: tek yardımcı tampon kullanır — daha az bellek ayırma
func mergeSortInPlace(data, buffer []int, lo, hi int) {
	if hi-lo <= 1 {
		return
	}
	mid := lo + (hi-lo)/2
	mergeSortInPlace(data, buffer, lo, mid)
	mergeSortInPlace(data, buffer, mid, hi)

	i, j, k := lo, mid, lo
	for i < mid && j < hi {
		if data[i] <= data[j] {
			buffer[k] = data[i]
			i++
		} else {
			buffer[k] = data[j]
			j++
		}
		k++
	}
	for i < mid {
		buffer[k] = data[i]
		i, k = i+1, k+1
	}
	for j < hi {
		buffer[k] = data[j]
		j, k = j+1, k+1
	}
	copy(data[lo:hi], buffer[lo:hi])
}

func main() {
	data := []int{5, 2, 9, 1, 7, 3, 8, 4, 6}
	fmt.Println("girdi:", data)

	sorted := mergeSort(slices.Clone(data))
	fmt.Println("sonuç:", sorted)
	fmt.Printf("birleştirme sayısı: %d, karşılaştırma: %d\n", merges, comparisons)

	// Yerinde sürüm
	inPlace := slices.Clone(data)
	buffer := make([]int, len(inPlace))
	mergeSortInPlace(inPlace, buffer, 0, len(inPlace))
	fmt.Println("tampon kullanan sürüm:", inPlace)
	fmt.Println("iki sürüm aynı mı:", slices.Equal(sorted, inPlace))

	// Karmaşıklık gözlemi
	fmt.Println()
	fmt.Printf("%10s %14s %14s %12s\n", "n", "karşılaştırma", "n log₂ n", "oran")
	for _, n := range []int{16, 64, 256, 1024, 4096} {
		big := make([]int, n)
		for i := range big {
			big[i] = (i * 7919) % n // deterministik karışık dizi
		}
		comparisons, merges = 0, 0
		mergeSort(big)
		expected := n * log2(n)
		fmt.Printf("%10d %14d %14d %12.2f\n", n, comparisons, expected,
			float64(comparisons)/float64(expected))
	}
}

func log2(n int) int {
	count := 0
	for n > 1 {
		n /= 2
		count++
	}
	return count
}
Çıktı
girdi: [5 2 9 1 7 3 8 4 6]
sonuç: [1 2 3 4 5 6 7 8 9]
birleştirme sayısı: 8, karşılaştırma: 21
tampon kullanan sürüm: [1 2 3 4 5 6 7 8 9]
iki sürüm aynı mı: true

         n  karşılaştırma       n log₂ n         oran
        16             35             64         0.55
        64            310            384         0.81
       256           1594           2048         0.78
      1024           8929          10240         0.87
      4096          41369          49152         0.84
En iyiO(n log n)OrtalamaO(n log n)En kötüO(n log n)AlanO(n)

Birleştirmeli sıralamanın dört ayırt edici özelliği vardır:

Her durumda O(n log n). Girdi ne olursa olsun aynı sayıda bölme ve birleştirme yapılır. Bu öngörülebilirlik, gerçek zamanlı sistemlerde değerlidir.

Kararlıdır. Birleştirme sırasında <= kullanıldığı için eşitlikte sol taraftaki eleman önce alınır ve göreli sıra korunur.

O(n) ek bellek gerektirir. En büyük dezavantajıdır. Yerinde birleştirme mümkündür ama karmaşıktır ve pratikte daha yavaştır.

Bağlı listelerde mükemmeldir. Listelerde birleştirme, yalnızca işaretçileri yeniden bağlamaktır — ek bellek gerekmez. Bu yüzden bağlı liste sıralamasının standart yöntemidir.

Hızlı sıralama

Birleştirmeli sıralama "böl, sonra birleştir" derken, hızlı sıralama tersini yapar: Önce düzenle, sonra böl. Bir pivot seçer, diziyi pivottan küçükler ve büyükler olarak ikiye ayırır (bölümleme), sonra her parçayı ayrı ayrı sıralar. Birleştirme adımı yoktur — bölümleme zaten işi yapmıştır.

[5, 2, 9, 1, 7, 3]   pivot = 3

bölümleme: [2, 1 | 3 | 5, 9, 7]
            küçük  piv  büyük

3 artık KESİN yerinde. Şimdi iki yanı ayrı sırala.

[1, 2] ve [5, 7, 9] → [1, 2, 3, 5, 7, 9]

Aşağıdaki görselleştirmede hızlı sıralamanın bölümleme adımlarını, pivotun nasıl yerine oturduğunu izleyebilirsin:

İki klasik bölümleme şeması vardır:

main.go
package main

import (
	"fmt"
	"math/rand/v2"
	"slices"
)

// lomutoPartition: son elemanı pivot alır, anlaşılması kolay
func lomutoPartition(data []int, lo, hi int) int {
	pivot := data[hi]
	i := lo // küçük elemanların sınırı

	for j := lo; j < hi; j++ {
		if data[j] < pivot {
			data[i], data[j] = data[j], data[i]
			i++
		}
	}
	data[i], data[hi] = data[hi], data[i] // pivotu yerine koy
	return i
}

// hoarePartition: iki uçtan ortaya doğru çalışır, daha az takas yapar
func hoarePartition(data []int, lo, hi int) int {
	pivot := data[lo+(hi-lo)/2]
	i, j := lo-1, hi+1

	for {
		for {
			i++
			if data[i] >= pivot {
				break
			}
		}
		for {
			j--
			if data[j] <= pivot {
				break
			}
		}
		if i >= j {
			return j
		}
		data[i], data[j] = data[j], data[i]
	}
}

func quickSortLomuto(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	p := lomutoPartition(data, lo, hi)
	quickSortLomuto(data, lo, p-1)
	quickSortLomuto(data, p+1, hi)
}

func quickSortHoare(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	p := hoarePartition(data, lo, hi)
	quickSortHoare(data, lo, p)
	quickSortHoare(data, p+1, hi)
}

// quickSortRandom: rastgele pivot — en kötü durumu pratikte imkânsız kılar
func quickSortRandom(data []int, lo, hi int, r *rand.Rand) {
	if lo >= hi {
		return
	}
	pick := lo + r.IntN(hi-lo+1)
	data[pick], data[hi] = data[hi], data[pick]

	p := lomutoPartition(data, lo, hi)
	quickSortRandom(data, lo, p-1, r)
	quickSortRandom(data, p+1, hi, r)
}

func main() {
	base := []int{5, 2, 9, 1, 7, 3, 8, 4, 6}

	a := slices.Clone(base)
	quickSortLomuto(a, 0, len(a)-1)
	fmt.Println("Lomuto:", a)

	b := slices.Clone(base)
	quickSortHoare(b, 0, len(b)-1)
	fmt.Println("Hoare: ", b)

	r := rand.New(rand.NewPCG(1, 2))
	c := slices.Clone(base)
	quickSortRandom(c, 0, len(c)-1, r)
	fmt.Println("rastgele pivot:", c)

	fmt.Println("üçü aynı mı:", slices.Equal(a, b) && slices.Equal(b, c))

	// Bölümleme adımını yakından görelim
	fmt.Println()
	demo := []int{5, 2, 9, 1, 7, 3}
	fmt.Println("bölümleme öncesi:", demo, "pivot:", demo[len(demo)-1])
	p := lomutoPartition(demo, 0, len(demo)-1)
	fmt.Println("bölümleme sonrası:", demo)
	fmt.Printf("pivot indeksi: %d, sol taraf: %v, sağ taraf: %v\n", p, demo[:p], demo[p+1:])
}
Çıktı
Lomuto: [1 2 3 4 5 6 7 8 9]
Hoare:  [1 2 3 4 5 6 7 8 9]
rastgele pivot: [1 2 3 4 5 6 7 8 9]
üçü aynı mı: true

bölümleme öncesi: [5 2 9 1 7 3] pivot: 3
bölümleme sonrası: [2 1 3 5 7 9]
pivot indeksi: 2, sol taraf: [2 1], sağ taraf: [5 7 9]
En iyiO(n log n)OrtalamaO(n log n)En kötüO(n²)AlanO(log n)

Pivot seçimi ve en kötü durum

Hızlı sıralamanın Aşil topuğu pivot seçimidir. Pivot her zaman en küçük ya da en büyük elemansa, bölümleme dengesiz olur ve özyineleme derinliği n'e çıkar.

KÖTÜ pivot seçimi (sıralı dizide son eleman):

[1, 2, 3, 4, 5]  pivot=5 → [1,2,3,4 | 5]     n-1 eleman
[1, 2, 3, 4]     pivot=4 → [1,2,3 | 4]       n-2 eleman
[1, 2, 3]        pivot=3 → [1,2 | 3]
...
Derinlik = n, her seviyede O(n) iş → O(n²)

İYİ pivot seçimi (medyana yakın):
Her bölümleme diziyi yarıya yakın böler → derinlik log n → O(n log n)
main.go
package main

import (
	"fmt"
	"math/rand/v2"
	"slices"
)

var depth, maxDepth, partitions int

func partition(data []int, lo, hi int) int {
	partitions++
	pivot := data[hi]
	i := lo
	for j := lo; j < hi; j++ {
		if data[j] < pivot {
			data[i], data[j] = data[j], data[i]
			i++
		}
	}
	data[i], data[hi] = data[hi], data[i]
	return i
}

// lastElementPivot: en kötü duruma açık
func quickSortLast(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	depth++
	maxDepth = max(maxDepth, depth)
	p := partition(data, lo, hi)
	quickSortLast(data, lo, p-1)
	quickSortLast(data, p+1, hi)
	depth--
}

// medianOfThree: ilk, orta ve son elemanın medyanını pivot yapar
func medianOfThree(data []int, lo, hi int) {
	mid := lo + (hi-lo)/2
	if data[mid] < data[lo] {
		data[mid], data[lo] = data[lo], data[mid]
	}
	if data[hi] < data[lo] {
		data[hi], data[lo] = data[lo], data[hi]
	}
	if data[hi] < data[mid] {
		data[hi], data[mid] = data[mid], data[hi]
	}
	data[mid], data[hi] = data[hi], data[mid] // medyanı sona koy
}

func quickSortMedian(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	depth++
	maxDepth = max(maxDepth, depth)
	medianOfThree(data, lo, hi)
	p := partition(data, lo, hi)
	quickSortMedian(data, lo, p-1)
	quickSortMedian(data, p+1, hi)
	depth--
}

func quickSortRandom(data []int, lo, hi int, r *rand.Rand) {
	if lo >= hi {
		return
	}
	depth++
	maxDepth = max(maxDepth, depth)
	pick := lo + r.IntN(hi-lo+1)
	data[pick], data[hi] = data[hi], data[pick]
	p := partition(data, lo, hi)
	quickSortRandom(data, lo, p-1, r)
	quickSortRandom(data, p+1, hi, r)
	depth--
}

func main() {
	const n = 500
	sorted := make([]int, n)
	for i := range sorted {
		sorted[i] = i
	}
	r := rand.New(rand.NewPCG(9, 13))
	shuffled := slices.Clone(sorted)
	r.Shuffle(n, func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })

	run := func(name string, data []int, sortFn func([]int)) {
		depth, maxDepth, partitions = 0, 0, 0
		copied := slices.Clone(data)
		sortFn(copied)
		fmt.Printf("  %-22s derinlik=%-5d bölümleme=%-6d sıralı=%t\n",
			name, maxDepth, partitions, slices.IsSorted(copied))
	}

	fmt.Printf("SIRALI girdi (n=%d) — teorik en iyi derinlik ≈ %d:\n", n, 9)
	run("son eleman pivot", sorted, func(d []int) { quickSortLast(d, 0, len(d)-1) })
	run("üçün medyanı", sorted, func(d []int) { quickSortMedian(d, 0, len(d)-1) })
	run("rastgele pivot", sorted, func(d []int) {
		quickSortRandom(d, 0, len(d)-1, rand.New(rand.NewPCG(1, 2)))
	})

	fmt.Println()
	fmt.Printf("KARIŞIK girdi (n=%d):\n", n)
	run("son eleman pivot", shuffled, func(d []int) { quickSortLast(d, 0, len(d)-1) })
	run("üçün medyanı", shuffled, func(d []int) { quickSortMedian(d, 0, len(d)-1) })
	run("rastgele pivot", shuffled, func(d []int) {
		quickSortRandom(d, 0, len(d)-1, rand.New(rand.NewPCG(1, 2)))
	})

	fmt.Println()
	fmt.Println("Gözlem: sıralı girdide son-eleman pivotu derinliği n'e çıkarır → O(n²)")
	fmt.Println("Gözlem: medyan veya rastgele pivot her iki girdide de logaritmik kalır")
}
Çıktı
SIRALI girdi (n=500) — teorik en iyi derinlik ≈ 9:
  son eleman pivot       derinlik=499   bölümleme=499    sıralı=true
  üçün medyanı           derinlik=8     bölümleme=255    sıralı=true
  rastgele pivot         derinlik=18    bölümleme=337    sıralı=true

KARIŞIK girdi (n=500):
  son eleman pivot       derinlik=19    bölümleme=333    sıralı=true
  üçün medyanı           derinlik=15    bölümleme=287    sıralı=true
  rastgele pivot         derinlik=18    bölümleme=334    sıralı=true

Gözlem: sıralı girdide son-eleman pivotu derinliği n'e çıkarır → O(n²)
Gözlem: medyan veya rastgele pivot her iki girdide de logaritmik kalır

Sayılar konuyu net gösteriyor: Sıralı bir girdide son elemanı pivot almak, özyineleme derinliğini eleman sayısına eşitler. Sıralı veri hiç de nadir değildir, bu yüzden gerçek uygulamalar üç stratejiden birini kullanır: rastgele pivot, üçün medyanı ya da her ikisi.

Hızlı sıralamanın buna rağmen tercih edilme sebebi, pratikte en hızlı olmasıdır. Yerinde çalışır (ek bellek yok), bellek erişimi ardışıktır (önbellek dostu) ve iç döngüsü çok sadedir. Birleştirmeli sıralamayla aynı karmaşıklık sınıfında olsa da sabit çarpanı belirgin biçimde küçüktür.

Heap sıralaması

Üçüncü yaklaşım, bir veri yapısından yararlanır. Heap dersinde gördüğün max-heap, en büyük elemanı O(1) sürede verir ve çıkardıktan sonra O(log n) sürede kendini toparlar. O hâlde: Diziyi heap'e çevir, en büyüğü sürekli çekip sona koy.

[5, 2, 9, 1, 7]

1) heapify → max-heap kur
   [9, 7, 5, 1, 2]

2) tepeyi sonla takas et, heap boyutunu küçült, düzelt
   [7, 2, 5, 1 | 9]
   [5, 2, 1 | 7, 9]
   [2, 1 | 5, 7, 9]
   [1 | 2, 5, 7, 9]
   [1, 2, 5, 7, 9]  ✓
main.go
package main

import (
	"fmt"
	"slices"
)

// siftDown: kökü doğru yerine indirir — O(log n)
func siftDown(data []int, root, size int) {
	for {
		child := 2*root + 1
		if child >= size {
			return
		}
		// İki çocuğun büyüğünü seç
		if child+1 < size && data[child+1] > data[child] {
			child++
		}
		if data[root] >= data[child] {
			return // heap özelliği sağlandı
		}
		data[root], data[child] = data[child], data[root]
		root = child
	}
}

// heapSort: yerinde, O(n log n) garanti
func heapSort(data []int) {
	n := len(data)

	// 1) Max-heap kur: son yapraktan geriye doğru — O(n)
	for i := n/2 - 1; i >= 0; i-- {
		siftDown(data, i, n)
	}

	// 2) En büyüğü sona al, heap'i küçült — n × O(log n)
	for end := n - 1; end > 0; end-- {
		data[0], data[end] = data[end], data[0]
		siftDown(data, 0, end)
	}
}

func isMaxHeap(data []int) bool {
	for i := range data {
		for _, c := range []int{2*i + 1, 2*i + 2} {
			if c < len(data) && data[i] < data[c] {
				return false
			}
		}
	}
	return true
}

func main() {
	data := []int{5, 2, 9, 1, 7, 3, 8}
	fmt.Println("girdi:", data)

	// Heapify adımını ayrı gösterelim
	heap := slices.Clone(data)
	for i := len(heap)/2 - 1; i >= 0; i-- {
		siftDown(heap, i, len(heap))
	}
	fmt.Println("heapify sonrası:", heap, "| geçerli max-heap mi:", isMaxHeap(heap))

	sorted := slices.Clone(data)
	heapSort(sorted)
	fmt.Println("sıralı:", sorted)

	// Farklı girdilerde tutarlı davranış
	fmt.Println()
	cases := map[string][]int{
		"sıralı":      {1, 2, 3, 4, 5, 6, 7},
		"ters sıralı": {7, 6, 5, 4, 3, 2, 1},
		"tekrarlı":    {3, 1, 3, 1, 3, 1},
		"tek eleman":  {42},
		"boş":         {},
	}
	for _, name := range []string{"sıralı", "ters sıralı", "tekrarlı", "tek eleman", "boş"} {
		d := slices.Clone(cases[name])
		heapSort(d)
		fmt.Printf("  %-12s%-20v sıralı: %t\n", name, d, slices.IsSorted(d))
	}

	// Kararsızlık
	fmt.Println()
	fmt.Println("heap sıralaması KARARSIZDIR: uzak elemanlar takas edilir")
}
Çıktı
girdi: [5 2 9 1 7 3 8]
heapify sonrası: [9 7 8 1 2 3 5] | geçerli max-heap mi: true
sıralı: [1 2 3 5 7 8 9]

  sıralı       → [1                    2                    3                    4                    5                    6                    7                   ] sıralı: true
  ters sıralı  → [1                    2                    3                    4                    5                    6                    7                   ] sıralı: true
  tekrarlı     → [1                    1                    1                    3                    3                    3                   ] sıralı: true
  tek eleman   → [42                  ] sıralı: true
  boş          → [] sıralı: true

heap sıralaması KARARSIZDIR: uzak elemanlar takas edilir
En iyiO(n log n)OrtalamaO(n log n)En kötüO(n log n)AlanO(1)

Heap sıralaması, iki dünyanın en iyisini birleştirir gibi görünür: birleştirmeli sıralama gibi garantili O(n log n), hızlı sıralama gibi yerinde. Peki neden varsayılan seçim değil?

Sebep önbellek davranışıdır. Heap işlemleri dizide 2i+1 ve 2i+2 gibi atlamalı erişimler yapar; büyük dizilerde bu, sürekli önbellek ıskası demektir. Hızlı sıralamanın ardışık erişimi pratikte iki-üç kat hız avantajı sağlar. Ayrıca heap sıralaması kararsızdır.

Yine de kritik bir rolü vardır: Hızlı sıralamanın en kötü durumuna karşı güvenlik ağı olarak kullanılır. Özyineleme derinliği tehlikeli seviyeye çıkarsa heap sıralamasına geçilir; böylece O(n²) hiçbir zaman gerçekleşmez.

O(n log n) alt sınırı

Şimdi temel bir soru: Daha hızlı bir sıralama algoritması mümkün mü? Karşılaştırma tabanlı algoritmalar için cevap kesin olarak hayır ve kanıtı zarif.

n eleman kaç farklı sırada olabilir?  n! (n faktöriyel)

Her karşılaştırma iki cevap verir: "küçük" veya "büyük"
Yani her karşılaştırma olasılık sayısını en iyi durumda YARIYA indirir.

k karşılaştırmayla ayırt edilebilecek durum sayısı: 2^k

Doğru sonucu bulmak için:  2^k ≥ n!
                            k ≥ log₂(n!)

Stirling yaklaşımı:         log₂(n!) ≈ n log₂ n − 1.44n

Sonuç: k = Ω(n log n)

Bu, bir algoritmanın zekâsıyla ilgili değil; bilgi teorik bir sınırdır. Elemanları yalnızca karşılaştırarak sıralıyorsan, n log n karşılaştırmanın altına inemezsin.

Peki bu sınır nasıl aşılır? Karşılaştırma yapmadan sıralayarak. Elemanlar hakkında ek bilgi varsa (belirli bir aralıkta tamsayılar, sabit uzunluklu metinler) doğrusal zamanlı sıralama mümkündür. Bunları Doğrusal Sıralama dersinde göreceksin.

Go'nun slices.Sort'u: pdqsort

Go 1.19'dan beri slices.Sort (ve sort.Slice), pdqsort — pattern-defeating quicksort — adlı melez bir algoritma kullanır. Üç algoritmayı da birleştirir:

Küçük dilimlerde eklemeli sıralama. Belirli bir eşiğin altında (yaklaşık 12 eleman) eklemeli sıralamaya geçer; sabit çarpan avantajı devreye girer.

Ana strateji hızlı sıralama. Bölümleme temeli hızlı sıralamadır: yerinde, önbellek dostu, hızlı.

Akıllı pivot seçimi. Küçük dilimlerde üçün medyanı, büyüklerde dokuzun medyanı gibi daha güçlü yöntemler kullanır.

Desen tespiti. "Pattern-defeating" adı buradan gelir: Girdinin zaten sıralı, ters sıralı ya da az sayıda farklı değer içerdiğini tespit eder ve bu durumlarda özel yollara sapar. Böylece kötü niyetli veya şanssız girdiler algoritmayı yavaşlatamaz.

Derinlik sınırı ve geri çekilme. Özyineleme derinliği çok artarsa heap sıralamasına geçer; bu, O(n²) olasılığını tamamen ortadan kaldırır.

main.go
package main

import (
	"cmp"
	"fmt"
	"slices"
	"sort"
	"strings"
)

type Employee struct {
	Name   string
	Salary int
	Dept   string
}

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

	// En basit kullanım: sıralanabilir tipler
	asc := slices.Clone(nums)
	slices.Sort(asc)
	fmt.Println("artan:", asc)

	// Azalan: karşılaştırmayı ters çevir
	desc := slices.Clone(nums)
	slices.SortFunc(desc, func(a, b int) int { return cmp.Compare(b, a) })
	fmt.Println("azalan:", desc)

	// Yapılar: çok ölçütlü sıralama
	employees := []Employee{
		{"Zeynep", 15000, "Mühendislik"},
		{"Ali", 12000, "Satış"},
		{"Mehmet", 15000, "Mühendislik"},
		{"Ayşe", 18000, "Yönetim"},
		{"Can", 12000, "Mühendislik"},
	}

	byDeptThenSalary := slices.Clone(employees)
	slices.SortFunc(byDeptThenSalary, func(a, b Employee) int {
		if c := strings.Compare(a.Dept, b.Dept); c != 0 {
			return c
		}
		return cmp.Compare(b.Salary, a.Salary) // maaş azalan
	})
	fmt.Println()
	fmt.Println("departman + maaş sırası:")
	for _, e := range byDeptThenSalary {
		fmt.Printf("  %-14s %-14s %d\n", e.Dept, e.Name, e.Salary)
	}

	// Kararlı sıralama: eşitlikte özgün sıra korunur
	stable := slices.Clone(employees)
	slices.SortStableFunc(stable, func(a, b Employee) int {
		return cmp.Compare(a.Salary, b.Salary)
	})
	fmt.Println()
	fmt.Println("maaşa göre KARARLI sıralama (eşit maaşlarda özgün sıra):")
	for _, e := range stable {
		fmt.Printf("  %-8s %d\n", e.Name, e.Salary)
	}

	// Eski API: sort.Slice (hâlâ çalışır)
	old := slices.Clone(nums)
	sort.Slice(old, func(i, j int) bool { return old[i] < old[j] })
	fmt.Println()
	fmt.Println("sort.Slice ile:", old)

	// Yararlı yardımcılar
	fmt.Println()
	fmt.Println("slices.IsSorted:", slices.IsSorted(asc))
	fmt.Println("slices.Min / Max:", slices.Min(nums), slices.Max(nums))
	fmt.Println("slices.MaxFunc (en yüksek maaş):",
		slices.MaxFunc(employees, func(a, b Employee) int { return cmp.Compare(a.Salary, b.Salary) }).Name)
}
Çıktı
artan: [1 2 3 5 7 9]
azalan: [9 7 5 3 2 1]

departman + maaş sırası:
  Mühendislik    Zeynep         15000
  Mühendislik    Mehmet         15000
  Mühendislik    Can            12000
  Satış          Ali            12000
  Yönetim        Ayşe           18000

maaşa göre KARARLI sıralama (eşit maaşlarda özgün sıra):
  Ali      12000
  Can      12000
  Zeynep   15000
  Mehmet   15000
  Ayşe     18000

sort.Slice ile: [1 2 3 5 7 9]

slices.IsSorted: true
slices.Min / Max: 1 9
slices.MaxFunc (en yüksek maaş): Ayşe

Üçünü karşılaştırma

ÖlçütBirleştirmeliHızlıHeap
En iyiO(n log n)O(n log n)O(n log n)
OrtalamaO(n log n)O(n log n)O(n log n)
En kötüO(n log n)O(n²)O(n log n)
Ek bellekO(n)O(log n)O(1)
KararlıEvetHayırHayır
Önbellek dostuOrtaÇok iyiKötü
Pratik hızOrtaEn hızlıOrta
ParalelleştirmeKolayOrtaZor

Pratikte kararlar şöyle verilir. Varsayılan olarak hızlı sıralama (yani slices.Sort) kullanılır; pratikte en hızlıdır ve modern uygulamalar en kötü durumu güvenlik ağıyla engeller. Kararlılık gerekiyorsa birleştirmeli sıralama tercih edilir; slices.SortStableFunc bunu yapar. Bellek çok kısıtlıysa heap sıralaması akla gelir, ama pratikte hızlı sıralamanın O(log n) yığın kullanımı neredeyse her zaman kabul edilebilirdir. Bağlı listelerde birleştirmeli sıralama açık ara en iyisidir. Çok büyük veriler diskte sıralanacaksa birleştirmeli sıralamanın harici varyantı kullanılır; parçaları belleğe sığdırıp birleştirerek çalışır.

Sık yapılan hatalar

  • Hızlı sıralamada pivotu sabit seçmek. Sıralı girdilerde O(n²)'ye düşer; rastgele veya medyan tabanlı seçim kullan.
  • Bölümlemeden sonra pivotu tekrar sıralamaya dâhil etmek. Lomuto'da pivot kesin yerindedir; [lo, p-1] ve [p+1, hi] ile devam edilir.
  • Hoare bölümlemesinde yanlış aralık kullanmak. Hoare j döndürür ve alt çağrılar [lo, j] ile [j+1, hi] olur; Lomuto'nun aralıklarıyla karıştırmak sonsuz özyinelemeye yol açar.
  • Birleştirmeli sıralamada her seviyede yeni tampon ayırmak. Tek bir tampon ayırıp yeniden kullanmak belirgin hız kazandırır.
  • Birleştirmede < kullanmak. Kararlılık için <= gerekir.
  • Heap sıralamasının kararlı olduğunu sanmak. Uzak elemanlar takas edildiği için kararsızdır.
  • O(n log n) sınırının her sıralama için geçerli olduğunu sanmak. Sınır yalnızca karşılaştırma tabanlı algoritmalar içindir.

Alıştırmalar

Alıştırma·Bağlı listeyi birleştirmeli sıralama
Kolay

Bir bağlı listeyi birleştirmeli sıralamayla sırala. Yeni düğüm oluşturmadan, yalnızca işaretçileri yeniden bağlayarak çalış — bu sayede ek bellek O(1) olur (özyineleme yığını hariç).

İpucu

Listeyi ortadan bölmek için hızlı-yavaş işaretçi tekniğini kullan. Birleştirme adımında kukla bir baş düğüm işini kolaylaştırır.

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

import (
	"fmt"
	"strings"
)

type Node struct {
	Value int
	Next  *Node
}

func build(values ...int) *Node {
	dummy := &Node{}
	tail := dummy
	for _, v := range values {
		tail.Next = &Node{Value: v}
		tail = tail.Next
	}
	return dummy.Next
}

func show(head *Node) string {
	var parts []string
	for n := head; n != nil; n = n.Next {
		parts = append(parts, fmt.Sprint(n.Value))
	}
	if len(parts) == 0 {
		return "boş"
	}
	return strings.Join(parts, " → ")
}

func isSorted(head *Node) bool {
	for n := head; n != nil && n.Next != nil; n = n.Next {
		if n.Value > n.Next.Value {
			return false
		}
	}
	return true
}

// split: listeyi ortadan ikiye böler (hızlı-yavaş işaretçi)
func split(head *Node) (*Node, *Node) {
	slow, fast := head, head.Next
	for fast != nil && fast.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next
	}
	second := slow.Next
	slow.Next = nil // bağı kopar
	return head, second
}

// mergeLists: iki sıralı listeyi birleştirir — yeni düğüm YOK
func mergeLists(a, b *Node) *Node {
	dummy := &Node{}
	tail := dummy

	for a != nil && b != nil {
		if a.Value <= b.Value { // <= : kararlı
			tail.Next = a
			a = a.Next
		} else {
			tail.Next = b
			b = b.Next
		}
		tail = tail.Next
	}
	if a != nil {
		tail.Next = a
	} else {
		tail.Next = b
	}
	return dummy.Next
}

// mergeSortList: O(n log n) zaman, O(log n) yığın
func mergeSortList(head *Node) *Node {
	if head == nil || head.Next == nil {
		return head
	}
	left, right := split(head)
	return mergeLists(mergeSortList(left), mergeSortList(right))
}

func main() {
	cases := [][]int{
		{5, 2, 9, 1, 7, 3},
		{1, 2, 3, 4},
		{4, 3, 2, 1},
		{3, 3, 1, 1, 2},
		{42},
		{},
	}

	for _, c := range cases {
		list := build(c...)
		fmt.Printf("%-22s → ", show(list))
		sorted := mergeSortList(list)
		fmt.Printf("%-24s sıralı: %t\n", show(sorted), isSorted(sorted))
	}
}
Çıktı
5 → 2 → 9 → 1 → 7 → 3  → 1 → 2 → 3 → 5 → 7 → 9    sıralı: true
1 → 2 → 3 → 4          → 1 → 2 → 3 → 4            sıralı: true
4 → 3 → 2 → 1          → 1 → 2 → 3 → 4            sıralı: true
3 → 3 → 1 → 1 → 2      → 1 → 1 → 2 → 3 → 3        sıralı: true
42                     → 42                       sıralı: true
boş                    → boş                      sıralı: true
ZamanO(n log n)AlanO(log n)

Bağlı listelerde birleştirmeli sıralamanın dizilere göre büyük bir avantajı vardır: Birleştirme adımı hiç ek bellek gerektirmez, çünkü yeni düğüm oluşturmak yerine var olan düğümlerin bağlantılarını değiştirirsin. Dizilerde bunu yapmak mümkün değildir; elemanları taşımak zorundasın.

Buna karşılık hızlı sıralama bağlı listelerde kötü çalışır: Rastgele erişim olmadığı için pivot seçmek ve bölümleme yapmak zordur. Bu yüzden liste sıralamasının fiilî standardı birleştirmeli sıralamadır.

Alıştırma·Üç yollu bölümleme
Orta

Çok sayıda tekrarlı eleman içeren dizilerde klasik hızlı sıralama verimsizdir. Diziyi üçe bölen bir sürüm yaz: pivottan küçükler, pivota eşitler, pivottan büyükler. Eşit elemanlar bir daha sıralamaya dâhil edilmesin.

İpucu

Hollanda bayrağı problemi olarak bilinir. Üç işaretçi tut: lt (küçüklerin sınırı), gt (büyüklerin sınırı) ve i (tarama işaretçisi).

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

import (
	"fmt"
	"math/rand/v2"
	"slices"
)

var partitionCalls int

// quickSort: klasik iki yollu
func quickSort(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	partitionCalls++
	pivot := data[hi]
	i := lo
	for j := lo; j < hi; j++ {
		if data[j] < pivot {
			data[i], data[j] = data[j], data[i]
			i++
		}
	}
	data[i], data[hi] = data[hi], data[i]
	quickSort(data, lo, i-1)
	quickSort(data, i+1, hi)
}

// quickSort3Way: eşit elemanları tek grupta toplar
func quickSort3Way(data []int, lo, hi int) {
	if lo >= hi {
		return
	}
	partitionCalls++

	pivot := data[lo]
	lt, i, gt := lo, lo, hi

	for i <= gt {
		switch {
		case data[i] < pivot:
			data[lt], data[i] = data[i], data[lt]
			lt++
			i++
		case data[i] > pivot:
			data[i], data[gt] = data[gt], data[i]
			gt--
		default:
			i++ // pivota eşit: yerinde kalsın
		}
	}
	// [lo, lt-1] küçük, [lt, gt] EŞİT (bitti), [gt+1, hi] büyük
	quickSort3Way(data, lo, lt-1)
	quickSort3Way(data, gt+1, hi)
}

func main() {
	r := rand.New(rand.NewPCG(5, 7))

	// Az sayıda farklı değer içeren büyük dizi
	const n = 3000
	fewValues := make([]int, n)
	for i := range fewValues {
		fewValues[i] = r.IntN(3) // yalnızca 0, 1, 2
	}
	manyValues := make([]int, n)
	for i := range manyValues {
		manyValues[i] = r.IntN(n)
	}

	datasets := []struct {
		name string
		data []int
	}{
		{"3 farklı değer", fewValues},
		{"n farklı değer", manyValues},
	}

	fmt.Printf("%-18s %-14s %14s %10s\n", "veri", "algoritma", "bölümleme", "sıralı")
	for _, ds := range datasets {
		a := slices.Clone(ds.data)
		partitionCalls = 0
		quickSort(a, 0, len(a)-1)
		fmt.Printf("%-18s %-14s %14d %10t\n", ds.name, "iki yollu", partitionCalls, slices.IsSorted(a))

		b := slices.Clone(ds.data)
		partitionCalls = 0
		quickSort3Way(b, 0, len(b)-1)
		fmt.Printf("%-18s %-14s %14d %10t\n", "", "üç yollu", partitionCalls, slices.IsSorted(b))
		fmt.Println()
	}

	// Doğruluk kontrolü
	demo := []int{3, 1, 3, 2, 1, 3, 2, 1}
	quickSort3Way(demo, 0, len(demo)-1)
	fmt.Println("küçük örnek:", demo)
}
Çıktı
veri               algoritma           bölümleme     sıralı
3 farklı değer     iki yollu                2997       true
                   üç yollu                    3       true

n farklı değer     iki yollu                2054       true
                   üç yollu                 1408       true

küçük örnek: [1 1 1 2 2 3 3 3]
En iyiO(n)OrtalamaO(n log n)En kötüO(n²)AlanO(log n)

Fark çarpıcı: Yalnızca üç farklı değer içeren bir dizide, klasik hızlı sıralama tekrar eden elemanları defalarca yeniden bölümlemeye devam eder. Üç yollu bölümleme ise eşit elemanları tek seferde tamamen halleder ve o bölgeye bir daha dokunmaz.

Bu iyileştirme, çok sayıda tekrarın olduğu durumda karmaşıklığı O(n × farklı değer sayısı) seviyesine indirir. Gerçek verilerde tekrarlar çok yaygındır — kategoriler, durum kodları, tarihler — bu yüzden modern sıralama uygulamaları bu tekniği içerir.

Alıştırma·Sıralama algoritması karşılaştırma laboratuvarı
Zor

Birleştirmeli, hızlı ve heap sıralamasını aynı veri kümeleri üzerinde çalıştırıp karşılaştırma sayısı, takas/taşıma sayısı ve maksimum özyineleme derinliğini ölçen bir program yaz. Beş farklı girdi deseni kullan ve sonuçları tablo hâlinde raporla.

İpucu

Her algoritma için ortak bir stats yapısı kullan. Girdi desenleri: rastgele, sıralı, ters sıralı, tümü aynı, az sayıda farklı değer.

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

import (
	"fmt"
	"math/rand/v2"
	"slices"
)

type stats struct {
	comparisons int
	moves       int
	maxDepth    int
	depth       int
}

func (s *stats) enter() {
	s.depth++
	s.maxDepth = max(s.maxDepth, s.depth)
}

func (s *stats) leave() { s.depth-- }

// --- Birleştirmeli sıralama ---

func mergeSort(data, buf []int, lo, hi int, s *stats) {
	if hi-lo <= 1 {
		return
	}
	s.enter()
	defer s.leave()

	mid := lo + (hi-lo)/2
	mergeSort(data, buf, lo, mid, s)
	mergeSort(data, buf, mid, hi, s)

	i, j, k := lo, mid, lo
	for i < mid && j < hi {
		s.comparisons++
		if data[i] <= data[j] {
			buf[k] = data[i]
			i++
		} else {
			buf[k] = data[j]
			j++
		}
		k++
		s.moves++
	}
	for i < mid {
		buf[k] = data[i]
		i, k = i+1, k+1
		s.moves++
	}
	for j < hi {
		buf[k] = data[j]
		j, k = j+1, k+1
		s.moves++
	}
	copy(data[lo:hi], buf[lo:hi])
}

// --- Hızlı sıralama (üçün medyanı) ---

func medianOfThree(data []int, lo, hi int, s *stats) {
	mid := lo + (hi-lo)/2
	s.comparisons += 3
	if data[mid] < data[lo] {
		data[mid], data[lo] = data[lo], data[mid]
		s.moves++
	}
	if data[hi] < data[lo] {
		data[hi], data[lo] = data[lo], data[hi]
		s.moves++
	}
	if data[hi] < data[mid] {
		data[hi], data[mid] = data[mid], data[hi]
		s.moves++
	}
	data[mid], data[hi] = data[hi], data[mid]
	s.moves++
}

func quickSort(data []int, lo, hi int, s *stats) {
	if lo >= hi {
		return
	}
	s.enter()
	defer s.leave()

	medianOfThree(data, lo, hi, s)
	pivot := data[hi]
	i := lo
	for j := lo; j < hi; j++ {
		s.comparisons++
		if data[j] < pivot {
			data[i], data[j] = data[j], data[i]
			s.moves++
			i++
		}
	}
	data[i], data[hi] = data[hi], data[i]
	s.moves++

	quickSort(data, lo, i-1, s)
	quickSort(data, i+1, hi, s)
}

// --- Heap sıralaması ---

func siftDown(data []int, root, size int, s *stats) {
	for {
		child := 2*root + 1
		if child >= size {
			return
		}
		if child+1 < size {
			s.comparisons++
			if data[child+1] > data[child] {
				child++
			}
		}
		s.comparisons++
		if data[root] >= data[child] {
			return
		}
		data[root], data[child] = data[child], data[root]
		s.moves++
		root = child
	}
}

func heapSort(data []int, s *stats) {
	n := len(data)
	for i := n/2 - 1; i >= 0; i-- {
		siftDown(data, i, n, s)
	}
	for end := n - 1; end > 0; end-- {
		data[0], data[end] = data[end], data[0]
		s.moves++
		siftDown(data, 0, end, s)
	}
	s.maxDepth = 1 // iteratif
}

func main() {
	const n = 1024
	r := rand.New(rand.NewPCG(11, 17))

	random := make([]int, n)
	sorted := make([]int, n)
	reversed := make([]int, n)
	allSame := make([]int, n)
	fewValues := make([]int, n)
	for i := range n {
		random[i] = r.IntN(10_000)
		sorted[i] = i
		reversed[i] = n - i
		allSame[i] = 7
		fewValues[i] = r.IntN(4)
	}

	datasets := []struct {
		name string
		data []int
	}{
		{"rastgele", random},
		{"sıralı", sorted},
		{"ters sıralı", reversed},
		{"tümü aynı", allSame},
		{"4 farklı değer", fewValues},
	}

	fmt.Printf("n = %d, teorik n·log₂n ≈ %d\n\n", n, n*10)
	fmt.Printf("%-16s %-14s %14s %12s %10s %8s\n",
		"veri", "algoritma", "karşılaştırma", "taşıma", "derinlik", "sıralı")

	for _, ds := range datasets {
		runs := []struct {
			name string
			fn   func([]int, *stats)
		}{
			{"birleştirmeli", func(d []int, s *stats) {
				buf := make([]int, len(d))
				mergeSort(d, buf, 0, len(d), s)
			}},
			{"hızlı", func(d []int, s *stats) { quickSort(d, 0, len(d)-1, s) }},
			{"heap", heapSort},
		}

		for i, run := range runs {
			data := slices.Clone(ds.data)
			var s stats
			run.fn(data, &s)

			label := ""
			if i == 0 {
				label = ds.name
			}
			fmt.Printf("%-16s %-14s %14d %12d %10d %8t\n",
				label, run.name, s.comparisons, s.moves, s.maxDepth, slices.IsSorted(data))
		}
		fmt.Println()
	}
}
Çıktı
n = 1024, teorik n·log₂n ≈ 10240

veri             algoritma       karşılaştırma       taşıma   derinlik   sıralı
rastgele         birleştirmeli            8979        10240         10     true
                 hızlı                   11369         6857         15     true
                 heap                    17280         9312          1     true

sıralı           birleştirmeli            5120        10240         10     true
                 hızlı                    9740         5121         10     true
                 heap                    18060         9968          1     true

ters sıralı      birleştirmeli            5120        10240         10     true
                 hızlı                   16442         9938         21     true
                 heap                    16407         8542          1     true

tümü aynı        birleştirmeli            5120        10240         10     true
                 hızlı                  526845         2046       1023     true
                 heap                     3066         1023          1     true

4 farklı değer   birleştirmeli            8141        10240         10     true
                 hızlı                  137550         3018        293     true
                 heap                    13911         7034          1     true

Tablodan çıkarılacak birkaç ders var. Birleştirmeli sıralamanın karşılaştırma sayısı her girdi deseninde neredeyse aynıdır — bu, garantili karmaşıklığın somut karşılığıdır. Hızlı sıralama, üçün medyanı sayesinde sıralı ve ters sıralı girdilerde de logaritmik derinlikte kalır. Heap sıralaması iteratif olduğu için özyineleme derinliği yoktur ama karşılaştırma sayısı diğerlerinden yüksektir.

En öğretici satırlar "tümü aynı" ve "4 farklı değer" durumlarıdır: Klasik hızlı sıralama burada gereksiz iş yapar; üç yollu bölümleme (önceki alıştırma) bu durumu çok daha iyi ele alır. Gerçek kütüphaneler her iki tekniği de içerir.

Son olarak bu ölçümlerin gerçek zamanı yansıtmadığını unutma: Karşılaştırma ve taşıma sayısı iyi bir modeldir ama önbellek davranışı, dallanma tahmini ve bellek ayırma maliyetleri hesaba katılmamıştır. Gerçek performansı ölçmek için go test -bench kullanılır; bunu Test ve Benchmark dersinde işledik.

Kısa sınav

Kısa sınav

Birleştirmeli sıralamanın en kötü durum karmaşıklığı nedir?

Hızlı sıralama hangi durumda O(n²)'ye düşer?

Hangi algoritma hem O(n log n) garantisi verir hem de O(1) ek bellek kullanır?

Karşılaştırma tabanlı sıralamanın alt sınırı neden Ω(n log n)'dir?

Gonun slices.Sort` fonksiyonu hangi algoritmayı kullanır?

Bağlı listeleri sıralamak için hangi algoritma en uygundur?

Özet

  • Birleştirmeli sıralama diziyi böler, her yarıyı sıralar, sonra birleştirir: her durumda O(n log n), kararlı, O(n) ek bellek.
  • Hızlı sıralama önce bölümleme yapar, sonra parçaları sıralar: pratikte en hızlı, yerinde, kararsız, en kötü durumda O(n²).
  • Pivot seçimi hızlı sıralamanın kaderini belirler; rastgele pivot veya üçün medyanı en kötü durumu pratikte ortadan kaldırır.
  • Heap sıralaması yerinde ve garantili O(n log n)'dir ama önbellek dostu değildir ve kararsızdır.
  • Karşılaştırma tabanlı sıralamanın alt sınırı Ω(n log n)'dir; bu bir bilgi teorik sınırdır, algoritma zekâsıyla aşılamaz.
  • Go'nun slices.Sort'u pdqsort kullanır: hızlı sıralama temelli, eklemeli sıralamayla hızlandırılmış, heap sıralamasıyla korunmuş melez bir algoritma.
  • Kararlılık gerekiyorsa slices.SortStableFunc, özel ölçüt gerekiyorsa slices.SortFunc kullanılır.
  • Çok tekrarlı verilerde üç yollu bölümleme belirgin kazanç sağlar.
  • Bağlı listelerde birleştirmeli sıralama, dizilerde hızlı sıralama tercih edilir.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.