go
Algoritmalar dersleri
Algoritmalar/İleri Konular

Problem Çözme Stratejileri

Desen tanıma, kısıtlardan karmaşıklık çıkarma ve mülakat hazırlığı.

Ders 18 / 1825 dkOrta
Bu derste öğreneceklerin
  • Problemi anlama ve örneklerle çalışma
  • Kısıtlardan hedef karmaşıklığı çıkarma
  • Desen tanıma tablosu
  • Kaba kuvvetten optimal çözüme
  • Kenar durumları ve test etme
  • Go ile hızlı giriş/çıkış (bufio)
  • Çalışma planı ve kaynaklar

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.

main.go
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")
}
Çıktı
[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 hesaplama

Bu 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.

main.go
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"
	}
}
Çıktı
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.

Problemde görürsen…Muhtemel araçDers
"sıralı dizide bul"İkili aramaArama
"en küçük/büyük değeri bul" (monoton)Cevap üzerinde ikili aramaArama
"bitişik alt dizi/alt metin"Kayan pencere, iki işaretçiİki İşaretçi
"sıralı dizide çift bul"İki işaretçiİki İşaretçi
"daha önce gördüm mü"Hash tablosu / kümeHash Tabloları
"frekans say", "grupla"Hash tablosuHash Tabloları
"en son eklenen ilk çıkar", iç içe yapıYığınYığın
"solundaki/sağındaki ilk büyük eleman"Monoton yığınYığın
"sırayla işle", "katman katman"Kuyruk, BFSKuyruk
"en büyük k eleman", "sürekli en küçük"Heap / öncelik kuyruğuHeap
"ön ekle başlayan"TrieTrie
"aynı grupta mı", "kaç bileşen"Union-FindUnion-Find
"aralık toplamı + güncelleme"Segment ağacı / FenwickSegment Ağacı
"en az kaç adımda" (eşit maliyet)BFSBFS ve DFS
"tüm yolları dene", "kaç farklı şekilde"DFS / geri izlemeGeri İzleme
"önce şunu yap" kısıtlarıTopolojik sıralamaTopolojik Sıralama
"en kısa yol" (ağırlıklı)DijkstraEn Kısa Yol
"en az maliyetle hepsini bağla"MSTMST
"en fazla/en az", "kaç yol" + seçimDinamik programlamaDP I
"iki metin/dizi karşılaştır"2D DPDP II
"her adımda en iyiyi al" (kanıtlanabilir)AçgözlüAçgözlü
"problemi yarıya böl"Böl ve fethetBöl ve Fethet
"n ≤ 20", "tüm alt kümeler"Bit maskesiBit Manipülasyonu

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.

main.go
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")
}
Çıktı
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.

main.go
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))
}
Çıktı
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.

main.go
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)
	}
}
Çıktı
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.

main.go
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")
}
Çıktı
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
ZamanO(n)AlanO(1) ek bellek

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

Alıştırma·Karmaşıklık tahmini
Kolay

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
main.go
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.")
}
Çıktı
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.

Alıştırma·Üç aşamalı çözüm
Orta

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
main.go
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)
}
Çıktı
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
ZamanO(n·k) → O(n) → O(n)AlanO(1) → O(n) → O(1)

Üç 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.

Alıştırma·Kişisel hata kontrol listesi
Zor

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
main.go
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.")
}
Çıktı
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.
Zamantest sayısı × girdi boyutuAlanO(n)

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

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 bufio ile tamponlu okuma/yazma kullan ve Flush ç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.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.