Ö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.
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
}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.84Birleş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:
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:])
}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]
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)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")
}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] ✓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")
}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
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.
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)
}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
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
jdö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
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
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))
}
}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
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.
Ç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
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)
}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]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.
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
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()
}
}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 trueTablodan çı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
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 gerekiyorsaslices.SortFunckullanı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.