Bir telefon rehberinde "Yılmaz" soyadını ararken ilk sayfadan başlayıp tek tek okumazsın. Kitabı ortadan açar, bakarsın: "M" mi? O hâlde ileri git. Sonra kalan yarının ortasını açarsın. Birkaç denemede aradığını bulursun. Bin sayfalık bir rehberde bu, en fazla on adım demektir.
Bu sezgisel davranışın adı ikili aramadır (binary search) ve bilgisayar biliminin en temel algoritmalarından biridir. Basitliği aldatıcıdır: Ünlü bir gözleme göre, bu algoritmayı ilk kez yazan programcıların büyük kısmı hatalı bir sürüm üretir. Sınır koşulları, taşma ve sonsuz döngü tuzakları sandığından çok daha sinsi.
İkili aramanın asıl gücü ise yalnızca "dizide eleman bulmak" değildir. Cevabın kendisi üzerinde de arama yapabilirsin: "Kapasitesi ne olmalı?", "En az kaç gün gerekir?" gibi sorular, doğru kurulduğunda birer ikili arama problemine dönüşür. Bu derste doğrusal aramayı, ikili aramanın doğru yazımını, alt/üst sınır varyantlarını, standart kütüphane araçlarını, cevap üzerinde aramayı ve döndürülmüş dizilerde aramayı öğreneceksin.
Doğrusal arama
En basit arama, elemanları teker teker kontrol etmektir. Yavaş görünse de iki büyük avantajı vardır: Dizinin sıralı olmasını gerektirmez ve küçük veri kümelerinde önbellek dostu olduğu için şaşırtıcı derecede hızlıdır.
package main
import (
"fmt"
"slices"
)
// linearSearch: O(n)
func linearSearch(data []int, target int) (index, comparisons int) {
for i, v := range data {
comparisons++
if v == target {
return i, comparisons
}
}
return -1, comparisons
}
// sentinelSearch: sınır kontrolünü ortadan kaldırır (klasik mikro-iyileştirme)
func sentinelSearch(data []int, target int) int {
if len(data) == 0 {
return -1
}
last := data[len(data)-1]
data[len(data)-1] = target // nöbetçi: arama mutlaka duracak
i := 0
for data[i] != target {
i++
}
data[len(data)-1] = last // diziyi geri yükle
if i < len(data)-1 || last == target {
return i
}
return -1
}
func main() {
data := []int{42, 17, 93, 8, 55, 23, 71}
fmt.Println("dizi:", data)
for _, target := range []int{42, 55, 71, 100} {
i, cmp := linearSearch(data, target)
fmt.Printf(" %-4d ara → indeks=%-3d karşılaştırma=%d\n", target, i, cmp)
}
fmt.Println()
fmt.Println("nöbetçili arama:")
for _, target := range []int{8, 71, 100} {
fmt.Printf(" %-4d → indeks=%d\n", target, sentinelSearch(slices.Clone(data), target))
}
fmt.Println()
fmt.Println("standart kütüphane:")
fmt.Println(" slices.Index(data, 55) =", slices.Index(data, 55))
fmt.Println(" slices.Contains(data, 93) =", slices.Contains(data, 93))
fmt.Println(" slices.IndexFunc (ilk 50'den büyük) =",
slices.IndexFunc(data, func(v int) bool { return v > 50 }))
}dizi: [42 17 93 8 55 23 71] 42 ara → indeks=0 karşılaştırma=1 55 ara → indeks=4 karşılaştırma=5 71 ara → indeks=6 karşılaştırma=7 100 ara → indeks=-1 karşılaştırma=7 nöbetçili arama: 8 → indeks=3 71 → indeks=6 100 → indeks=-1 standart kütüphane: slices.Index(data, 55) = 4 slices.Contains(data, 93) = true slices.IndexFunc (ilk 50'den büyük) = 2
Doğrusal aramayı küçümseme. Yirmi elemanlı bir dilimde doğrusal tarama, ikili aramadan genelde daha hızlıdır: Dallanma tahmini iyi çalışır, bellek erişimi ardışıktır ve hiçbir hazırlık gerektirmez. Sıralama maliyetini de hesaba katarsan, tek seferlik aramalarda sıralayıp ikili arama yapmak nadiren mantıklıdır.
İkili arama
Sıralı bir dizide her adımda kalan olasılıkların yarısını elersin. Bin elemanlı bir dizide en fazla on, bir milyonlukta yirmi adım yeter.
ara: 23 [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
0 1 2 3 4 5 6 7 8 9
adım 1: lo=0 hi=9 mid=4 → 16 < 23 → sağ yarı
adım 2: lo=5 hi=9 mid=7 → 56 > 23 → sol yarı
adım 3: lo=5 hi=6 mid=5 → 23 = 23 → BULUNDU
3 adımda bulundu (doğrusal aramada 6 adım gerekirdi)Aşağıdaki görselleştirmede farklı hedefler arayarak aralığın nasıl daraldığını izleyebilirsin:
package main
import "fmt"
// binarySearch: iteratif — O(log n), O(1) bellek
func binarySearch(data []int, target int) (index, steps int) {
lo, hi := 0, len(data)-1
for lo <= hi {
steps++
mid := lo + (hi-lo)/2 // TAŞMA GÜVENLİ: (lo+hi)/2 değil!
switch {
case data[mid] == target:
return mid, steps
case data[mid] < target:
lo = mid + 1
default:
hi = mid - 1
}
}
return -1, steps
}
// binarySearchRecursive: özyinelemeli — O(log n) bellek (çağrı yığını)
func binarySearchRecursive(data []int, target, lo, hi int) int {
if lo > hi {
return -1
}
mid := lo + (hi-lo)/2
switch {
case data[mid] == target:
return mid
case data[mid] < target:
return binarySearchRecursive(data, target, mid+1, hi)
default:
return binarySearchRecursive(data, target, lo, mid-1)
}
}
func main() {
data := []int{2, 5, 8, 12, 16, 23, 38, 56, 72, 91}
fmt.Println("sıralı dizi:", data)
fmt.Println()
for _, target := range []int{23, 2, 91, 50} {
i, steps := binarySearch(data, target)
r := binarySearchRecursive(data, target, 0, len(data)-1)
fmt.Printf("%-4d → indeks=%-3d adım=%d (özyinelemeli aynı: %t)\n", target, i, steps, i == r)
}
// Adım sayısı logaritmik büyür
fmt.Println()
fmt.Printf("%12s %10s %14s\n", "eleman", "ikili arama", "doğrusal (en kötü)")
for _, n := range []int{10, 100, 1_000, 1_000_000, 1_000_000_000} {
big := make([]int, n)
for i := range big {
big[i] = i * 2
}
_, steps := binarySearch(big[:min(n, 1_000_000)], -1) // bulunamayan değer: en kötü durum
if n > 1_000_000 {
steps = 30 // log₂(10⁹) ≈ 30
}
fmt.Printf("%12d %10d %14d\n", n, steps, n)
}
}sıralı dizi: [2 5 8 12 16 23 38 56 72 91]
23 → indeks=5 adım=3 (özyinelemeli aynı: true)
2 → indeks=0 adım=3 (özyinelemeli aynı: true)
91 → indeks=9 adım=4 (özyinelemeli aynı: true)
50 → indeks=-1 adım=4 (özyinelemeli aynı: true)
eleman ikili arama doğrusal (en kötü)
10 3 10
100 6 100
1000 9 1000
1000000 19 1000000
1000000000 30 1000000000Alt sınır ve üst sınır
Klasik ikili arama, hedefi bulur ama tekrarlı elemanlarda hangisini bulduğu belirsizdir. Üstelik "bulunamadı" cevabı çoğu zaman yetersizdir; asıl istediğin genelde "nereye eklenmeli" bilgisidir.
dizi: [1, 3, 3, 3, 5, 7, 7, 9]
0 1 2 3 4 5 6 7
lowerBound(3) = 1 ← 3'ün ilk göründüğü yer
upperBound(3) = 4 ← 3'ten büyük ilk elemanın yeri
sayı(3) = 4 - 1 = 3
lowerBound(4) = 4 ← 4 yok; eklenmesi gereken yer
upperBound(4) = 4 ← aynı: eleman yok demektirpackage main
import (
"fmt"
"slices"
)
// lowerBound: target'tan KÜÇÜK OLMAYAN ilk elemanın indeksi
func lowerBound(data []int, target int) int {
lo, hi := 0, len(data) // dikkat: hi = len, len-1 değil
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] < target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
// upperBound: target'tan BÜYÜK ilk elemanın indeksi
func upperBound(data []int, target int) int {
lo, hi := 0, len(data)
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] <= target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
func count(data []int, target int) int {
return upperBound(data, target) - lowerBound(data, target)
}
// insertSorted: sıralı diziye sıralı biçimde ekler
func insertSorted(data []int, value int) []int {
i := lowerBound(data, value)
return slices.Insert(data, i, value)
}
func main() {
data := []int{1, 3, 3, 3, 5, 7, 7, 9}
fmt.Println("dizi:", data)
fmt.Println()
fmt.Printf("%8s %12s %12s %8s\n", "değer", "lowerBound", "upperBound", "adet")
for _, v := range []int{1, 3, 4, 7, 9, 10, 0} {
fmt.Printf("%8d %12d %12d %8d\n", v, lowerBound(data, v), upperBound(data, v), count(data, v))
}
fmt.Println()
updated := insertSorted(slices.Clone(data), 4)
fmt.Println("4 eklendikten sonra:", updated)
updated = insertSorted(updated, 0)
fmt.Println("0 eklendikten sonra:", updated)
updated = insertSorted(updated, 100)
fmt.Println("100 eklendikten sonra:", updated)
}dizi: [1 3 3 3 5 7 7 9]
değer lowerBound upperBound adet
1 0 1 1
3 1 4 3
4 4 4 0
7 5 7 2
9 7 8 1
10 8 8 0
0 0 0 0
4 eklendikten sonra: [1 3 3 3 4 5 7 7 9]
0 eklendikten sonra: [0 1 3 3 3 4 5 7 7 9]
100 eklendikten sonra: [0 1 3 3 3 4 5 7 7 9 100]lo < hi ve hi = len(data) yazımına dikkat: Bu, klasik ikili aramadan farklı bir aralık kuralıdır. Klasik sürüm kapalı aralık [lo, hi] kullanır; sınır sürümleri yarı açık aralık [lo, hi) kullanır. İkisini karıştırmak, ikili aramadaki hataların en yaygın kaynağıdır. Bir kuralı seç ve fonksiyon boyunca tutarlı kal.
Standart kütüphane
Go bu işlemleri hazır sunar ve günlük kodda elle yazmana gerek yoktur.
package main
import (
"cmp"
"fmt"
"slices"
"sort"
)
type Product struct {
Name string
Price int
}
func main() {
nums := []int{2, 5, 8, 12, 16, 23, 38}
// slices.BinarySearch: indeks ve bulunup bulunmadığı
i, found := slices.BinarySearch(nums, 16)
fmt.Println("16 →", i, found)
i, found = slices.BinarySearch(nums, 20)
fmt.Println("20 →", i, found, "← bulunamadı, ekleneceği yer", i)
// slices.BinarySearchFunc: özel karşılaştırma
products := []Product{
{"kalem", 15}, {"defter", 40}, {"çanta", 250}, {"laptop", 18000},
}
j, ok := slices.BinarySearchFunc(products, 250, func(p Product, target int) int {
return cmp.Compare(p.Price, target)
})
fmt.Println("250 TL'lik ürün →", j, ok, products[j].Name)
// sort.Search: "koşulu sağlayan ilk indeks" — çok genel
// Koşul false...false,true...true biçiminde olmalı
firstOver20 := sort.Search(len(nums), func(k int) bool { return nums[k] > 20 })
fmt.Println("20'den büyük ilk eleman indeksi:", firstOver20, "değer:", nums[firstOver20])
// sort.Search ile lowerBound
lower := sort.Search(len(nums), func(k int) bool { return nums[k] >= 12 })
fmt.Println("12 için lowerBound:", lower)
// Sıralı olup olmadığını kontrol et: ikili arama ön koşulu
fmt.Println()
fmt.Println("dizi sıralı mı:", slices.IsSorted(nums))
unsorted := []int{3, 1, 2}
fmt.Println("sırasız dizide arama güvenilir mi:", slices.IsSorted(unsorted))
badIdx, badFound := slices.BinarySearch(unsorted, 1)
fmt.Println("sırasız dizide 1 aramak:", badIdx, badFound, "← anlamsız sonuç")
}16 → 4 true 20 → 5 false ← bulunamadı, ekleneceği yer 5 250 TL'lik ürün → 2 true çanta 20'den büyük ilk eleman indeksi: 5 değer: 23 12 için lowerBound: 3 dizi sıralı mı: true sırasız dizide arama güvenilir mi: false sırasız dizide 1 aramak: 0 false ← anlamsız sonuç
sort.Search en genel araçtır ve mantığı şudur: Bir koşul fonksiyonu verirsin, o da koşulu sağlayan ilk indeksi döndürür. Tek şart, koşulun false, false, ..., true, true biçiminde monoton olmasıdır. Bu genellik, bir sonraki bölümün kapısını açar.
Cevap üzerinde ikili arama
İşte ikili aramanın asıl gücü. Bir problemi şu kalıba sokabiliyorsan, ikili aramayla çözebilirsin:
"X değeri yeterli mi?" sorusunun cevabı monoton olmalı:
X: 1 2 3 4 5 6 7 8
yeterli? hayır hayır hayır hayır EVET EVET EVET EVET
↑
aradığımız sınırKlasik örnek: Bir kitabı belirli sayıda günde bitirmek için günde en az kaç sayfa okumalısın? Az okursan yetişmez, çok okursan yetişir — yani "yeterli mi" sorusu monotondur.
package main
import "fmt"
// daysNeeded: verilen hızda kaç gün gerekir
func daysNeeded(chapters []int, speed int) int {
days, current := 1, 0
for _, pages := range chapters {
if current+pages > speed {
days++
current = 0
}
current += pages
}
return days
}
// minSpeed: deadline günde bitirmek için gereken en düşük hız
func minSpeed(chapters []int, deadline int) int {
lo, hi := 0, 0
for _, p := range chapters {
lo = max(lo, p) // en az en uzun bölüm kadar okumalı
hi += p // en fazla hepsini bir günde
}
for lo < hi {
mid := lo + (hi-lo)/2
if daysNeeded(chapters, mid) <= deadline {
hi = mid // bu hız yeterli, daha azını dene
} else {
lo = mid + 1 // yetersiz, artır
}
}
return lo
}
// sqrtInt: tam sayı karekök — cevap üzerinde ikili arama
func sqrtInt(n int) int {
if n < 2 {
return n
}
lo, hi := 1, n/2+1
for lo < hi {
mid := lo + (hi-lo)/2
if mid*mid <= n {
lo = mid + 1
} else {
hi = mid
}
}
return lo - 1
}
// minCapacity: ağırlıkları sırayı bozmadan taşıyacak en küçük gemi kapasitesi
func minCapacity(weights []int, days int) int {
lo, hi := 0, 0
for _, w := range weights {
lo = max(lo, w)
hi += w
}
needed := func(capacity int) int {
used, load := 1, 0
for _, w := range weights {
if load+w > capacity {
used++
load = 0
}
load += w
}
return used
}
for lo < hi {
mid := lo + (hi-lo)/2
if needed(mid) <= days {
hi = mid
} else {
lo = mid + 1
}
}
return lo
}
func main() {
chapters := []int{30, 45, 20, 60, 25, 40}
fmt.Println("bölüm sayfaları:", chapters, "toplam:", 220)
for _, deadline := range []int{1, 2, 3, 6} {
speed := minSpeed(chapters, deadline)
fmt.Printf(" %d günde bitirmek için günde en az %3d sayfa (gerçekte %d gün sürer)\n",
deadline, speed, daysNeeded(chapters, speed))
}
fmt.Println()
fmt.Println("tam sayı karekök:")
for _, n := range []int{0, 1, 8, 16, 17, 99, 100, 1_000_000} {
fmt.Printf(" √%-9d = %d\n", n, sqrtInt(n))
}
fmt.Println()
weights := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("paket ağırlıkları:", weights)
for _, d := range []int{1, 5, 10} {
fmt.Printf(" %2d günde taşımak için gereken kapasite: %d\n", d, minCapacity(weights, d))
}
}bölüm sayfaları: [30 45 20 60 25 40] toplam: 220 1 günde bitirmek için günde en az 220 sayfa (gerçekte 1 gün sürer) 2 günde bitirmek için günde en az 125 sayfa (gerçekte 2 gün sürer) 3 günde bitirmek için günde en az 80 sayfa (gerçekte 3 gün sürer) 6 günde bitirmek için günde en az 60 sayfa (gerçekte 6 gün sürer) tam sayı karekök: √0 = 0 √1 = 1 √8 = 2 √16 = 4 √17 = 4 √99 = 9 √100 = 10 √1000000 = 1000 paket ağırlıkları: [1 2 3 4 5 6 7 8 9 10] 1 günde taşımak için gereken kapasite: 55 5 günde taşımak için gereken kapasite: 15 10 günde taşımak için gereken kapasite: 10
Bu desene "cevap üzerinde ikili arama" ya da "parametrik arama" denir. Tanıma işaretleri şunlardır: Problem "en küçük/en büyük şu değeri bul" diye soruyor, ve belirli bir değerin geçerli olup olmadığını kontrol etmek kolay. Bu iki koşul sağlanıyorsa, olası cevaplar üzerinde ikili arama yapabilirsin.
Kurulum sırasında dikkat edilecek üç nokta var: Arama aralığının alt ve üst sınırlarını doğru belirlemek, kontrol fonksiyonunun gerçekten monoton olduğundan emin olmak, ve döngü bittiğinde lo'nun aradığın sınır olduğunu doğrulamak.
Döndürülmüş dizide arama
Sıralı bir dizi bir noktadan döndürülmüşse ([4,5,6,7,0,1,2]) hâlâ ikili arama yapabilirsin. Anahtar gözlem şudur: Diziyi ortadan böldüğünde en az bir yarı kesinlikle sıralıdır.
package main
import "fmt"
// searchRotated: döndürülmüş sıralı dizide arama — O(log n)
func searchRotated(data []int, target int) int {
lo, hi := 0, len(data)-1
for lo <= hi {
mid := lo + (hi-lo)/2
if data[mid] == target {
return mid
}
if data[lo] <= data[mid] { // sol yarı sıralı
if data[lo] <= target && target < data[mid] {
hi = mid - 1 // hedef sıralı yarıda
} else {
lo = mid + 1
}
} else { // sağ yarı sıralı
if data[mid] < target && target <= data[hi] {
lo = mid + 1
} else {
hi = mid - 1
}
}
}
return -1
}
// findRotationPoint: en küçük elemanın indeksi (dönüş noktası)
func findRotationPoint(data []int) int {
lo, hi := 0, len(data)-1
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] > data[hi] {
lo = mid + 1 // en küçük sağda
} else {
hi = mid // en küçük mid'de veya solunda
}
}
return lo
}
// findPeak: tepe elemanı bul (komşularından büyük)
func findPeak(data []int) int {
lo, hi := 0, len(data)-1
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] < data[mid+1] {
lo = mid + 1 // yokuş yukarı: tepe sağda
} else {
hi = mid // yokuş aşağı: tepe burada veya solda
}
}
return lo
}
func main() {
rotated := []int{4, 5, 6, 7, 0, 1, 2}
fmt.Println("döndürülmüş dizi:", rotated)
fmt.Println("dönüş noktası indeksi:", findRotationPoint(rotated),
"değer:", rotated[findRotationPoint(rotated)])
fmt.Println()
for _, target := range []int{0, 4, 2, 7, 3} {
fmt.Printf(" %d ara → indeks %d\n", target, searchRotated(rotated, target))
}
fmt.Println()
fmt.Println("farklı dönüş miktarları:")
for _, d := range [][]int{
{1, 2, 3, 4, 5},
{5, 1, 2, 3, 4},
{3, 4, 5, 1, 2},
{2, 3, 4, 5, 1},
} {
fmt.Printf(" %v → dönüş noktası: %d (en küçük: %d)\n",
d, findRotationPoint(d), d[findRotationPoint(d)])
}
fmt.Println()
peaks := []int{1, 3, 7, 12, 9, 5, 2}
p := findPeak(peaks)
fmt.Println("dizi:", peaks)
fmt.Println("tepe indeksi:", p, "değer:", peaks[p])
}döndürülmüş dizi: [4 5 6 7 0 1 2] dönüş noktası indeksi: 4 değer: 0 0 ara → indeks 4 4 ara → indeks 0 2 ara → indeks 6 7 ara → indeks 3 3 ara → indeks -1 farklı dönüş miktarları: [1 2 3 4 5] → dönüş noktası: 0 (en küçük: 1) [5 1 2 3 4] → dönüş noktası: 1 (en küçük: 1) [3 4 5 1 2] → dönüş noktası: 3 (en küçük: 1) [2 3 4 5 1] → dönüş noktası: 4 (en küçük: 1) dizi: [1 3 7 12 9 5 2] tepe indeksi: 3 değer: 12
Bu üç problem aynı fikri paylaşır: Her adımda, aradığın şeyin hangi yarıda olduğunu kesin olarak söyleyebiliyorsan ikili arama uygulanabilir. Dizinin tamamen sıralı olması gerekmez; yeterli olan, her adımda bir yarıyı güvenle elemenin mümkün olmasıdır.
İkili aramayı doğru yazmak
Bu algoritmanın hata yapmaya bu kadar müsait olması tesadüf değil. Küçük bir kod parçasında birbirine bağlı birkaç karar vardır ve biri yanlış olduğunda sonuç ya yanlış cevap ya sonsuz döngüdür. Doğru yazmanın yolu, bu kararları bilinçli olarak vermekten geçer.
Aralık kuralını baştan seç. Kapalı aralık mı kullanacaksın, yarı açık mı? Bu seçim, başlangıç değerlerini, döngü koşulunu ve sınır güncellemelerini birlikte belirler. Kapalı aralıkta üst sınır son indeks olur, döngü koşulu küçük-eşit olur ve sınırlar bir eksiltilip artırılarak güncellenir. Yarı açık aralıkta üst sınır uzunluk olur, koşul kesin küçüktür ve üst sınır orta noktaya doğrudan atanır. İkisini karıştırmak en yaygın hatadır.
Her yinelemede aralığın küçüldüğünden emin ol. Sonsuz döngülerin tek sebebi budur: Orta nokta sınırlardan birine eşit kalıyor ve güncelleme aralığı daraltmıyor. Kodunu yazdıktan sonra iki elemanlı bir dizide zihinsel olarak çalıştır; sorun varsa orada ortaya çıkar.
Ne aradığını netleştir. "Var mı" sorusu ile "nereye eklenmeli" sorusu farklı cevaplar ister. Tekrarlı elemanlarda "ilki" mi "sonuncusu" mu istediğine karar vermeden kod yazmaya başlama.
Sınır durumlarını sına. Boş dizi, tek elemanlı dizi, aranan değerin en başta olması, en sonda olması, hiç olmaması, tüm elemanların aynı olması. Altı durum var ve hataların neredeyse tamamı bunlardan birinde ortaya çıkar.
Ön koşulu unutma. İkili arama yalnızca sıralı veride doğru çalışır. Sırasız bir dizide hata vermez — sadece anlamsız bir cevap döndürür ki bu çok daha tehlikelidir. Veriyi sen sıralamıyorsan, sıralı geldiğinden emin ol.
Son bir tavsiye: Günlük kodda standart kütüphaneyi kullan. İkili aramayı elle yazmak, öğrenmek ve özel varyantlar için gereklidir; ama sıradan bir arama için hazır ve iyi test edilmiş fonksiyonlar dururken kendi sürümünü yazmak gereksiz risktir.
Sık yapılan hatalar
mid = (lo + hi) / 2yazmak. Büyük indekslerde taşma riski vardır;lo + (hi-lo)/2kullan.- Aralık kurallarını karıştırmak. Kapalı ve yarı açık aralık yazımlarını aynı fonksiyonda harmanlamak sonsuz döngü üretir.
- Sırasız dizide ikili arama yapmak. Hata alınmaz, yalnızca yanlış cevap döner.
- Tekrarlı elemanlarda hangi indeksin döndüğünü varsaymak. Klasik sürüm rastgele birini bulur; ilkini veya sonuncusunu istiyorsan sınır varyantlarını kullan.
- Cevap üzerinde aramada monotonluğu doğrulamamak. Kontrol fonksiyonu monoton değilse sonuç anlamsızdır.
- Küçük dizilerde ikili aramayı zorlamak. Yirmi elemanda doğrusal tarama genelde daha hızlıdır.
- Sıralama maliyetini göz ardı etmek. Tek bir arama için sıralamak, doğrudan taramaktan pahalıdır.
Alıştırmalar
Sıralı ve tekrarlı elemanlar içeren bir dizide, bir değerin ilk ve son göründüğü indeksleri bulan bir fonksiyon yaz. Değer yoksa (-1, -1) döndür. İki ikili arama kullan.
İpucu
İlk konum için alt sınır, son konum için üst sınır aramasını kullan. Üst sınırın bir eksiği son konumu verir.
Çözümü göster
package main
import "fmt"
func lowerBound(data []int, target int) int {
lo, hi := 0, len(data)
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] < target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
func upperBound(data []int, target int) int {
lo, hi := 0, len(data)
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] <= target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
// searchRange: ilk ve son konum
func searchRange(data []int, target int) (first, last int) {
lo := lowerBound(data, target)
if lo == len(data) || data[lo] != target {
return -1, -1
}
return lo, upperBound(data, target) - 1
}
func main() {
data := []int{1, 2, 2, 2, 3, 5, 5, 8, 8, 8, 8, 9}
fmt.Println("dizi:", data)
fmt.Println()
fmt.Printf("%8s %8s %8s %8s\n", "değer", "ilk", "son", "adet")
for _, v := range []int{2, 5, 8, 1, 9, 4, 100} {
first, last := searchRange(data, v)
count := 0
if first != -1 {
count = last - first + 1
}
fmt.Printf("%8d %8d %8d %8d\n", v, first, last, count)
}
fmt.Println()
fmt.Println("boş dizi:", func() string {
f, l := searchRange(nil, 5)
return fmt.Sprint(f, l)
}())
fmt.Println("tek elemanlı, eşleşen:", func() string {
f, l := searchRange([]int{7}, 7)
return fmt.Sprint(f, l)
}())
fmt.Println("hepsi aynı:", func() string {
f, l := searchRange([]int{4, 4, 4, 4}, 4)
return fmt.Sprint(f, l)
}())
}dizi: [1 2 2 2 3 5 5 8 8 8 8 9]
değer ilk son adet
2 1 3 3
5 5 6 2
8 7 10 4
1 0 0 1
9 11 11 1
4 -1 -1 0
100 -1 -1 0
boş dizi: -1 -1
tek elemanlı, eşleşen: 0 0
hepsi aynı: 0 3İki ayrı ikili arama yapmak, tek geçişte hem ilk hem son konumu bulmaya çalışmaktan çok daha sade bir koddur ve karmaşıklık yine logaritmik kalır. Sınır aramalarını bir kez doğru yazdığında, tekrarlı elemanlarla ilgili pek çok problemi bunların üzerine kurabilirsin.
0'dan n'e kadar olan sayılardan biri eksik olacak şekilde sıralı bir dizi verilmiş. Eksik sayıyı ikili aramayla O(log n) sürede bul. Ayrıca aynı problemi toplam formülüyle O(n) sürede çözüp sonuçları karşılaştır.
İpucu
Eksik sayıdan önceki her indekste data[i] == i olur; sonrasında data[i] > i. Bu, monoton bir koşuldur.
Çözümü göster
package main
import "fmt"
// missingBinary: O(log n)
func missingBinary(data []int) int {
lo, hi := 0, len(data)
for lo < hi {
mid := lo + (hi-lo)/2
if data[mid] == mid {
lo = mid + 1 // buraya kadar eksik yok
} else {
hi = mid // eksik burada veya solda
}
}
return lo
}
// missingSum: O(n) — Gauss toplam formülü
func missingSum(data []int) int {
n := len(data)
expected := n * (n + 1) / 2
actual := 0
for _, v := range data {
actual += v
}
return expected - actual
}
// missingXOR: O(n) — taşma riski yok
func missingXOR(data []int) int {
result := len(data)
for i, v := range data {
result ^= i ^ v
}
return result
}
func main() {
cases := [][]int{
{0, 1, 2, 4, 5, 6}, // 3 eksik
{1, 2, 3, 4}, // 0 eksik
{0, 1, 2, 3}, // 4 eksik (sonda)
{0}, // 1 eksik
{1}, // 0 eksik
}
fmt.Printf("%-20s %10s %10s %10s %8s\n", "dizi", "ikili", "toplam", "XOR", "aynı")
for _, c := range cases {
b := missingBinary(c)
s := missingSum(c)
x := missingXOR(c)
fmt.Printf("%-20v %10d %10d %10d %8t\n", c, b, s, x, b == s && s == x)
}
// Büyük dizide adım farkı
fmt.Println()
n := 1_000_000
big := make([]int, 0, n)
for i := 0; i <= n; i++ {
if i != 777_777 {
big = append(big, i)
}
}
fmt.Println("1 milyon elemanlı dizide eksik sayı:", missingBinary(big))
fmt.Println("ikili arama ~20 adım, toplam yöntemi 1.000.000 adım gerektirir")
}dizi ikili toplam XOR aynı [0 1 2 4 5 6 ] 3 3 3 true [1 2 3 4 ] 0 0 0 true [0 1 2 3 ] 4 4 4 true [0 ] 1 1 1 true [1 ] 0 0 0 true 1 milyon elemanlı dizide eksik sayı: 777777 ikili arama ~20 adım, toplam yöntemi 1.000.000 adım gerektirir
Üç çözüm de doğrudur ama farklı üstünlükleri vardır. İkili arama en hızlısıdır ama dizinin sıralı olmasını gerektirir. Toplam formülü sırasız dizilerde de çalışır ama çok büyük n değerlerinde taşma riski taşır. XOR çözümü hem sırasız dizilerde çalışır hem taşma riski yoktur — aynı sayı iki kez XOR'lanınca birbirini götürür, geriye yalnızca eksik olan kalır. Bu numarayı Bit Manipülasyonu dersinde ayrıntısıyla bulabilirsin.
Bir dizi görevin süreleri ve çalışan sayısı verilmiş. Görevleri sırayı bozmadan çalışanlara bölüştürürken, en çok yüklenen çalışanın toplam süresini en aza indiren dağılımı bul. Cevap üzerinde ikili arama kullan ve bulunan dağılımı da göster.
İpucu
"Maksimum yük X olabilir mi?" sorusu monotondur: X büyüdükçe daha kolay sağlanır. Kontrol fonksiyonu açgözlü çalışsın — sığdığı sürece mevcut çalışana ekle.
Çözümü göster
package main
import "fmt"
// workersNeeded: maksimum yük limit olursa kaç çalışan gerekir
func workersNeeded(tasks []int, limit int) int {
workers, load := 1, 0
for _, t := range tasks {
if load+t > limit {
workers++
load = 0
}
load += t
}
return workers
}
// minMaxLoad: en çok yüklenen çalışanın süresini en aza indirir
func minMaxLoad(tasks []int, workers int) int {
lo, hi := 0, 0
for _, t := range tasks {
lo = max(lo, t) // en az en uzun görev kadar
hi += t // en fazla hepsi tek çalışanda
}
for lo < hi {
mid := lo + (hi-lo)/2
if workersNeeded(tasks, mid) <= workers {
hi = mid // bu limit yeterli, daha azını dene
} else {
lo = mid + 1
}
}
return lo
}
// distribute: bulunan limite göre görevleri böler
func distribute(tasks []int, limit int) [][]int {
var out [][]int
current := []int{}
load := 0
for _, t := range tasks {
if load+t > limit {
out = append(out, current)
current = []int{}
load = 0
}
current = append(current, t)
load += t
}
if len(current) > 0 {
out = append(out, current)
}
return out
}
func sum(xs []int) int {
total := 0
for _, x := range xs {
total += x
}
return total
}
func main() {
tasks := []int{7, 2, 5, 10, 8, 3, 6}
fmt.Println("görev süreleri:", tasks, "| toplam:", sum(tasks))
fmt.Println()
for _, workers := range []int{1, 2, 3, 4, 7, 10} {
limit := minMaxLoad(tasks, workers)
groups := distribute(tasks, limit)
fmt.Printf("%2d çalışan → maksimum yük: %2d\n", workers, limit)
for i, g := range groups {
fmt.Printf(" çalışan %d: %-18v toplam=%d\n", i+1, g, sum(g))
}
}
fmt.Println()
fmt.Println("kontrol: çalışan sayısı arttıkça maksimum yük azalır (veya sabit kalır)")
prev := 1 << 60
monotone := true
for w := 1; w <= 10; w++ {
cur := minMaxLoad(tasks, w)
if cur > prev {
monotone = false
}
prev = cur
}
fmt.Println("monotonluk doğrulandı:", monotone)
}görev süreleri: [7 2 5 10 8 3 6] | toplam: 41
1 çalışan → maksimum yük: 41
çalışan 1: [7 2 5 10 8 3 6 ] toplam=41
2 çalışan → maksimum yük: 24
çalışan 1: [7 2 5 10 ] toplam=24
çalışan 2: [8 3 6 ] toplam=17
3 çalışan → maksimum yük: 17
çalışan 1: [7 2 5 ] toplam=14
çalışan 2: [10 ] toplam=10
çalışan 3: [8 3 6 ] toplam=17
4 çalışan → maksimum yük: 14
çalışan 1: [7 2 5 ] toplam=14
çalışan 2: [10 ] toplam=10
çalışan 3: [8 3 ] toplam=11
çalışan 4: [6 ] toplam=6
7 çalışan → maksimum yük: 10
çalışan 1: [7 2 ] toplam=9
çalışan 2: [5 ] toplam=5
çalışan 3: [10 ] toplam=10
çalışan 4: [8 ] toplam=8
çalışan 5: [3 6 ] toplam=9
10 çalışan → maksimum yük: 10
çalışan 1: [7 2 ] toplam=9
çalışan 2: [5 ] toplam=5
çalışan 3: [10 ] toplam=10
çalışan 4: [8 ] toplam=8
çalışan 5: [3 6 ] toplam=9
kontrol: çalışan sayısı arttıkça maksimum yük azalır (veya sabit kalır)
monotonluk doğrulandı: trueBu problem, cevap üzerinde ikili aramanın ders kitabı örneğidir. Doğrudan "en iyi dağılımı bul" diye düşünürsen problem karmaşık görünür — olası tüm bölünmeleri denemek üstel sayıda seçenek demektir. Ama soruyu tersine çevirip "maksimum yük X olabilir mi?" diye sorduğunda, cevap basit ve açgözlü bir kontrolle bulunur. Geriye kalan iş, en küçük geçerli X'i ikili aramayla bulmaktır.
Alt sınırın en uzun görev süresi olduğuna dikkat et: Hiçbir çalışan tek bir görevi bölemeyeceği için maksimum yük en azından o kadar olmak zorundadır. Üst sınır ise tüm görevlerin tek çalışana verilmesidir. Doğru sınırları seçmek, hem doğruluk hem verimlilik açısından önemlidir.
Kısa sınav
İkili aramada orta noktayı neden lo + (hi-lo)/2 şeklinde hesaplarız?
lowerBound fonksiyonu ne döndürür?
İkili arama sırasız bir dizide çalıştırılırsa ne olur?
Cevap üzerinde ikili arama yapabilmek için ne gerekir?
Döndürülmüş sıralı bir dizide ikili arama neden hâlâ mümkündür?
20 elemanlı bir dilimde arama yapmak için hangisi genelde daha hızlıdır?
Özet
- Doğrusal arama O(n)'dir, sıralı veri gerektirmez ve küçük dizilerde pratikte en hızlı seçenektir.
- İkili arama sıralı veride O(log n) çalışır: Bir milyon elemanda yalnızca yirmi adım.
- Orta noktayı
lo + (hi-lo)/2ile hesapla; aralık kuralını (kapalı veya yarı açık) seç ve tutarlı kal. - Alt sınır, hedeften küçük olmayan ilk elemanı; üst sınır, hedeften büyük ilk elemanı verir. Farkları eleman sayısını verir.
slices.BinarySearchvesort.Searchgünlük kullanım için hazır ve iyi test edilmiş araçlardır.- Cevap üzerinde ikili arama, "en küçük geçerli değeri bul" problemlerini monoton bir kontrol fonksiyonuyla çözer.
- Döndürülmüş dizilerde her adımda bir yarı sıralıdır; bu yeterlidir.
- İkili aramanın ön koşulu sıralılıktır; sağlanmazsa hata değil, yanlış cevap alırsın.
- Sınır durumlarını mutlaka sına: boş dizi, tek eleman, en baştaki ve en sondaki değerler, hiç bulunmayan değer.