Bu yolculuğun sonuna geldin. Veri yapılarını, algoritmaları, karmaşıklık analizini ve Go'yu öğrendin. Ama bir konu daha var ve belki de en önemlisi: Bu bilgiyi daha önce hiç görmediğin bir problemde nasıl kullanacaksın?
Algoritma bilgisi ile problem çözme becerisi farklı şeylerdir. İkili aramayı ezbere bilmek, bir problemin ikili arama gerektirdiğini fark etmekle aynı şey değil. Deneyimli problem çözücülerin yaptığı şey sihir değildir; sistematik bir süreç izler ve desen tanıma yaparlar.
Bu ders o süreci anlatıyor. Problemi anlamayı, kısıtlardan hedef karmaşıklığı çıkarmayı, desenleri tanımayı, kaba kuvvetten optimal çözüme ilerlemeyi, kenar durumlarını yakalamayı ve Go'da hızlı giriş/çıkış yapmayı öğreneceksin. Sonunda da nasıl çalışmaya devam edeceğine dair bir plan bulacaksın.
Problemi anlamak
En sık yapılan hata, problemi tam anlamadan kod yazmaya başlamaktır. Aşağıdaki adımlar zaman kaybı gibi görünür ama net kazanç sağlar.
Problemi kendi cümlelerinle yeniden yaz. Metni okuyup "tamam anladım" demek yetmez. Bir cümleyle özetlemeye çalış: "Bu problem, bir dizide toplamı hedefe eşit olan iki eleman aramamı istiyor." Özetleyemiyorsan anlamadın demektir.
Girdi ve çıktıyı kesinleştir. Girdi ne türde, hangi aralıkta, sıralı mı, tekrarlı eleman olabilir mi? Çıktı tek bir değer mi, bir liste mi, bir yol mu? Birden çok geçerli cevap varsa hangisi isteniyor?
Küçük bir örnek üzerinde elle çalış. Bu, en değerli adımdır. Üç-beş elemanlı bir örnekte cevabı kâğıt üzerinde bul. Bu süreçte problemin yapısını fark edersin — ve genelde çözümün fikri de burada doğar.
Belirsizlikleri sor ya da varsayımını yaz. Girdi boş olabilir mi? Negatif sayı gelebilir mi? Çakışan aralıklar mümkün mü? Cevabı bilmiyorsan varsayımını açıkça belirt; sonradan değişmesi gerekirse nereye bakacağını bilirsin.
package main
import "fmt"
// Örnek problem: "Bir dizide toplamı hedefe eşit iki eleman var mı?"
//
// SORULAR:
// - Dizi sıralı mı? → hayır, varsayalım sırasız
// - Aynı eleman iki kez? → hayır, farklı indeksler
// - Negatif sayı olabilir mi? → evet
// - Boş dizi? → cevap: yok
// - Birden çok çift varsa? → herhangi biri yeterli
// Elle çalışma:
// dizi = [2, 7, 11, 15], hedef = 9
// 2 + 7 = 9 ✓ → indeksler (0, 1)
//
// dizi = [3, 3], hedef = 6
// 3 + 3 = 6 ✓ → indeksler (0, 1) ← tekrarlı değerler sorun değil
//
// dizi = [1, 2], hedef = 100
// yok
func twoSum(nums []int, target int) (int, int, bool) {
seen := make(map[int]int, len(nums)) // değer → indeks
for i, v := range nums {
if j, ok := seen[target-v]; ok {
return j, i, true
}
seen[v] = i
}
return 0, 0, false
}
func main() {
cases := []struct {
nums []int
target int
}{
{[]int{2, 7, 11, 15}, 9},
{[]int{3, 3}, 6},
{[]int{1, 2}, 100},
{[]int{}, 0},
{[]int{-3, 4, 3, 90}, 0},
{[]int{5}, 5},
}
for _, c := range cases {
i, j, ok := twoSum(c.nums, c.target)
if !ok {
fmt.Printf("%-18v hedef=%-5d → bulunamadı\n", c.nums, c.target)
continue
}
fmt.Printf("%-18v hedef=%-5d → indeks (%d,%d), değerler %d+%d\n",
c.nums, c.target, i, j, c.nums[i], c.nums[j])
}
fmt.Println()
fmt.Println("Son iki durum, elle çalışmadan düşünülmesi kolay atlanan kenar durumlar:")
fmt.Println(" boş dizi ve tek elemanlı dizi")
}[2 7 11 15 ] hedef=9 → indeks (0,1), değerler 2+7 [3 3 ] hedef=6 → indeks (0,1), değerler 3+3 [1 2 ] hedef=100 → bulunamadı [] hedef=0 → bulunamadı [-3 4 3 90 ] hedef=0 → indeks (0,2), değerler -3+3 [5 ] hedef=5 → bulunamadı Son iki durum, elle çalışmadan düşünülmesi kolay atlanan kenar durumlar: boş dizi ve tek elemanlı dizi
Kısıtlardan karmaşıklık çıkarmak
Bu, deneyimli problem çözücülerin en güçlü aracıdır: Girdi boyutuna bakarak hangi karmaşıklığın kabul edilebilir olduğunu tahmin etmek. Modern bir bilgisayar saniyede kabaca 10⁸ temel işlem yapar; bu sayıdan geriye doğru hesaplarsın.
n değeri kabul edilebilir karmaşıklık tipik yaklaşım
─────────────────────────────────────────────────────────────────
n ≤ 10 O(n!) tüm permütasyonlar
n ≤ 20 O(2ⁿ) alt kümeler, bit maskesi
n ≤ 100 O(n⁴) 4 iç içe döngü
n ≤ 500 O(n³) Floyd-Warshall, aralık DP
n ≤ 5.000 O(n²) iki iç içe döngü, klasik DP
n ≤ 10⁶ O(n log n) sıralama, heap, ikili arama
n ≤ 10⁷ O(n) tek geçiş, iki işaretçi
n > 10⁸ O(log n) veya O(1) matematik, önceden hesaplamaBu tablo tersten de okunur ve orada gerçek değerini gösterir: Problemi gördüğünde kısıta bakıp "bu n ile O(n²) olmaz, O(n log n) bulmalıyım" diyebilirsin. Bu bilgi, hangi araçları deneyeceğini daraltır.
package main
import "fmt"
func main() {
const opsPerSecond = 100_000_000 // ~10^8
complexities := []struct {
name string
calc func(n int) float64
}{
{"O(log n)", func(n int) float64 { return log2(n) }},
{"O(n)", func(n int) float64 { return float64(n) }},
{"O(n log n)", func(n int) float64 { return float64(n) * log2(n) }},
{"O(n²)", func(n int) float64 { return float64(n) * float64(n) }},
{"O(n³)", func(n int) float64 { return float64(n) * float64(n) * float64(n) }},
{"O(2^n)", func(n int) float64 {
if n > 60 {
return 1e30
}
return pow2(n)
}},
}
sizes := []int{10, 100, 1_000, 100_000, 1_000_000}
fmt.Printf("%-12s", "karmaşıklık")
for _, n := range sizes {
fmt.Printf("%14s", fmt.Sprint("n=", n))
}
fmt.Println()
for _, c := range complexities {
fmt.Printf("%-12s", c.name)
for _, n := range sizes {
ops := c.calc(n)
seconds := ops / opsPerSecond
fmt.Printf("%14s", format(seconds))
}
fmt.Println()
}
fmt.Println()
fmt.Println("Okuma: 'anlık' = ihmal edilebilir, 'ÇOK UZUN' = pratik değil")
fmt.Println()
fmt.Println("Pratik kural: yarışma ve mülakatlarda süre sınırı genelde 1-2 saniyedir.")
fmt.Println("Kısıta bakıp hedef karmaşıklığı belirle, sonra o karmaşıklıkta çözüm ara.")
}
func log2(n int) float64 {
count := 0.0
for n > 1 {
n /= 2
count++
}
return count
}
func pow2(n int) float64 {
result := 1.0
for range n {
result *= 2
}
return result
}
func format(seconds float64) string {
switch {
case seconds < 0.001:
return "anlık"
case seconds < 1:
return fmt.Sprintf("%.0f ms", seconds*1000)
case seconds < 60:
return fmt.Sprintf("%.1f sn", seconds)
case seconds < 3600:
return fmt.Sprintf("%.0f dk", seconds/60)
case seconds < 86400*365:
return fmt.Sprintf("%.0f gün", seconds/86400)
default:
return "ÇOK UZUN"
}
}karmaşıklık n=10 n=100 n=1000 n=100000 n=1000000 O(log n) anlık anlık anlık anlık anlık O(n) anlık anlık anlık 1 ms 10 ms O(n log n) anlık anlık anlık 16 ms 190 ms O(n²) anlık anlık 10 ms 2 dk 0 gün O(n³) anlık 10 ms 10.0 sn 116 gün ÇOK UZUN O(2^n) anlık ÇOK UZUN ÇOK UZUN ÇOK UZUN ÇOK UZUN Okuma: 'anlık' = ihmal edilebilir, 'ÇOK UZUN' = pratik değil Pratik kural: yarışma ve mülakatlarda süre sınırı genelde 1-2 saniyedir. Kısıta bakıp hedef karmaşıklığı belirle, sonra o karmaşıklıkta çözüm ara.
Tablodaki en çarpıcı satır O(2^n)'dir: n = 100 için evrenin yaşından uzun sürer. Buna karşılık O(n log n), bir milyon eleman için milisaniyeler alır. Bu fark, algoritma seçiminin donanımdan neden daha önemli olduğunu gösterir — daha hızlı bilgisayar almak O(2^n) problemini çözmez.
Desen tanıma tablosu
Problemin metnindeki ifadeler, hangi aracı kullanacağına dair güçlü ipuçları verir. Aşağıdaki tablo, bu dersler boyunca gördüğün tüm araçları bir arada topluyor.
Bu tablo bir reçete değil, bir başlangıç noktasıdır. Bazı problemler birden çok aracı birleştirir; bazıları hiçbir kalıba tam uymaz. Ama tabloyu tarayarak aday çözümler üretmek, boş sayfaya bakmaktan çok daha verimlidir.
Kaba kuvvetten optimal çözüme
Deneyimli problem çözücülerin en önemli alışkanlığı şudur: Önce çalışan bir çözüm yaz, sonra iyileştir. Doğrudan optimal çözümü hedeflemek çoğu zaman tıkanmayla sonuçlanır.
package main
import "fmt"
// PROBLEM: Bir dizide, toplamı en büyük olan bitişik alt diziyi bul.
// ADIM 1 — KABA KUVVET: tüm alt dizileri dene
// O(n³): her başlangıç, her bitiş, her toplam
func bruteForce(nums []int) int {
if len(nums) == 0 {
return 0
}
best := nums[0]
for i := range nums {
for j := i; j < len(nums); j++ {
sum := 0
for k := i; k <= j; k++ {
sum += nums[k]
}
best = max(best, sum)
}
}
return best
}
// ADIM 2 — TEKRAR EDEN İŞİ ORTADAN KALDIR: toplamı artımlı hesapla
// O(n²)
func improved(nums []int) int {
if len(nums) == 0 {
return 0
}
best := nums[0]
for i := range nums {
sum := 0
for j := i; j < len(nums); j++ {
sum += nums[j] // önceki toplamı yeniden kullan
best = max(best, sum)
}
}
return best
}
// ADIM 3 — YAPIYI FARK ET: "buraya kadarki en iyi" bilgisini taşı
// O(n) — Kadane algoritması
func optimal(nums []int) int {
if len(nums) == 0 {
return 0
}
best, current := nums[0], nums[0]
for _, v := range nums[1:] {
// Ya yeni baştan başla ya mevcut diziyi genişlet
current = max(v, current+v)
best = max(best, current)
}
return best
}
func main() {
cases := [][]int{
{-2, 1, -3, 4, -1, 2, 1, -5, 4},
{1, 2, 3},
{-5, -2, -8},
{5},
{},
}
fmt.Printf("%-32s %10s %10s %10s %8s\n", "dizi", "O(n³)", "O(n²)", "O(n)", "aynı")
for _, c := range cases {
a, b, d := bruteForce(c), improved(c), optimal(c)
fmt.Printf("%-32v %10d %10d %10d %8t\n", c, a, b, d, a == b && b == d)
}
// Büyük girdide fark
fmt.Println()
n := 3000
big := make([]int, n)
for i := range big {
big[i] = (i*7919)%201 - 100 // deterministik, negatifler dâhil
}
fmt.Printf("n=%d için üç çözümün sonucu:\n", n)
fmt.Println(" O(n³):", bruteForce(big[:300]), "(yalnızca 300 eleman, tamamı çok yavaş olurdu)")
fmt.Println(" O(n²):", improved(big))
fmt.Println(" O(n): ", optimal(big))
fmt.Println()
fmt.Println("İşlem sayısı tahmini (n=3000):")
fmt.Println(" O(n³) ≈", 3000*3000*3000/1_000_000, "milyon işlem → dakikalar")
fmt.Println(" O(n²) ≈", 3000*3000/1_000_000, "milyon işlem → anlık")
fmt.Println(" O(n) ≈", 3000, "işlem → ölçülemez")
fmt.Println()
fmt.Println("STRATEJİ: Kaba kuvvetten optimal çözüme giden yol")
fmt.Println(" 1. Çalışan bir çözüm yaz (doğruluğu garanti et)")
fmt.Println(" 2. Tekrar eden işi bul ve ortadan kaldır")
fmt.Println(" 3. Problemin yapısal özelliğini keşfet")
fmt.Println(" 4. Her adımda önceki çözümle karşılaştırarak doğrula")
}dizi O(n³) O(n²) O(n) aynı [-2 1 -3 4 -1 2 1 -5 4 ] 6 6 6 true [1 2 3 ] 6 6 6 true [-5 -2 -8 ] -2 -2 -2 true [5 ] 5 5 5 true [] 0 0 0 true n=3000 için üç çözümün sonucu: O(n³): 591 (yalnızca 300 eleman, tamamı çok yavaş olurdu) O(n²): 591 O(n): 591 İşlem sayısı tahmini (n=3000): O(n³) ≈ 27000 milyon işlem → dakikalar O(n²) ≈ 9 milyon işlem → anlık O(n) ≈ 3000 işlem → ölçülemez STRATEJİ: Kaba kuvvetten optimal çözüme giden yol 1. Çalışan bir çözüm yaz (doğruluğu garanti et) 2. Tekrar eden işi bul ve ortadan kaldır 3. Problemin yapısal özelliğini keşfet 4. Her adımda önceki çözümle karşılaştırarak doğrula
Bu ilerleyişteki en değerli şey, her adımda bir önceki çözümün doğrulama aracı olarak kullanılabilmesidir. Kaba kuvvet yavaş ama kesin doğrudur; optimize ettiğin sürümü onunla karşılaştırarak hata yapmadığından emin olursun. Bu teknik, bu ders boyunca pek çok alıştırmada kullandığımız yaklaşımın ta kendisidir.
Kenar durumları ve test etme
Çözümün doğru görünmesiyle doğru olması farklı şeylerdir. Hataların büyük kısmı beş yerde yaşar.
Boş girdi. Boş dizi, boş metin, sıfır düğümlü graf. Fonksiyonun ilk satırı genelde bu kontrolü içermelidir.
Tek elemanlı girdi. İki işaretçi, kayan pencere ve böl-ve-fethet çözümlerinde en sık kırılan durumdur.
Uç değerler. En büyük ve en küçük olası değerler; taşma riskini kontrol et. int32 sınırına yakın toplamalar sessizce bozulur.
Tekrarlı ve eşit elemanlar. Tüm elemanlar aynıysa? İki eleman eşitse hangisi seçilir? Sıralama kararlılığı önemli mi?
Zıt uçlar. Tamamen sıralı, tamamen ters sıralı, tek değerli girdi. Hızlı sıralama gibi algoritmalar tam olarak burada kötüleşir.
package main
import (
"fmt"
"math/rand/v2"
"slices"
)
// Test edilecek fonksiyon: sıralı diziden tekrarları temizle
func dedup(nums []int) []int {
if len(nums) < 2 {
return nums
}
write := 1
for read := 1; read < len(nums); read++ {
if nums[read] != nums[write-1] {
nums[write] = nums[read]
write++
}
}
return nums[:write]
}
// Referans uygulama: yavaş ama kesin doğru
func dedupReference(nums []int) []int {
var out []int
for _, v := range nums {
if len(out) == 0 || out[len(out)-1] != v {
out = append(out, v)
}
}
return out
}
func main() {
// 1) Elle seçilmiş kenar durumları
edgeCases := []struct {
name string
in []int
}{
{"boş", []int{}},
{"tek eleman", []int{5}},
{"iki aynı", []int{3, 3}},
{"iki farklı", []int{1, 2}},
{"hepsi aynı", []int{7, 7, 7, 7}},
{"tekrarsız", []int{1, 2, 3, 4}},
{"karışık", []int{1, 1, 2, 3, 3, 3, 4}},
{"negatifler", []int{-5, -5, 0, 0, 3}},
}
fmt.Printf("%-14s %-24s %-20s %8s\n", "durum", "girdi", "çıktı", "doğru")
allPass := true
for _, tc := range edgeCases {
input := slices.Clone(tc.in)
want := dedupReference(tc.in)
got := dedup(input)
ok := slices.Equal(got, want)
if !ok {
allPass = false
}
fmt.Printf("%-14s %-24v %-20v %8t\n", tc.name, tc.in, got, ok)
}
fmt.Println("tüm kenar durumları geçti:", allPass)
// 2) Rastgele test: referansla karşılaştır (sabit tohum → deterministik)
fmt.Println()
r := rand.New(rand.NewPCG(42, 1024))
failures := 0
const trials = 2000
for range trials {
n := r.IntN(15)
input := make([]int, n)
for i := range input {
input[i] = r.IntN(6) // küçük aralık: çok tekrar olsun
}
slices.Sort(input)
want := dedupReference(input)
got := dedup(slices.Clone(input))
if !slices.Equal(got, want) {
failures++
if failures == 1 {
fmt.Println(" ilk hata:", input, "→", got, "beklenen", want)
}
}
}
fmt.Printf("%d rastgele testten %d tanesi başarısız\n", trials, failures)
// 3) Değişmez kontrolü
fmt.Println()
fmt.Println("değişmezler:")
test := []int{1, 1, 2, 2, 3}
result := dedup(slices.Clone(test))
fmt.Println(" sonuç sıralı mı:", slices.IsSorted(result))
fmt.Println(" tekrar kalmadı mı:", len(result) == len(slices.Compact(slices.Clone(test))))
fmt.Println(" uzunluk azaldı mı:", len(result) <= len(test))
}durum girdi çıktı doğru boş [] [] true tek eleman [5 ] [5 ] true iki aynı [3 3 ] [3 ] true iki farklı [1 2 ] [1 2 ] true hepsi aynı [7 7 7 7 ] [7 ] true tekrarsız [1 2 3 4 ] [1 2 3 4 ] true karışık [1 1 2 3 3 3 4 ] [1 2 3 4 ] true negatifler [-5 -5 0 0 3 ] [-5 0 3 ] true tüm kenar durumları geçti: true 2000 rastgele testten 0 tanesi başarısız değişmezler: sonuç sıralı mı: true tekrar kalmadı mı: true uzunluk azaldı mı: true
Bu üç katmanlı test yaklaşımı — elle seçilmiş kenar durumları, referansla rastgele karşılaştırma, değişmez kontrolü — hataların neredeyse tamamını yakalar. İkinci katman özellikle güçlüdür: Yavaş ama basit bir referans uygulaması yazıp binlerce rastgele girdide karşılaştırmak, düşünmediğin durumları bulur. Test yazmanın ayrıntılarını Test ve Benchmark dersinde bulabilirsin.
Go ile hızlı giriş/çıkış
Yarışma tarzı problemlerde girdi büyük olabilir ve fmt.Scan ile okumak süre sınırını aşabilir. Sebebi, her çağrının sistem çağrısı yapmasıdır. Çözüm tamponlamadır.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// Yarışma problemleri için standart hızlı giriş/çıkış iskeleti
func main() {
// Bu örnek, gerçek stdin yerine bir metinden okuyor;
// gerçek kullanımda os.Stdin kullanılır.
input := `5
3 1 4 1 5
merhaba dünya
7 8
`
reader := bufio.NewReaderSize(strings.NewReader(input), 1<<16)
writer := bufio.NewWriterSize(os.Stdout, 1<<16)
defer writer.Flush() // ÇOK ÖNEMLİ: unutulursa çıktı hiç yazılmaz
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 1<<20), 1<<20) // uzun satırlar için
scanner.Split(bufio.ScanWords) // kelime kelime oku
readInt := func() int {
scanner.Scan()
n, _ := strconv.Atoi(scanner.Text())
return n
}
readString := func() string {
scanner.Scan()
return scanner.Text()
}
// Girdiyi oku
n := readInt()
nums := make([]int, n)
for i := range nums {
nums[i] = readInt()
}
word1 := readString()
word2 := readString()
a, b := readInt(), readInt()
// İşle ve yaz
sum := 0
for _, v := range nums {
sum += v
}
fmt.Fprintln(writer, "eleman sayısı:", n)
fmt.Fprintln(writer, "sayılar:", nums)
fmt.Fprintln(writer, "toplam:", sum)
fmt.Fprintf(writer, "kelimeler: %q %q\n", word1, word2)
fmt.Fprintln(writer, "son iki sayının çarpımı:", a*b)
// Çok sayıda satır yazarken Fprintln, Println'den kat kat hızlıdır
fmt.Fprintln(writer, "--- hızlı çıktı örneği ---")
for i := 1; i <= 5; i++ {
fmt.Fprintf(writer, "satır %d\n", i)
}
}eleman sayısı: 5 sayılar: [3 1 4 1 5] toplam: 14 kelimeler: "merhaba" "dünya" son iki sayının çarpımı: 56 --- hızlı çıktı örneği --- satır 1 satır 2 satır 3 satır 4 satır 5
Pratik bilgiler: bufio.ScanWords boşluk ve satır sonlarını ayırıcı sayar, bu yüzden satır yapısına bakmadan sayı okuyabilirsin. Girdi çok büyükse scanner.Buffer ile tamponu büyütmek gerekir; varsayılan 64 KB sınırı uzun satırlarda yetmez. Çıktıda yüz binlerce satır varsa fmt.Fprintln(writer, ...) kullanmak fmt.Println'e göre on kattan fazla hızlanma sağlar.
Bir problemi baştan sona çözmek
Öğrendiklerini birleştirelim. Aşağıdaki örnek, sürecin tamamını tek bir problem üzerinde gösteriyor.
package main
import (
"fmt"
"slices"
)
// PROBLEM: Bir dizide, her elemanın kendisi hariç diğer tüm elemanların
// çarpımını içeren yeni bir dizi döndür. Bölme işlemi KULLANMA.
//
// ADIM 1 — ANLAMA
// girdi: [1, 2, 3, 4]
// çıktı: [24, 12, 8, 6]
// (2*3*4, 1*3*4, 1*2*4, 1*2*3)
//
// Sorular: sıfır olabilir mi? → evet
// negatif? → evet
// boş/tek eleman? → varsayım: boş dizi döner
//
// ADIM 2 — KISITLARDAN KARMAŞIKLIK
// n ≤ 10^5 verilmişse O(n²) olmaz (10^10 işlem) → O(n) veya O(n log n) gerekir
//
// ADIM 3 — KABA KUVVET
// Her eleman için diğerlerini çarp: O(n²)
//
// ADIM 4 — YAPIYI FARK ET
// Bir elemanın cevabı = (solundakilerin çarpımı) × (sağındakilerin çarpımı)
// Önek ve sonek çarpımları! → O(n) ve bölme yok
// bruteForce: O(n²) — doğruluk referansı
func bruteForce(nums []int) []int {
out := make([]int, len(nums))
for i := range nums {
product := 1
for j := range nums {
if i != j {
product *= nums[j]
}
}
out[i] = product
}
return out
}
// optimal: O(n) zaman, O(1) ek bellek (çıktı hariç)
func optimal(nums []int) []int {
n := len(nums)
if n == 0 {
return nil
}
out := make([]int, n)
// 1. geçiş: out[i] = solundakilerin çarpımı
prefix := 1
for i := range nums {
out[i] = prefix
prefix *= nums[i]
}
// 2. geçiş: sağdan gelen çarpımla birleştir
suffix := 1
for i := n - 1; i >= 0; i-- {
out[i] *= suffix
suffix *= nums[i]
}
return out
}
func main() {
// ADIM 5 — TEST: kenar durumları
cases := [][]int{
{1, 2, 3, 4},
{2, 3},
{5},
{},
{1, 0, 3}, // bir sıfır
{0, 0, 3}, // iki sıfır
{-1, 2, -3}, // negatifler
{1, 1, 1, 1},
}
fmt.Printf("%-20s %-24s %-24s %8s\n", "girdi", "kaba kuvvet", "optimal", "aynı")
for _, c := range cases {
a := bruteForce(c)
b := optimal(c)
fmt.Printf("%-20v %-24v %-24v %8t\n", c, a, b, slices.Equal(a, b))
}
// Büyük girdide doğrulama
fmt.Println()
n := 2000
big := make([]int, n)
for i := range big {
big[i] = (i % 3) + 1 // taşmayı önlemek için küçük değerler, ama 0 yok
}
a, b := bruteForce(big), optimal(big)
fmt.Printf("n=%d: iki çözüm aynı mı: %t\n", n, slices.Equal(a, b))
fmt.Println(" ilk üç değer:", b[:3])
fmt.Println()
fmt.Println("ÇÖZÜM SÜRECİ ÖZETİ:")
fmt.Println(" 1. Örnekle çalıştım: [1,2,3,4] → [24,12,8,6]")
fmt.Println(" 2. Kısıta baktım: O(n²) olmaz")
fmt.Println(" 3. Kaba kuvveti yazdım (doğruluk referansı)")
fmt.Println(" 4. Yapıyı fark ettim: sol çarpım × sağ çarpım")
fmt.Println(" 5. Kenar durumlarını test ettim: boş, tek, sıfırlar, negatifler")
fmt.Println(" 6. Kaba kuvvetle karşılaştırarak doğruladım")
}girdi kaba kuvvet optimal aynı [1 2 3 4 ] [24 12 8 6 ] [24 12 8 6 ] true [2 3 ] [3 2 ] [3 2 ] true [5 ] [1 ] [1 ] true [] [] [] true [1 0 3 ] [0 3 0 ] [0 3 0 ] true [0 0 3 ] [0 0 0 ] [0 0 0 ] true [-1 2 -3 ] [-6 3 -2 ] [-6 3 -2 ] true [1 1 1 1 ] [1 1 1 1 ] [1 1 1 1 ] true n=2000: iki çözüm aynı mı: true ilk üç değer: [0 0 0] ÇÖZÜM SÜRECİ ÖZETİ: 1. Örnekle çalıştım: [1,2,3,4] → [24,12,8,6] 2. Kısıta baktım: O(n²) olmaz 3. Kaba kuvveti yazdım (doğruluk referansı) 4. Yapıyı fark ettim: sol çarpım × sağ çarpım 5. Kenar durumlarını test ettim: boş, tek, sıfırlar, negatifler 6. Kaba kuvvetle karşılaştırarak doğruladım
Bu problemin çözümündeki anahtar fikir — "cevap = soldakilerin çarpımı × sağdakilerin çarpımı" — elle örnek üzerinde çalışırken ortaya çıkar. Doğrudan koda başlasan bu yapıyı fark etmek çok daha zor olurdu.
Çalışma planı
Buraya kadar geldiysen temelleri tamamladın. Peki bundan sonra nasıl devam edeceksin? Aşağıdaki plan, pratik deneyimden çıkmış birkaç ilkeye dayanıyor.
Düzenli ve az çalış, seyrek ve çok değil. Günde bir problem, haftada yedi problemden daha etkilidir. Algoritma becerisi kas hafızası gibi çalışır; sürekli tekrar gerektirir. Otuz dakikalık günlük bir oturum, ayda bir yapılan sekiz saatlik maratondan fazla kazandırır.
Konu konu çalış, rastgele değil. Bir hafta boyunca yalnızca iki işaretçi problemleri çöz, sonraki hafta yalnızca DP. Aynı kalıbı farklı problemlerde görmek, deseni tanıma becerisini geliştirir. Rastgele problem çözmek eğlencelidir ama öğrenme hızını düşürür.
Tıkandığında yirmi dakika kuralı. Bir problemde yirmi-otuz dakika gerçekten uğraş; ilerlemiyorsan çözüme bak. Ama sonra kodu kapat ve çözümü sıfırdan kendin yaz. Çözümü okumak öğrenmez; yeniden üretmek öğretir.
Çözdüğün problemi geri dön ve gözden geçir. Çalışan bir çözüm bulduktan sonra "daha iyisi var mı?" diye sor. Karmaşıklığı düşürebilir misin? Kod daha okunur olabilir mi? Bu adım, problem çözmeyi mekanik bir işten gerçek öğrenmeye çeviren şeydir.
Yanlış yaptıklarını kaydet. Hangi hataları tekrar tekrar yaptığını yazmaya başla: sınır hataları, boş girdi kontrolü, taşma, döngü yönü. Bu liste zamanla senin kişisel kontrol listen olur ve aynı hataya üçüncü kez düşmeni engeller.
Kendi kodunu test etmeyi alışkanlık yap. Bu derste gördüğün yaklaşım — kaba kuvvet referansı yazıp rastgele girdilerle karşılaştırmak — mülakatlarda ve gerçek işte de geçerlidir. "Çalışıyor gibi görünüyor" ile "test ettim" arasındaki fark, mühendislikte en önemli farklardan biridir.
Uygulama yaz, yalnızca problem çözme. Öğrendiğin veri yapılarını gerçek bir projede kullanmak, kavrayışı bambaşka bir seviyeye taşır. Bir metin editörü, bir küçük veritabanı, bir yönlendirici, bir sıkıştırma aracı — hepsi bu derslerde gördüğün yapıların üzerine kurulur.
Son bir not: Bu derslerdeki her konuyu tam olarak hatırlamana gerek yok. Önemli olan, bir problemle karşılaştığında nereye bakacağını ve hangi soruları soracağını bilmektir. Karmaşıklık analizi, desen tanıma ve test etme alışkanlığı — bu üçü kalıcıdır. Algoritmaların ayrıntıları gerektiğinde tekrar bakılır.
Sık yapılan hatalar
- Problemi anlamadan kod yazmaya başlamak. Beş dakikalık düşünme, yarım saatlik yanlış yönü önler.
- Kısıtları okumamak. Girdi boyutu hedef karmaşıklığı söyler; bunu atlamak yanlış yaklaşımla saatler harcamaya yol açar.
- Doğrudan optimal çözümü hedeflemek. Kaba kuvvetle başlamak hem doğruluk referansı verir hem yapıyı görmeni sağlar.
- Kenar durumlarını test etmemek. Boş girdi, tek eleman ve uç değerler hataların çoğunu barındırır.
- Taşmayı hesaba katmamak. Çarpım ve toplam biriktiren döngülerde sınırları düşün.
writer.Flush()unutmak. Tamponlu çıktıda program sonlanırken veri kaybolur.- Çözümü okuyup geçmek. Çözümü kapatıp sıfırdan yazmadan öğrenme gerçekleşmez.
Alıştırmalar
Verilen girdi kısıtlarına göre hangi karmaşıklığın kabul edilebilir olduğunu belirleyen ve uygun yaklaşımları öneren bir yardımcı yaz. Kendi problem çözme sürecinde kullanabileceğin bir araç olsun.
İpucu
Saniyede yapılabilecek işlem sayısını sabit varsay (10⁸) ve her karmaşıklık için hangi n değerine kadar makul olduğunu hesapla.
Çözümü göster
package main
import "fmt"
type Approach struct {
Complexity string
MaxN int
Techniques string
}
var approaches = []Approach{
{"O(n!)", 11, "tüm permütasyonlar, geri izleme"},
{"O(2^n)", 25, "alt kümeler, bit maskesi DP, geri izleme"},
{"O(n³)", 500, "Floyd-Warshall, aralık DP, matris işlemleri"},
{"O(n² log n)", 2_000, "her çift için ikili arama"},
{"O(n²)", 6_000, "iki iç içe döngü, klasik DP, Bellman-Ford"},
{"O(n √n)", 200_000, "karekök ayrıştırma, Mo's algoritması"},
{"O(n log² n)", 500_000, "iç içe ikili arama, segment ağacı üzerinde arama"},
{"O(n log n)", 5_000_000, "sıralama, heap, ikili arama, böl ve fethet"},
{"O(n)", 20_000_000, "tek geçiş, iki işaretçi, kayan pencere, hash"},
{"O(log n)", 1_000_000_000, "ikili arama, hızlı üs alma"},
}
// recommend: verilen n için uygun yaklaşımları önerir
func recommend(n int) []Approach {
var out []Approach
for _, a := range approaches {
if n <= a.MaxN {
out = append(out, a)
}
}
return out
}
// tooSlow: verilen n için makul OLMAYAN yaklaşımlar
func tooSlow(n int) []Approach {
var out []Approach
for _, a := range approaches {
if n > a.MaxN {
out = append(out, a)
}
}
return out
}
func main() {
fmt.Println("KARMAŞIKLIK REHBERİ (saniyede ~10^8 işlem varsayımıyla)")
fmt.Println()
fmt.Printf("%-14s %14s %s\n", "karmaşıklık", "makul n sınırı", "teknikler")
for _, a := range approaches {
fmt.Printf("%-14s %14d %s\n", a.Complexity, a.MaxN, a.Techniques)
}
fmt.Println()
for _, n := range []int{10, 1_000, 100_000, 10_000_000} {
fmt.Printf("n = %d için:\n", n)
ok := recommend(n)
fmt.Println(" KULLANILABİLİR:")
for _, a := range ok {
fmt.Printf(" %-14s %s\n", a.Complexity, a.Techniques)
}
slow := tooSlow(n)
if len(slow) > 0 {
fmt.Print(" ÇOK YAVAŞ: ")
for i, a := range slow {
if i > 0 {
fmt.Print(", ")
}
fmt.Print(a.Complexity)
}
fmt.Println()
}
fmt.Println()
}
fmt.Println("KULLANIM: Problemi okurken kısıta bak, bu tabloyu tara,")
fmt.Println("hedef karmaşıklığı belirle, sonra o karmaşıklıkta çözüm ara.")
}KARMAŞIKLIK REHBERİ (saniyede ~10^8 işlem varsayımıyla)
karmaşıklık makul n sınırı teknikler
O(n!) 11 tüm permütasyonlar, geri izleme
O(2^n) 25 alt kümeler, bit maskesi DP, geri izleme
O(n³) 500 Floyd-Warshall, aralık DP, matris işlemleri
O(n² log n) 2000 her çift için ikili arama
O(n²) 6000 iki iç içe döngü, klasik DP, Bellman-Ford
O(n √n) 200000 karekök ayrıştırma, Mo's algoritması
O(n log² n) 500000 iç içe ikili arama, segment ağacı üzerinde arama
O(n log n) 5000000 sıralama, heap, ikili arama, böl ve fethet
O(n) 20000000 tek geçiş, iki işaretçi, kayan pencere, hash
O(log n) 1000000000 ikili arama, hızlı üs alma
n = 10 için:
KULLANILABİLİR:
O(n!) tüm permütasyonlar, geri izleme
O(2^n) alt kümeler, bit maskesi DP, geri izleme
O(n³) Floyd-Warshall, aralık DP, matris işlemleri
O(n² log n) her çift için ikili arama
O(n²) iki iç içe döngü, klasik DP, Bellman-Ford
O(n √n) karekök ayrıştırma, Mo's algoritması
O(n log² n) iç içe ikili arama, segment ağacı üzerinde arama
O(n log n) sıralama, heap, ikili arama, böl ve fethet
O(n) tek geçiş, iki işaretçi, kayan pencere, hash
O(log n) ikili arama, hızlı üs alma
n = 1000 için:
KULLANILABİLİR:
O(n² log n) her çift için ikili arama
O(n²) iki iç içe döngü, klasik DP, Bellman-Ford
O(n √n) karekök ayrıştırma, Mo's algoritması
O(n log² n) iç içe ikili arama, segment ağacı üzerinde arama
O(n log n) sıralama, heap, ikili arama, böl ve fethet
O(n) tek geçiş, iki işaretçi, kayan pencere, hash
O(log n) ikili arama, hızlı üs alma
ÇOK YAVAŞ: O(n!), O(2^n), O(n³)
n = 100000 için:
KULLANILABİLİR:
O(n √n) karekök ayrıştırma, Mo's algoritması
O(n log² n) iç içe ikili arama, segment ağacı üzerinde arama
O(n log n) sıralama, heap, ikili arama, böl ve fethet
O(n) tek geçiş, iki işaretçi, kayan pencere, hash
O(log n) ikili arama, hızlı üs alma
ÇOK YAVAŞ: O(n!), O(2^n), O(n³), O(n² log n), O(n²)
n = 10000000 için:
KULLANILABİLİR:
O(n) tek geçiş, iki işaretçi, kayan pencere, hash
O(log n) ikili arama, hızlı üs alma
ÇOK YAVAŞ: O(n!), O(2^n), O(n³), O(n² log n), O(n²), O(n √n), O(n log² n), O(n log n)
KULLANIM: Problemi okurken kısıta bak, bu tabloyu tara,
hedef karmaşıklığı belirle, sonra o karmaşıklıkta çözüm ara.Bu tablo bir kesin kural değil, bir başlangıç tahminidir. Sabit çarpanlar, bellek erişim deseni ve dil seçimi gerçek sınırları kaydırabilir. Go, C++'a yakın hızda çalışır ve bu tablo Go için makul bir yaklaşımdır; yorumlanan dillerde sınırlar belirgin biçimde düşer.
Tablonun asıl değeri, hangi yaklaşımları eleyeceğini söylemesidir. n = 100.000 verilen bir problemde O(n²) aramayı bırakıp O(n log n) düşünmeye başlamak, doğru yolda ilerlemenin en hızlı yoludur.
Bir problemi üç aşamada çöz ve her aşamayı öncekiyle karşılaştırarak doğrula. Problem: Bir dizide k uzunluğundaki bitişik alt dizilerin en büyük toplamını bul.
İpucu
Aşama 1: her pencereyi tek tek topla — O(n·k). Aşama 2: önek toplamları — O(n). Aşama 3: kayan pencere ile O(1) ek bellek.
Çözümü göster
package main
import (
"fmt"
"math/rand/v2"
)
// AŞAMA 1 — Kaba kuvvet: her pencereyi baştan topla — O(n·k)
func stage1(nums []int, k int) (int, bool) {
if k <= 0 || len(nums) < k {
return 0, false
}
best := 1 << 62
best = -best
for i := 0; i+k <= len(nums); i++ {
sum := 0
for j := i; j < i+k; j++ {
sum += nums[j]
}
best = max(best, sum)
}
return best, true
}
// AŞAMA 2 — Önek toplamları: O(n) zaman, O(n) bellek
func stage2(nums []int, k int) (int, bool) {
if k <= 0 || len(nums) < k {
return 0, false
}
prefix := make([]int, len(nums)+1)
for i, v := range nums {
prefix[i+1] = prefix[i] + v
}
best := prefix[k] - prefix[0]
for i := 1; i+k <= len(nums); i++ {
best = max(best, prefix[i+k]-prefix[i])
}
return best, true
}
// AŞAMA 3 — Kayan pencere: O(n) zaman, O(1) bellek
func stage3(nums []int, k int) (int, bool) {
if k <= 0 || len(nums) < k {
return 0, false
}
sum := 0
for i := range k {
sum += nums[i]
}
best := sum
for i := k; i < len(nums); i++ {
sum += nums[i] - nums[i-k] // giren eklenir, çıkan düşülür
best = max(best, sum)
}
return best, true
}
func main() {
cases := []struct {
nums []int
k int
}{
{[]int{1, 4, 2, 10, 23, 3, 1, 0, 20}, 4},
{[]int{5, 5, 5}, 1},
{[]int{-1, -2, -3, -4}, 2},
{[]int{3}, 1},
{[]int{1, 2}, 5}, // k çok büyük
{[]int{}, 1}, // boş
{[]int{7, 8, 9}, 0}, // k = 0
}
fmt.Printf("%-30s %4s %10s %10s %10s %8s\n", "dizi", "k", "aşama1", "aşama2", "aşama3", "aynı")
for _, c := range cases {
a, ok1 := stage1(c.nums, c.k)
b, ok2 := stage2(c.nums, c.k)
d, ok3 := stage3(c.nums, c.k)
same := ok1 == ok2 && ok2 == ok3 && (!ok1 || (a == b && b == d))
show := func(v int, ok bool) string {
if !ok {
return "geçersiz"
}
return fmt.Sprint(v)
}
fmt.Printf("%-30v %4d %10s %10s %10s %8t\n",
c.nums, c.k, show(a, ok1), show(b, ok2), show(d, ok3), same)
}
// Rastgele testlerle üç aşamayı karşılaştır
fmt.Println()
r := rand.New(rand.NewPCG(7, 13))
failures := 0
const trials = 3000
for range trials {
n := r.IntN(30)
k := r.IntN(10)
nums := make([]int, n)
for i := range nums {
nums[i] = r.IntN(41) - 20
}
a, ok1 := stage1(nums, k)
b, ok2 := stage2(nums, k)
d, ok3 := stage3(nums, k)
if ok1 != ok2 || ok2 != ok3 || (ok1 && (a != b || b != d)) {
failures++
if failures == 1 {
fmt.Println(" ilk uyuşmazlık:", nums, "k =", k, a, b, d)
}
}
}
fmt.Printf("%d rastgele testten %d uyuşmazlık\n", trials, failures)
// Büyük girdide aşama 1'in maliyeti
fmt.Println()
n, k := 20_000, 5_000
big := make([]int, n)
for i := range big {
big[i] = (i*31)%101 - 50
}
fmt.Printf("n=%d, k=%d için:\n", n, k)
fmt.Println(" aşama 1 işlem sayısı ≈", (n-k+1)*k/1_000_000, "milyon")
fmt.Println(" aşama 3 işlem sayısı ≈", n)
v2, _ := stage2(big, k)
v3, _ := stage3(big, k)
fmt.Println(" aşama 2 ve 3 sonuçları eşit mi:", v2 == v3)
}dizi k aşama1 aşama2 aşama3 aynı [1 4 2 10 23 3 1 0 20 ] 4 39 39 39 true [5 5 5 ] 1 5 5 5 true [-1 -2 -3 -4 ] 2 -3 -3 -3 true [3 ] 1 3 3 3 true [1 2 ] 5 geçersiz geçersiz geçersiz true [] 1 geçersiz geçersiz geçersiz true [7 8 9 ] 0 geçersiz geçersiz geçersiz true 3000 rastgele testten 0 uyuşmazlık n=20000, k=5000 için: aşama 1 işlem sayısı ≈ 75 milyon aşama 3 işlem sayısı ≈ 20000 aşama 2 ve 3 sonuçları eşit mi: true
Üç aşamalı ilerleyiş, iyileştirmenin nasıl yapıldığını gösteriyor. Birinci aşamadan ikinciye geçişte tekrar eden işi ortadan kaldırdık: Her pencereyi baştan toplamak yerine önek toplamlarını bir kez hesapladık. İkinciden üçüncüye geçişte ise gereksiz belleği attık: Tüm önek dizisini tutmak yerine yalnızca mevcut pencere toplamını güncelledik.
İkinci ve üçüncü aşamanın zaman karmaşıklığı aynıdır ama üçüncüsü daha azdır bellek kullanır ve önbellek davranışı daha iyidir. Bu, asimptotik karmaşıklığın son söz olmadığını gösteren tipik bir örnektir.
Kendi çözümlerini otomatik olarak sınayan bir test yardımcısı yaz: bir çözüm fonksiyonu, bir referans fonksiyon ve bir girdi üreteci alsın; kenar durumlarını ve rastgele girdileri denesin, ilk uyuşmazlığı en küçük hâline indirgeyerek raporlasın.
İpucu
Uyuşmazlık bulduğunda girdiyi küçültmeye çalış: elemanları tek tek çıkarıp hatanın devam edip etmediğini kontrol et. Buna "küçültme" (shrinking) denir ve özellik temelli test kütüphanelerinin temel özelliğidir.
Çözümü göster
package main
import (
"fmt"
"math/rand/v2"
"slices"
)
type TestResult struct {
Passed int
Failed int
Minimal []int
GotWant [2]int
HasError bool
}
// Tester: bir çözümü referansla karşılaştırır ve hatayı küçültür
type Tester struct {
Solution func([]int) int
Reference func([]int) int
rng *rand.Rand
}
func NewTester(solution, reference func([]int) int, seed uint64) *Tester {
return &Tester{solution, reference, rand.New(rand.NewPCG(seed, seed*7+1))}
}
// shrink: hatalı girdiyi olabildiğince küçültür
func (t *Tester) shrink(input []int) []int {
current := slices.Clone(input)
changed := true
for changed {
changed = false
// 1) Eleman çıkarmayı dene
for i := range current {
candidate := slices.Delete(slices.Clone(current), i, i+1)
if t.fails(candidate) {
current = candidate
changed = true
break
}
}
if changed {
continue
}
// 2) Değerleri sıfıra yaklaştırmayı dene
for i := range current {
if current[i] == 0 {
continue
}
candidate := slices.Clone(current)
candidate[i] /= 2
if t.fails(candidate) {
current = candidate
changed = true
break
}
}
}
return current
}
func (t *Tester) fails(input []int) bool {
defer func() { _ = recover() }() // çözüm panik yaparsa da hata sayılır
return t.Solution(slices.Clone(input)) != t.Reference(slices.Clone(input))
}
// Run: kenar durumları + rastgele testler
func (t *Tester) Run(trials int) TestResult {
var result TestResult
edgeCases := [][]int{
{}, {0}, {1}, {-1},
{0, 0}, {1, 1}, {1, -1},
{1, 2, 3}, {3, 2, 1}, {5, 5, 5, 5},
{-5, -3, -1}, {1 << 20, 1 << 20},
}
check := func(input []int) {
if t.fails(input) {
result.Failed++
if !result.HasError {
result.HasError = true
result.Minimal = t.shrink(input)
result.GotWant = [2]int{
t.Solution(slices.Clone(result.Minimal)),
t.Reference(slices.Clone(result.Minimal)),
}
}
return
}
result.Passed++
}
for _, e := range edgeCases {
check(e)
}
for range trials {
n := t.rng.IntN(12)
input := make([]int, n)
for i := range input {
input[i] = t.rng.IntN(21) - 10
}
check(input)
}
return result
}
// --- Test edilecek fonksiyonlar ---
// referenceMaxSubarray: yavaş ama kesin doğru
func referenceMaxSubarray(nums []int) int {
if len(nums) == 0 {
return 0
}
best := nums[0]
for i := range nums {
sum := 0
for j := i; j < len(nums); j++ {
sum += nums[j]
best = max(best, sum)
}
}
return best
}
// correctKadane: doğru uygulama
func correctKadane(nums []int) int {
if len(nums) == 0 {
return 0
}
best, cur := nums[0], nums[0]
for _, v := range nums[1:] {
cur = max(v, cur+v)
best = max(best, cur)
}
return best
}
// buggyKadane: KASITLI HATA — negatif dizilerde 0 döndürür
func buggyKadane(nums []int) int {
best, cur := 0, 0 // hata: nums[0] ile başlamalı
for _, v := range nums {
cur = max(0, cur+v)
best = max(best, cur)
}
return best
}
func main() {
fmt.Println("DOĞRU uygulama test ediliyor:")
good := NewTester(correctKadane, referenceMaxSubarray, 42)
r1 := good.Run(2000)
fmt.Printf(" geçen: %d, başarısız: %d\n", r1.Passed, r1.Failed)
if r1.HasError {
fmt.Println(" en küçük hatalı girdi:", r1.Minimal)
} else {
fmt.Println(" hata bulunamadı ✓")
}
fmt.Println()
fmt.Println("HATALI uygulama test ediliyor:")
bad := NewTester(buggyKadane, referenceMaxSubarray, 42)
r2 := bad.Run(2000)
fmt.Printf(" geçen: %d, başarısız: %d\n", r2.Passed, r2.Failed)
if r2.HasError {
fmt.Println(" en küçük hatalı girdi:", r2.Minimal)
fmt.Printf(" çözüm döndürdü: %d, doğrusu: %d\n", r2.GotWant[0], r2.GotWant[1])
fmt.Println(" → hata: tüm elemanlar negatifken 0 döndürüyor")
}
fmt.Println()
fmt.Println("KÜÇÜLTMENİN DEĞERİ:")
fmt.Println("Rastgele bulunan hatalı girdi 12 elemanlı olabilir;")
fmt.Println("küçültme onu 1-2 elemanlı en sade hâline indirir.")
fmt.Println("Hatayı anlamak böylece çok daha kolay olur.")
}DOĞRU uygulama test ediliyor: geçen: 2012, başarısız: 0 hata bulunamadı ✓ HATALI uygulama test ediliyor: geçen: 1864, başarısız: 148 en küçük hatalı girdi: [-1] çözüm döndürdü: 0, doğrusu: -1 → hata: tüm elemanlar negatifken 0 döndürüyor KÜÇÜLTMENİN DEĞERİ: Rastgele bulunan hatalı girdi 12 elemanlı olabilir; küçültme onu 1-2 elemanlı en sade hâline indirir. Hatayı anlamak böylece çok daha kolay olur.
Bu araç, özellik temelli test (property-based testing) yaklaşımının çekirdeğini uygular ve iki güçlü fikri birleştirir. Birincisi, tek tek test durumu yazmak yerine bir referans uygulama ile karşılaştırmak: Yavaş ama kesin doğru bir çözüm, hızlı çözümün doğruluğunu binlerce girdide sınar. İkincisi küçültme: Bulunan hatalı girdiyi olabildiğince sadeleştirerek hatayı anlaşılır hâle getirmek.
Küçültmenin değeri pratikte çok büyüktür. On iki elemanlı rastgele bir dizide hata bulmak, sorunu anlamana yetmez; ama aynı hata iki elemanlı bir dizide de ortaya çıkıyorsa nedeni genelde hemen görünür. Örnekteki hatalı uygulamada küçültme, sorunu "tek negatif elemanlı dizi" durumuna indirger ve hata anında anlaşılır hâle gelir.
Go'nun yerleşik fuzzing desteği bu işi otomatikleştirir ve bulduğu bozuk girdileri kalıcı test durumlarına dönüştürür; ayrıntıları Test ve Benchmark dersinde bulabilirsin. Kendi test aracını yazmak ise altta ne olduğunu anlamanın en iyi yoludur.
Kısa sınav
Bir problemde n ≤ 10⁶ kısıtı varsa hangi karmaşıklık hedeflenmelidir?
Problem çözmede önerilen ilk adım nedir?
Kaba kuvvet çözümü yazmanın faydası nedir?
Go`da büyük girdiyi hızlı okumak için ne kullanılır?
Tamponlu yazıcıda Flush çağrılmazsa ne olur?
Hataların en sık bulunduğu yer neresidir?
Özet
- Problemi kendi cümlelerinle yeniden yaz, girdi/çıktıyı kesinleştir ve küçük bir örnek üzerinde elle çalış.
- Girdi kısıtı hedef karmaşıklığı söyler: n ≤ 20 için üstel, n ≤ 5.000 için karesel, n ≤ 10⁶ için O(n log n).
- Desen tanıma tablosunu tara: problem metnindeki ifadeler hangi aracı kullanacağına dair güçlü ipucu verir.
- Önce kaba kuvvet yaz; hem doğruluk referansı olur hem yapıyı görmeni sağlar.
- İyileştirme iki yolla gelir: tekrar eden işi ortadan kaldırmak ve problemin yapısal özelliğini keşfetmek.
- Kenar durumlarını mutlaka test et: boş girdi, tek eleman, uç değerler, tekrarlı elemanlar, zıt uçlar.
- Referans uygulamayla rastgele karşılaştırma, düşünmediğin hataları bulur; küçültme onları anlaşılır hâle getirir.
- Büyük girdilerde
bufioile tamponlu okuma/yazma kullan veFlushçağrısını unutma. - Düzenli ve konu konu çalış; tıkandığında çözüme bak ama sonra sıfırdan kendin yaz.
- Kalıcı olan üç şey: karmaşıklık analizi, desen tanıma ve test etme alışkanlığı. Ayrıntılar gerektiğinde tekrar bakılır.