go
Algoritmalar dersleri
Algoritmalar/Tasarım Paradigmaları

Böl ve Fethet

Problemi parçala, çöz, birleştir; yineleme bağıntıları ve Master teoremi.

Ders 7 / 1830 dkOrta
Bu derste öğreneceklerin
  • Böl, fethet, birleştir adımları
  • Yineleme bağıntıları (recurrence relations)
  • Master teoremi
  • Hızlı üs alma
  • Quickselect ile k. en küçük eleman
  • Maksimum alt dizi toplamı
  • Ters çevrim sayma (inversion count)

Büyük bir problemi çözmenin en eski stratejisi, onu parçalamaktır. Bir kütüphanedeki kitapları tek başına saymak zordur; ama işi dört kişiye bölüp her biri bir kanadı sayarsa ve sonunda toplarsanız çok daha hızlı bitirirsiniz. Bu strateji algoritma dünyasında böl ve fethet (divide and conquer) adını taşır ve üç adımdan oluşur.

Böl: Problemi aynı türden daha küçük alt problemlere ayır. Fethet: Alt problemleri özyinelemeli olarak çöz; yeterince küçüldüklerinde doğrudan cevapla. Birleştir: Alt problemlerin cevaplarından asıl cevabı oluştur.

Bu kalıbı zaten gördün: Birleştirmeli sıralama ve hızlı sıralama bunun örnekleridir; ikili arama da bir dejenere hâlidir (yalnızca bir alt probleme iner). Bu derste kalıbı genel olarak inceleyeceğiz: Yineleme bağıntılarıyla karmaşıklığı nasıl hesaplayacağını, Master teoremini, hızlı üs almayı, k'ıncı en küçük elemanı doğrusal beklenen sürede bulan quickselect'i, maksimum alt dizi problemini ve ters çevrim sayımını öğreneceksin.

Üç adım ve genel kalıp

function çöz(problem):
    if problem yeterince küçük:
        return doğrudan_çöz(problem)        ← temel durum

    alt1, alt2, ... = böl(problem)          ← BÖL
    c1 = çöz(alt1)                          ← FETHET
    c2 = çöz(alt2)
    return birleştir(c1, c2)                ← BİRLEŞTİR

Kalıbın işe yaraması için iki koşul gerekir. Birincisi, alt problemler bağımsız olmalıdır — birbirlerinin sonucuna ihtiyaç duymamalıdır. (Duyuyorlarsa doğru araç dinamik programlamadır; onu Dinamik Programlama I dersinde göreceksin.) İkincisi, birleştirme adımı bölmeden kazandığın işten daha pahalı olmamalıdır.

Bu iki koşul sağlandığında kazanç büyük olabilir. Ama her zaman kazanç yoktur: Bir dizinin toplamını böl ve fethetle hesaplamak, doğrudan döngüyle hesaplamakla aynı karmaşıklıktadır — yalnızca daha karmaşık bir kodla.

Yineleme bağıntıları

Böl ve fethet algoritmalarının karmaşıklığı, kendini tekrar eden bir denklemle ifade edilir. Birleştirmeli sıralama için:

T(n) = 2·T(n/2) + O(n)
        ↑      ↑       ↑
   alt problem  boyut  birleştirme
     sayısı            maliyeti

Açalım:
T(n)   = 2T(n/2)  + n
       = 2(2T(n/4) + n/2) + n = 4T(n/4) + 2n
       = 8T(n/8) + 3n
       ...
       = 2^k·T(n/2^k) + k·n

n/2^k = 1 olduğunda k = log₂ n:
T(n) = n·T(1) + n·log₂ n = O(n log n)

Ağaç olarak düşünmek daha sezgiseldir:

seviye 0:          n               → n iş
seviye 1:      n/2   n/2           → n iş
seviye 2:   n/4 n/4 n/4 n/4        → n iş
   ...
seviye log n:  1 1 1 ... 1         → n iş

log n seviye × her seviyede n iş = O(n log n)

Master teoremi

Her yineleme bağıntısını elle açmak yorucudur. Master teoremi, en yaygın biçim için hazır bir cevap verir:

T(n) = a·T(n/b) + f(n)     (a ≥ 1, b > 1)

Karşılaştırılacak büyüklük: n^(log_b a)

Durum 1: f(n) = O(n^(log_b a − ε))      → T(n) = Θ(n^(log_b a))
         (birleştirme ucuz; alt problemler baskın)

Durum 2: f(n) = Θ(n^(log_b a))          → T(n) = Θ(n^(log_b a) · log n)
         (dengeli; her seviye eşit iş yapar)

Durum 3: f(n) = Ω(n^(log_b a + ε))      → T(n) = Θ(f(n))
         (birleştirme pahalı; kök baskın)

Örneklerle bakalım:

AlgoritmaBağıntıabn^(log_b a)DurumSonuç
İkili aramaT(n) = T(n/2) + O(1)12n⁰ = 12O(log n)
Birleştirmeli sıralamaT(n) = 2T(n/2) + O(n)22n¹ = n2O(n log n)
Karatsuba çarpmaT(n) = 3T(n/2) + O(n)32n^1.581O(n^1.58)
Naif matris çarpımıT(n) = 8T(n/2) + O(n²)821O(n³)
StrassenT(n) = 7T(n/2) + O(n²)72n^2.811O(n^2.81)
Ağaç yüksekliğiT(n) = 2T(n/2) + O(1)22n1O(n)
main.go
package main

import (
	"fmt"
	"math"
)

// masterTheorem: a, b ve f(n)'in derecesi verildiğinde sonucu belirler
func masterTheorem(a, b int, fDegree float64) (string, string) {
	critical := math.Log(float64(a)) / math.Log(float64(b))

	const eps = 1e-9
	switch {
	case fDegree < critical-eps:
		return "Durum 1", fmt.Sprintf("Θ(n^%.2f)", critical)
	case math.Abs(fDegree-critical) < eps:
		return "Durum 2", fmt.Sprintf("Θ(n^%.2f · log n)", critical)
	default:
		return "Durum 3", fmt.Sprintf("Θ(n^%.2f)", fDegree)
	}
}

func main() {
	examples := []struct {
		name    string
		a, b    int
		fDegree float64
	}{
		{"ikili arama: T(n)=T(n/2)+O(1)", 1, 2, 0},
		{"birleştirmeli: T(n)=2T(n/2)+O(n)", 2, 2, 1},
		{"Karatsuba: T(n)=3T(n/2)+O(n)", 3, 2, 1},
		{"matris çarpımı: T(n)=8T(n/2)+O(n²)", 8, 2, 2},
		{"Strassen: T(n)=7T(n/2)+O(n²)", 7, 2, 2},
		{"ağaç yüksekliği: T(n)=2T(n/2)+O(1)", 2, 2, 0},
		{"pahalı birleştirme: T(n)=2T(n/2)+O(n²)", 2, 2, 2},
	}

	fmt.Printf("%-38s %10s %22s\n", "bağıntı", "durum", "sonuç")
	for _, e := range examples {
		durum, sonuc := masterTheorem(e.a, e.b, e.fDegree)
		fmt.Printf("%-38s %10s %22s\n", e.name, durum, sonuc)
	}

	fmt.Println()
	fmt.Println("Kritik büyüklük n^(log_b a) ile f(n)'i karşılaştırırız:")
	fmt.Println("  f küçükse → alt problemler baskın")
	fmt.Println("  eşitse    → her seviye eşit iş, log n çarpanı eklenir")
	fmt.Println("  f büyükse → birleştirme baskın")
}
Çıktı
bağıntı                                     durum                  sonuç
ikili arama: T(n)=T(n/2)+O(1)             Durum 2      Θ(n^0.00 · log n)
birleştirmeli: T(n)=2T(n/2)+O(n)          Durum 2      Θ(n^1.00 · log n)
Karatsuba: T(n)=3T(n/2)+O(n)              Durum 1              Θ(n^1.58)
matris çarpımı: T(n)=8T(n/2)+O(n²)        Durum 1              Θ(n^3.00)
Strassen: T(n)=7T(n/2)+O(n²)              Durum 1              Θ(n^2.81)
ağaç yüksekliği: T(n)=2T(n/2)+O(1)        Durum 1              Θ(n^1.00)
pahalı birleştirme: T(n)=2T(n/2)+O(n²)    Durum 3              Θ(n^2.00)

Kritik büyüklük n^(log_b a) ile f(n)'i karşılaştırırız:
  f küçükse → alt problemler baskın
  eşitse    → her seviye eşit iş, log n çarpanı eklenir
  f büyükse → birleştirme baskın

Master teoreminin sınırları da vardır: Alt problem boyutları eşit olmayan bağıntılara (T(n) = T(n/3) + T(2n/3) + n gibi) uygulanamaz. Bu durumlarda özyineleme ağacını elle çizmek ya da yerine koyma yöntemi kullanmak gerekir.

Hızlı üs alma

Böl ve fethetin en zarif uygulamalarından biri: a^n hesaplamak. Naif yöntem n çarpma yapar; böl ve fethet log n çarpmaya iner.

a^n = (a^(n/2))²           n çift ise
a^n = a · (a^((n-1)/2))²   n tek ise

2^10 = (2^5)²
2^5  = 2 · (2^2)²
2^2  = (2^1)²
2^1  = 2 · (2^0)²
2^0  = 1

Toplam: 4 çarpma (naif yöntemde 10)
main.go
package main

import "fmt"

var multiplications int

// powNaive: O(n)
func powNaive(a, n int) int {
	result := 1
	for range n {
		result *= a
		multiplications++
	}
	return result
}

// powFast: O(log n) — böl ve fethet
func powFast(a, n int) int {
	if n == 0 {
		return 1
	}
	half := powFast(a, n/2)
	multiplications++
	result := half * half
	if n%2 == 1 {
		multiplications++
		result *= a
	}
	return result
}

// powIterative: aynı fikrin döngü hâli (bitlere bakar)
func powIterative(a, n int) int {
	result := 1
	for n > 0 {
		if n&1 == 1 {
			result *= a
			multiplications++
		}
		a *= a
		multiplications++
		n >>= 1
	}
	return result
}

// powMod: modüler üs alma — kriptografinin temel işlemi
func powMod(a, n, mod int) int {
	result := 1
	a %= mod
	for n > 0 {
		if n&1 == 1 {
			result = result * a % mod
		}
		a = a * a % mod
		n >>= 1
	}
	return result
}

func main() {
	fmt.Printf("%6s %14s %10s %14s %10s\n", "n", "naif sonuç", "çarpma", "hızlı sonuç", "çarpma")
	for _, n := range []int{1, 5, 10, 20, 30} {
		multiplications = 0
		naive := powNaive(2, n)
		naiveMults := multiplications

		multiplications = 0
		fast := powFast(2, n)
		fastMults := multiplications

		fmt.Printf("%6d %14d %10d %14d %10d\n", n, naive, naiveMults, fast, fastMults)
	}

	fmt.Println()
	multiplications = 0
	fmt.Println("3^13 iteratif =", powIterative(3, 13), "| çarpma:", multiplications)

	fmt.Println()
	fmt.Println("modüler üs alma (RSA'nın temeli):")
	fmt.Println("  7^128 mod 13 =", powMod(7, 128, 13))
	fmt.Println("  2^1000 mod 1_000_000_007 =", powMod(2, 1000, 1_000_000_007))
	fmt.Println("→ naif yöntemle 2^1000 hesaplamak taşma yaratırdı")
}
Çıktı
n     naif sonuç     çarpma    hızlı sonuç     çarpma
     1              2          1              2          2
     5             32          5             32          5
    10           1024         10           1024          6
    20        1048576         20        1048576          7
    30     1073741824         30     1073741824          9

3^13 iteratif = 1594323 | çarpma: 7

modüler üs alma (RSA'nın temeli):
  7^128 mod 13 = 3
  2^1000 mod 1_000_000_007 = 688423210
→ naif yöntemle 2^1000 hesaplamak taşma yaratırdı
ZamanO(log n)AlanO(log n) özyinelemeli, O(1) iteratif

Hızlı üs alma yalnızca sayılarla sınırlı değildir. Aynı fikir matris üs almada kullanılır ve Fibonacci sayılarını O(log n) sürede hesaplamayı mümkün kılar. Modüler sürümü ise açık anahtarlı şifrelemenin temel işlemidir: Çok büyük üsleri makul sürede hesaplamanın tek yolu budur.

Quickselect: k'ıncı en küçük eleman

Bir dizide k'ıncı en küçük elemanı bulmak istiyorsun. Sıralayıp k'ıncı elemanı almak O(n log n) sürer. Ama tüm diziyi sıralamaya gerek yok — yalnızca k'ıncı elemanın yerini bilmek yeterli.

Quickselect, hızlı sıralamanın bölümleme adımını kullanır ama yalnızca bir tarafa iner:

[7, 2, 9, 1, 5, 3]   k = 3 (3. en küçük)

bölümleme (pivot=3): [2, 1 | 3 | 7, 9, 5]
                      indeks: 2

pivot indeksi 2, aranan indeks 2 (0 tabanlı k-1) → BULUNDU: 3

T(n) = T(n/2) + O(n) = O(n)   ← sadece bir tarafa indiğimiz için
main.go
package main

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

var partitions int

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

// quickselect: k. en küçük eleman (1 tabanlı) — beklenen O(n)
func quickselect(data []int, k int, r *rand.Rand) (int, bool) {
	if k < 1 || k > len(data) {
		return 0, false
	}
	work := slices.Clone(data)
	target := k - 1
	lo, hi := 0, len(work)-1

	for lo <= hi {
		// Rastgele pivot: en kötü durumu pratikte imkânsız kılar
		pick := lo + r.IntN(hi-lo+1)
		work[pick], work[hi] = work[hi], work[pick]

		p := partition(work, lo, hi)
		switch {
		case p == target:
			return work[p], true
		case p < target:
			lo = p + 1 // sağ tarafa in
		default:
			hi = p - 1 // sol tarafa in
		}
	}
	return 0, false
}

// median: ortanca değer
func median(data []int, r *rand.Rand) float64 {
	n := len(data)
	if n == 0 {
		return 0
	}
	if n%2 == 1 {
		v, _ := quickselect(data, n/2+1, r)
		return float64(v)
	}
	a, _ := quickselect(data, n/2, r)
	b, _ := quickselect(data, n/2+1, r)
	return float64(a+b) / 2
}

// topK: en büyük k eleman
func topK(data []int, k int, r *rand.Rand) []int {
	if k >= len(data) {
		out := slices.Clone(data)
		slices.Sort(out)
		return out
	}
	threshold, _ := quickselect(data, len(data)-k+1, r)
	var out []int
	for _, v := range data {
		if v >= threshold {
			out = append(out, v)
		}
	}
	slices.Sort(out)
	return out[max(0, len(out)-k):]
}

func main() {
	r := rand.New(rand.NewPCG(3, 7))
	data := []int{7, 2, 9, 1, 5, 3, 8, 6, 4}

	fmt.Println("dizi:", data)
	sorted := slices.Clone(data)
	slices.Sort(sorted)
	fmt.Println("sıralı hâli:", sorted)
	fmt.Println()

	for k := 1; k <= len(data); k++ {
		partitions = 0
		v, _ := quickselect(data, k, r)
		fmt.Printf("  %d. en küçük = %-3d (sıralıdaki: %-3d doğru: %-5t bölümleme: %d)\n",
			k, v, sorted[k-1], v == sorted[k-1], partitions)
	}

	fmt.Println()
	fmt.Println("ortanca:", median(data, r))
	fmt.Println("en büyük 3:", topK(data, 3, r))

	// Büyük dizide karşılaştırma
	fmt.Println()
	const n = 10_000
	big := make([]int, n)
	for i := range big {
		big[i] = r.IntN(100_000)
	}
	partitions = 0
	v, _ := quickselect(big, n/2, r)
	selectParts := partitions

	bigSorted := slices.Clone(big)
	slices.Sort(bigSorted)

	fmt.Printf("n=%d için ortanca bulma:\n", n)
	fmt.Println("  quickselect sonucu:", v, "doğru mu:", v == bigSorted[n/2-1])
	fmt.Println("  quickselect bölümleme sayısı:", selectParts)
	fmt.Println("  sıralama yapsaydık ~n log n =", n*14, "karşılaştırma gerekirdi")
}
Çıktı
dizi: [7 2 9 1 5 3 8 6 4]
sıralı hâli: [1 2 3 4 5 6 7 8 9]

  1. en küçük = 1   (sıralıdaki: 1   doğru: true  bölümleme: 3)
  2. en küçük = 2   (sıralıdaki: 2   doğru: true  bölümleme: 3)
  3. en küçük = 3   (sıralıdaki: 3   doğru: true  bölümleme: 3)
  4. en küçük = 4   (sıralıdaki: 4   doğru: true  bölümleme: 6)
  5. en küçük = 5   (sıralıdaki: 5   doğru: true  bölümleme: 2)
  6. en küçük = 6   (sıralıdaki: 6   doğru: true  bölümleme: 3)
  7. en küçük = 7   (sıralıdaki: 7   doğru: true  bölümleme: 3)
  8. en küçük = 8   (sıralıdaki: 8   doğru: true  bölümleme: 3)
  9. en küçük = 9   (sıralıdaki: 9   doğru: true  bölümleme: 1)

ortanca: 5
en büyük 3: [7 8 9]

n=10000 için ortanca bulma:
  quickselect sonucu: 50311 doğru mu: true
  quickselect bölümleme sayısı: 23
  sıralama yapsaydık ~n log n = 140000 karşılaştırma gerekirdi
En iyiO(n)OrtalamaO(n)En kötüO(n²)AlanO(1)

Quickselect'in beklenen süresi O(n)'dir ve bunun sebeği zarif bir toplamdır: Her adımda dizinin yarısını elediğin için toplam iş n + n/2 + n/4 + ... < 2n olur. En kötü durumda (her bölümleme dengesiz) O(n²) olur; rastgele pivot bunu pratikte ortadan kaldırır.

Garantili O(n) isteyen bir varyant da vardır — "beşlerin medyanı" yöntemiyle pivot seçen medyan-medyanların algoritması. Teorik olarak zariftir ama sabit çarpanı büyük olduğu için pratikte rastgele pivotlu quickselect tercih edilir.

Maksimum alt dizi toplamı

Bir dizideki en büyük toplamlı bitişik alt diziyi bul. Böl ve fethetle çözümü öğreticidir, çünkü birleştirme adımı ilk bakışta göze çarpmaz.

[-2, 1, -3, 4, -1, 2, 1, -5, 4]

Diziyi ortadan böl. En iyi alt dizi üç yerden biri olabilir:
  1) tamamen SOL yarıda
  2) tamamen SAĞ yarıda
  3) ORTADAN GEÇEN (birleştirme adımı bunu bulur)

3. durum için: ortadan sola doğru en iyi + ortadan sağa doğru en iyi
main.go
package main

import "fmt"

// maxCrossing: ortadan geçen en iyi alt diziyi bulur — O(n)
func maxCrossing(data []int, lo, mid, hi int) int {
	leftBest, sum := -1<<62, 0
	for i := mid; i >= lo; i-- {
		sum += data[i]
		leftBest = max(leftBest, sum)
	}

	rightBest, sum := -1<<62, 0
	for i := mid + 1; i <= hi; i++ {
		sum += data[i]
		rightBest = max(rightBest, sum)
	}
	return leftBest + rightBest
}

// maxSubarrayDC: böl ve fethet — O(n log n)
func maxSubarrayDC(data []int, lo, hi int) int {
	if lo == hi {
		return data[lo]
	}
	mid := lo + (hi-lo)/2
	return max(
		maxSubarrayDC(data, lo, mid),
		maxSubarrayDC(data, mid+1, hi),
		maxCrossing(data, lo, mid, hi),
	)
}

// kadane: dinamik programlama — O(n), çok daha basit
func kadane(data []int) (best, start, end int) {
	if len(data) == 0 {
		return 0, -1, -1
	}
	best, current := data[0], data[0]
	start, end, tempStart := 0, 0, 0

	for i := 1; i < len(data); i++ {
		if current < 0 {
			current = data[i] // geçmişi at, buradan başla
			tempStart = i
		} else {
			current += data[i]
		}
		if current > best {
			best, start, end = current, tempStart, i
		}
	}
	return best, start, end
}

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

	fmt.Printf("%-32s %10s %10s %8s %-18s\n", "dizi", "böl-fethet", "Kadane", "aynı", "en iyi aralık")
	for _, c := range cases {
		dc := maxSubarrayDC(c, 0, len(c)-1)
		k, start, end := kadane(c)
		fmt.Printf("%-32v %10d %10d %8t %-18v\n", c, dc, k, dc == k, c[start:end+1])
	}

	fmt.Println()
	fmt.Println("Böl ve fethet: O(n log n) — öğretici ama optimal değil")
	fmt.Println("Kadane algoritması: O(n) — dinamik programlama yaklaşımı")
}
Çıktı
dizi                             böl-fethet     Kadane     aynı en iyi aralık
[-2                               1                                -3                               4                                -1                               2                                1                                -5                               4                               ]          6          6     true [4                  -1                 2                  1                 ]
[1                                2                                3                                4                                5                               ]         15         15     true [1                  2                  3                  4                  5                 ]
[-5                               -2                               -8                               -1                              ]         -1         -1     true [-1                ]
[5                                -3                               5                               ]          7          7     true [5                  -3                 5                 ]
[42                              ]         42         42     true [42                ]

Böl ve fethet: O(n log n) — öğretici ama optimal değil
Kadane algoritması: O(n) — dinamik programlama yaklaşımı

Bu örnek önemli bir ders içerir: Böl ve fethet her zaman en iyi çözüm değildir. Bu problem için Kadane algoritması hem daha hızlı (O(n)) hem daha kısadır. Böl ve fethet çözümü, yaklaşımın nasıl uygulandığını göstermek açısından değerlidir; ama pratikte doğrusal çözüm kullanılır.

Genel kural şudur: Alt problemlerin sonuçları birbirinden bağımsızsa ve birleştirme ucuzsa böl ve fethet kazandırır. Bu problemde ise "buraya kadarki en iyi" bilgisi soldan sağa taşınabildiği için tek geçiş yeterli oluyor.

Ters çevrim sayma

Bir dizide kaç tane i < j iken a[i] > a[j] olan çift var? Bu sayı, dizinin ne kadar "sırasız" olduğunun ölçüsüdür. Kaba kuvvet O(n²) sürer; birleştirmeli sıralamanın içine yerleştirilen basit bir sayaç bunu O(n log n)'e indirir.

Birleştirme sırasında: sağ diziden bir eleman alındığında,
sol dizide KALAN tüm elemanlar onunla ters çevrim oluşturur.

sol:  [3, 5]     sağ: [1, 4]
       ↑                ↑
1 < 3 → 1 alınır, sol'da kalan 2 eleman (3 ve 5) ters çevrim!
       ters çevrim += 2
main.go
package main

import (
	"fmt"
	"slices"
)

// mergeCount: birleştirirken ters çevrim sayar
func mergeCount(data, buf []int, lo, mid, hi int) int {
	i, j, k := lo, mid, lo
	inversions := 0

	for i < mid && j < hi {
		if data[i] <= data[j] {
			buf[k] = data[i]
			i++
		} else {
			buf[k] = data[j]
			j++
			inversions += mid - i // sol dizide kalan her eleman bir ters çevrim
		}
		k++
	}
	for i < mid {
		buf[k] = data[i]
		i, k = i+1, k+1
	}
	for j < hi {
		buf[k] = data[j]
		j, k = j+1, k+1
	}
	copy(data[lo:hi], buf[lo:hi])
	return inversions
}

// countInversions: O(n log n)
func countInversions(data []int) int {
	work := slices.Clone(data)
	buf := make([]int, len(work))

	var sort func(lo, hi int) int
	sort = func(lo, hi int) int {
		if hi-lo <= 1 {
			return 0
		}
		mid := lo + (hi-lo)/2
		count := sort(lo, mid) + sort(mid, hi)
		return count + mergeCount(work, buf, lo, mid, hi)
	}
	return sort(0, len(work))
}

// bruteForce: O(n²)
func bruteForce(data []int) int {
	count := 0
	for i := range data {
		for j := i + 1; j < len(data); j++ {
			if data[i] > data[j] {
				count++
			}
		}
	}
	return count
}

func main() {
	cases := [][]int{
		{1, 2, 3, 4, 5}, // sıralı: 0
		{5, 4, 3, 2, 1}, // ters: n(n-1)/2 = 10
		{2, 4, 1, 3, 5}, // karışık
		{1, 3, 2, 3, 1}, // tekrarlı
		{7},             // tek
		{},              // boş
	}

	fmt.Printf("%-24s %14s %14s %8s\n", "dizi", "böl-fethet", "kaba kuvvet", "aynı")
	for _, c := range cases {
		fast := countInversions(c)
		slow := bruteForce(c)
		fmt.Printf("%-24v %14d %14d %8t\n", c, fast, slow, fast == slow)
	}

	// Büyük dizide fark
	fmt.Println()
	const n = 2000
	descending := make([]int, n)
	for i := range descending {
		descending[i] = n - i
	}
	fmt.Printf("n=%d ters sıralı dizi:\n", n)
	fmt.Println("  ters çevrim sayısı:", countInversions(descending))
	fmt.Println("  teorik maksimum n(n-1)/2 =", n*(n-1)/2)
	fmt.Println("  böl-fethet ~n log n =", n*11, "işlem")
	fmt.Println("  kaba kuvvet n² =", n*n, "işlem")
}
Çıktı
dizi                         böl-fethet    kaba kuvvet     aynı
[1                        2                        3                        4                        5                       ]              0              0     true
[5                        4                        3                        2                        1                       ]             10             10     true
[2                        4                        1                        3                        5                       ]              3              3     true
[1                        3                        2                        3                        1                       ]              4              4     true
[7                       ]              0              0     true
[]              0              0     true

n=2000 ters sıralı dizi:
  ters çevrim sayısı: 1999000
  teorik maksimum n(n-1)/2 = 1999000
  böl-fethet ~n log n = 22000 işlem
  kaba kuvvet n² = 4000000 işlem
ZamanO(n log n)AlanO(n)

Buradaki fikir çok geneldir: Var olan bir böl-ve-fethet algoritmasının içine ek bilgi toplamak. Birleştirmeli sıralama zaten diziyi gezip birleştiriyor; birleştirme adımına küçük bir sayaç eklemek neredeyse bedavadır. Aynı teknikle "her eleman için sağında kaçından büyük" gibi soruları da yanıtlayabilirsin.

Ters çevrim sayısının pratik anlamı vardır: İki sıralamanın ne kadar benzediğini ölçen istatistiksel bir ölçüttür ve öneri sistemlerinde, sıralama kalitesi değerlendirmede kullanılır. Ayrıca eklemeli sıralamanın yapacağı kaydırma sayısına tam olarak eşittir.

Böl ve fethet ne zaman işe yarar?

Kalıbı tanımak, onu doğru yerde kullanmanın yarısıdır. Aşağıdaki işaretler, bir problemin böl ve fethetle çözülebileceğini gösterir.

Problem kendine benzer alt problemlere ayrılabiliyorsa. Diziyi ikiye bölmek, aralığı yarıya indirmek, ağacın alt ağaçlarını ayrı ele almak — hepsi aynı türden daha küçük problemler üretir. Alt problemlerin asıl problemle aynı biçimde olması kritiktir; farklı türden bir problem üretiyorsan kalıp uymaz.

Alt problemler bağımsızsa. Sol yarının çözümü sağ yarının çözümüne ihtiyaç duymuyorsa böl ve fethet uygundur. Duyuyorsa alt problemler örtüşüyor demektir ve dinamik programlamaya geçmen gerekir. Fibonacci hesabını özyinelemeyle yapmanın neden korkunç yavaş olduğunun sebebi tam olarak budur: Aynı alt problem defalarca çözülür.

Birleştirme ucuzsa. Master teoreminin üçüncü durumu bu tuzağı anlatır: Birleştirme maliyeti çok büyükse bölmenin kazancı silinir. Birleştirme adımını tasarlarken maliyetini hesaplamayı ihmal etme.

Paralelleştirme isteniyorsa. Alt problemler bağımsız olduğu için ayrı işlemcilerde çalıştırılabilirler. Bu, böl ve fethetin modern donanımda ayrıca değerli olmasının sebebidir. Go'da bu, her alt problemi bir goroutine'de çalıştırmak kadar kolaydır — ama küçük alt problemlerde goroutine kurma maliyeti kazancı yer, bu yüzden belirli bir boyutun altında sıralı çalışmaya geçmek gerekir.

Temel durumu doğru seçmek. Özyinelemeyi tek elemana kadar indirmek zorunda değilsin. Yaklaşık yirmi elemanın altında basit bir algoritmaya geçmek, hem özyineleme yükünü hem de bellek ayırmayı azaltır. Gördüğün melez sıralama algoritmaları tam olarak bunu yapar.

Son olarak, kalıbın sınırını bilmek gerekir: Her problem bölünmeye uygun değildir. Bir dizinin toplamını ya da maksimumunu bulmak gibi problemlerde böl ve fethet, doğrudan döngüyle aynı karmaşıklığı verir ve yalnızca kodu karmaşıklaştırır. Kazanç, bölmenin gerçekten iş azalttığı durumlarda ortaya çıkar.

Sık yapılan hatalar

  • Temel durumu yanlış yazmak. Boş veya tek elemanlı girdi için doğru cevabı düşünmeden kod yazmaya başlamak sonsuz özyinelemeye yol açar.
  • Alt problemlerin bağımsız olduğunu varsaymak. Örtüşen alt problemler varsa böl ve fethet üstel sürede çalışır; dinamik programlama gerekir.
  • Birleştirme maliyetini hesaba katmamak. Pahalı birleştirme, bölmenin kazancını silebilir.
  • Master teoremini uygun olmayan bağıntılara uygulamak. Alt problem boyutları eşit değilse teorem geçerli değildir.
  • Quickselect'te sabit pivot kullanmak. Sıralı girdide O(n²)'ye düşer.
  • Böl ve fetheti her yere uygulamak. Bazı problemlerde doğrusal çözüm vardır ve daha basittir.
  • Özyineleme derinliğini göz ardı etmek. Dengesiz bölmeler derinliği n'e çıkarabilir.

Alıştırmalar

Alıştırma·Hızlı üs alma ile Fibonacci
Kolay

Matris üs alma kullanarak n. Fibonacci sayısını O(log n) sürede hesapla. Sonucu naif özyinelemeli ve iteratif sürümlerle karşılaştır.

İpucu

[[1,1],[1,0]] matrisinin n. kuvvetinin sol üst elemanı F(n+1), sağ üst elemanı F(n)'dir. Matris çarpımını 2×2 için elle yaz.

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

import "fmt"

type matrix [2][2]int

func multiply(a, b matrix) matrix {
	return matrix{
		{a[0][0]*b[0][0] + a[0][1]*b[1][0], a[0][0]*b[0][1] + a[0][1]*b[1][1]},
		{a[1][0]*b[0][0] + a[1][1]*b[1][0], a[1][0]*b[0][1] + a[1][1]*b[1][1]},
	}
}

// matrixPow: hızlı üs alma — O(log n) matris çarpımı
func matrixPow(m matrix, n int) matrix {
	result := matrix{{1, 0}, {0, 1}} // birim matris
	for n > 0 {
		if n&1 == 1 {
			result = multiply(result, m)
		}
		m = multiply(m, m)
		n >>= 1
	}
	return result
}

// fibMatrix: O(log n)
func fibMatrix(n int) int {
	if n <= 0 {
		return 0
	}
	base := matrix{{1, 1}, {1, 0}}
	return matrixPow(base, n)[0][1]
}

// fibIterative: O(n)
func fibIterative(n int) int {
	if n <= 0 {
		return 0
	}
	a, b := 0, 1
	for range n - 1 {
		a, b = b, a+b
	}
	return b
}

var naiveCalls int

// fibNaive: O(2^n) — örtüşen alt problemler yüzünden felaket
func fibNaive(n int) int {
	naiveCalls++
	if n <= 1 {
		return n
	}
	return fibNaive(n-1) + fibNaive(n-2)
}

func main() {
	fmt.Printf("%6s %14s %14s %8s\n", "n", "matris", "iteratif", "aynı")
	for _, n := range []int{0, 1, 5, 10, 30, 50, 80} {
		m := fibMatrix(n)
		it := fibIterative(n)
		fmt.Printf("%6d %14d %14d %8t\n", n, m, it, m == it)
	}

	fmt.Println()
	fmt.Println("naif özyinelemeli sürümün çağrı sayısı:")
	for _, n := range []int{10, 20, 25, 30} {
		naiveCalls = 0
		v := fibNaive(n)
		fmt.Printf("  F(%d) = %-8d çağrı: %d\n", n, v, naiveCalls)
	}

	fmt.Println()
	fmt.Println("F(80) hesaplamak için:")
	fmt.Println("  matris yöntemi: ~7 matris çarpımı")
	fmt.Println("  iteratif: 79 toplama")
	fmt.Println("  naif özyineleme: yaklaşık 2^80 çağrı — evren ömrü yetmez")
}
Çıktı
n         matris       iteratif     aynı
     0              0              0     true
     1              1              1     true
     5              5              5     true
    10             55             55     true
    30         832040         832040     true
    50    12586269025    12586269025     true
    80 23416728348467685 23416728348467685     true

naif özyinelemeli sürümün çağrı sayısı:
  F(10) = 55       çağrı: 177
  F(20) = 6765     çağrı: 21891
  F(25) = 75025    çağrı: 242785
  F(30) = 832040   çağrı: 2692537

F(80) hesaplamak için:
  matris yöntemi: ~7 matris çarpımı
  iteratif: 79 toplama
  naif özyineleme: yaklaşık 2^80 çağrı — evren ömrü yetmez
ZamanO(log n)AlanO(1)

Naif özyinelemenin çağrı sayısına dikkat: n her arttığında neredeyse ikiye katlanıyor. Sebebi, aynı alt problemin defalarca çözülmesidir — F(30) hesaplarken F(10) yüzlerce kez yeniden hesaplanır. Bu, böl ve fethetin örtüşen alt problemlerde neden başarısız olduğunun en net örneğidir ve dinamik programlamanın varlık sebebidir.

Matris yöntemi pratikte iteratif sürümden hızlı değildir çünkü sabit çarpanı büyüktür; ama n çok büyük olduğunda (örneğin modüler aritmetikle F(10¹⁸) hesaplarken) tek uygulanabilir yöntemdir.

Alıştırma·En yakın nokta çifti
Orta

Düzlemde verilen noktalar arasında en yakın iki noktayı bul. Kaba kuvvet O(n²) sürer; böl ve fethetle bunu O(n log² n) seviyesine indir. Sonuçları kaba kuvvetle karşılaştırarak doğrula.

İpucu

Noktaları x'e göre sırala, ortadan böl. En yakın çift ya solda, ya sağda, ya da orta şeritte olabilir. Orta şeridi kontrol ederken yalnızca y farkı mevcut en küçük mesafeden küçük olan çiftlere bak.

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

import (
	"fmt"
	"math"
	"slices"
)

type Point struct {
	X, Y float64
}

func dist(a, b Point) float64 {
	dx, dy := a.X-b.X, a.Y-b.Y
	return math.Sqrt(dx*dx + dy*dy)
}

// bruteForce: O(n²)
func bruteForce(points []Point) (float64, Point, Point) {
	best := math.Inf(1)
	var p1, p2 Point
	for i := range points {
		for j := i + 1; j < len(points); j++ {
			if d := dist(points[i], points[j]); d < best {
				best, p1, p2 = d, points[i], points[j]
			}
		}
	}
	return best, p1, p2
}

// closestPair: böl ve fethet — O(n log² n)
func closestPair(points []Point) (float64, Point, Point) {
	if len(points) < 2 {
		return math.Inf(1), Point{}, Point{}
	}
	byX := slices.Clone(points)
	slices.SortFunc(byX, func(a, b Point) int {
		if a.X < b.X {
			return -1
		}
		if a.X > b.X {
			return 1
		}
		return 0
	})
	return solve(byX)
}

func solve(byX []Point) (float64, Point, Point) {
	n := len(byX)
	if n <= 3 {
		return bruteForce(byX) // küçük durumda doğrudan çöz
	}

	mid := n / 2
	midX := byX[mid].X

	dl, l1, l2 := solve(byX[:mid])
	dr, r1, r2 := solve(byX[mid:])

	best, p1, p2 := dl, l1, l2
	if dr < best {
		best, p1, p2 = dr, r1, r2
	}

	// Orta şerit: x farkı best'ten küçük olan noktalar
	var strip []Point
	for _, p := range byX {
		if math.Abs(p.X-midX) < best {
			strip = append(strip, p)
		}
	}
	slices.SortFunc(strip, func(a, b Point) int {
		if a.Y < b.Y {
			return -1
		}
		if a.Y > b.Y {
			return 1
		}
		return 0
	})

	// Geometrik gerçek: her nokta için en fazla 7 komşuyu kontrol etmek yeter
	for i := range strip {
		for j := i + 1; j < len(strip) && strip[j].Y-strip[i].Y < best; j++ {
			if d := dist(strip[i], strip[j]); d < best {
				best, p1, p2 = d, strip[i], strip[j]
			}
		}
	}
	return best, p1, p2
}

func main() {
	points := []Point{
		{2, 3}, {12, 30}, {40, 50}, {5, 1}, {12, 10}, {3, 4},
		{20, 25}, {7, 8}, {1, 1}, {30, 35},
	}

	fmt.Println("nokta sayısı:", len(points))

	bd, bp1, bp2 := bruteForce(points)
	cd, cp1, cp2 := closestPair(points)

	fmt.Printf("kaba kuvvet : mesafe=%.4f çift=(%.0f,%.0f)-(%.0f,%.0f)\n",
		bd, bp1.X, bp1.Y, bp2.X, bp2.Y)
	fmt.Printf("böl ve fethet: mesafe=%.4f çift=(%.0f,%.0f)-(%.0f,%.0f)\n",
		cd, cp1.X, cp1.Y, cp2.X, cp2.Y)
	fmt.Println("mesafeler eşit mi:", math.Abs(bd-cd) < 1e-9)

	// Daha büyük deterministik veri kümesi
	fmt.Println()
	var big []Point
	for i := range 200 {
		x := float64((i*37)%211) * 1.5
		y := float64((i*53)%197) * 1.3
		big = append(big, Point{x, y})
	}
	bd2, _, _ := bruteForce(big)
	cd2, _, _ := closestPair(big)
	fmt.Printf("n=%d: kaba kuvvet=%.6f böl-fethet=%.6f aynı=%t\n",
		len(big), bd2, cd2, math.Abs(bd2-cd2) < 1e-9)
	fmt.Println("kaba kuvvet karşılaştırma sayısı:", len(big)*(len(big)-1)/2)
}
Çıktı
nokta sayısı: 10
kaba kuvvet : mesafe=1.4142 çift=(2,3)-(3,4)
böl ve fethet: mesafe=1.4142 çift=(2,3)-(3,4)
mesafeler eşit mi: true

n=200: kaba kuvvet=4.684015 böl-fethet=4.684015 aynı=true
kaba kuvvet karşılaştırma sayısı: 19900
ZamanO(n log² n)AlanO(n)

Bu problemin böl ve fethet çözümü, birleştirme adımının neden dikkatli tasarlanması gerektiğini gösteriyor. Naif bir birleştirme, orta şeritteki tüm çiftleri kontrol ederek O(n²)'ye dönerdi. Kurtarıcı, geometrik bir gözlemdir: Şeritte y'ye göre sıralanmış noktalar arasında, her nokta için yalnızca sabit sayıda komşuyu kontrol etmek yeterlidir — çünkü mevcut en küçük mesafeden daha yakın olabilecek nokta sayısı sınırlıdır.

Şeridi her seferinde yeniden sıralamak yerine baştan y'ye göre sıralanmış bir kopya taşırsan karmaşıklık O(n log n)'e iner. Bu, algoritma tasarımında sık görülen bir iyileştirmedir: Tekrar eden bir işi önceden bir kez yapmak.

Alıştırma·Karatsuba çarpma
Zor

Çok büyük sayıların çarpımını Karatsuba algoritmasıyla hesapla. Klasik yöntem O(n²) basamak çarpımı yapar; Karatsuba bunu O(n^1.585)'e indirir. Basamak dizileri üzerinde çalış ve sonuçları naif çarpımla karşılaştır.

İpucu

x = a·10^m + b, y = c·10^m + d ise x·y = ac·10^2m + (ad+bc)·10^m + bd. Karatsuba'nın hilesi: ad+bc = (a+b)(c+d) − ac − bd, yani dört çarpım yerine üç.

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

import (
	"fmt"
	"strings"
)

var multiplications int

// naiveMultiply: klasik uzun çarpma — O(n²)
func naiveMultiply(x, y []int) []int {
	result := make([]int, len(x)+len(y))
	for i := range x {
		for j := range y {
			result[i+j] += x[i] * y[j]
			multiplications++
		}
	}
	normalize(result)
	return trim(result)
}

// karatsuba: üç çarpımla böl ve fethet — O(n^1.585)
func karatsuba(x, y []int) []int {
	n := max(len(x), len(y))
	if n <= 8 { // küçük durumda naif yöntem daha hızlı
		return naiveMultiply(x, y)
	}

	m := n / 2
	a, b := split(x, m) // x = a·10^m + b
	c, d := split(y, m) // y = c·10^m + d

	ac := karatsuba(a, c)
	bd := karatsuba(b, d)
	abcd := karatsuba(add(a, b), add(c, d))
	middle := sub(sub(abcd, ac), bd) // (a+b)(c+d) − ac − bd = ad + bc

	// sonuç = ac·10^(2m) + middle·10^m + bd
	result := add(add(shift(ac, 2*m), shift(middle, m)), bd)
	normalize(result)
	return trim(result)
}

// Yardımcılar: basamaklar küçükten büyüğe (little-endian) saklanır

func split(x []int, m int) (high, low []int) {
	if len(x) <= m {
		return []int{0}, slices_clone(x)
	}
	return slices_clone(x[m:]), slices_clone(x[:m])
}

func shift(x []int, m int) []int {
	out := make([]int, len(x)+m)
	copy(out[m:], x)
	return out
}

func add(x, y []int) []int {
	n := max(len(x), len(y))
	out := make([]int, n+1)
	for i := range n {
		if i < len(x) {
			out[i] += x[i]
		}
		if i < len(y) {
			out[i] += y[i]
		}
	}
	normalize(out)
	return trim(out)
}

func sub(x, y []int) []int {
	out := slices_clone(x)
	for i := range y {
		out[i] -= y[i]
	}
	// ödünç al
	for i := 0; i < len(out)-1; i++ {
		for out[i] < 0 {
			out[i] += 10
			out[i+1]--
		}
	}
	return trim(out)
}

func normalize(x []int) {
	carry := 0
	for i := range x {
		x[i] += carry
		carry = x[i] / 10
		x[i] %= 10
	}
}

func trim(x []int) []int {
	i := len(x) - 1
	for i > 0 && x[i] == 0 {
		i--
	}
	return x[:i+1]
}

func slices_clone(x []int) []int {
	out := make([]int, len(x))
	copy(out, x)
	return out
}

func parse(s string) []int {
	out := make([]int, len(s))
	for i := range s {
		out[len(s)-1-i] = int(s[i] - '0')
	}
	return trim(out)
}

func format(x []int) string {
	var sb strings.Builder
	for i := len(x) - 1; i >= 0; i-- {
		fmt.Fprint(&sb, x[i])
	}
	return sb.String()
}

func main() {
	cases := [][2]string{
		{"1234", "5678"},
		{"999", "999"},
		{"12345678901234567890", "98765432109876543210"},
		{"1", "0"},
	}

	for _, c := range cases {
		x, y := parse(c[0]), parse(c[1])

		multiplications = 0
		naive := naiveMultiply(x, y)
		naiveMults := multiplications

		multiplications = 0
		kara := karatsuba(x, y)
		karaMults := multiplications

		fmt.Printf("%s × %s\n", c[0], c[1])
		fmt.Printf("  naif    = %-42s (%d basamak çarpımı)\n", format(naive), naiveMults)
		fmt.Printf("  Karatsuba = %-40s (%d basamak çarpımı)\n", format(kara), karaMults)
		fmt.Printf("  aynı mı: %t\n\n", format(naive) == format(kara))
	}

	// Büyük sayılarda çarpım sayısı karşılaştırması
	big1 := strings.Repeat("9", 128)
	big2 := strings.Repeat("7", 128)
	x, y := parse(big1), parse(big2)

	multiplications = 0
	naiveMultiply(x, y)
	naiveCount := multiplications

	multiplications = 0
	karatsuba(x, y)
	karaCount := multiplications

	fmt.Println("128 basamaklı iki sayı:")
	fmt.Println("  naif basamak çarpımı:     ", naiveCount)
	fmt.Println("  Karatsuba basamak çarpımı:", karaCount)
	fmt.Printf("  kazanç: %.2f kat\n", float64(naiveCount)/float64(karaCount))
}
Çıktı
1234 × 5678
  naif    = 7006652                                    (16 basamak çarpımı)
  Karatsuba = 7006652                                  (16 basamak çarpımı)
  aynı mı: true

999 × 999
  naif    = 998001                                     (9 basamak çarpımı)
  Karatsuba = 998001                                   (9 basamak çarpımı)
  aynı mı: true

12345678901234567890 × 98765432109876543210
  naif    = 1219326311370217952237463801111263526900   (400 basamak çarpımı)
  Karatsuba = 1219326311370217952237463801111263526900 (245 basamak çarpımı)
  aynı mı: true

1 × 0
  naif    = 0                                          (1 basamak çarpımı)
  Karatsuba = 0                                        (1 basamak çarpımı)
  aynı mı: true

128 basamaklı iki sayı:
  naif basamak çarpımı:      16384
  Karatsuba basamak çarpımı: 5254
  kazanç: 3.12 kat
ZamanO(n^1.585)AlanO(n)

Karatsuba, 1960'ta yirmi üç yaşındaki Anatoly Karatsuba tarafından bulundu ve o güne kadar imkânsız sanılan bir şeyi gösterdi: Çarpma, basamak sayısının karesinden daha hızlı yapılabilir. Kolmogorov'un aksi yönde bir varsayımı vardı; Karatsuba onu bir hafta içinde çürüttü.

Algoritmanın kalbindeki hile, ad + bc toplamını doğrudan hesaplamak yerine (a+b)(c+d) − ac − bd biçiminde yazmaktır. Böylece dört çarpım yerine üç çarpım yapılır. Master teoremiyle: T(n) = 3T(n/2) + O(n)O(n^log₂3)O(n^1.585).

Küçük sayılarda naif yöntemin daha hızlı olduğuna dikkat et — Karatsuba'nın ek toplama ve çıkarma maliyeti küçük girdilerde kazancı yer. Bu yüzden gerçek uygulamalar bir eşik kullanır. Aynı fikir daha da ileri götürülmüştür: Toom-Cook ve Fourier dönüşümü tabanlı yöntemler, çok büyük sayılarda neredeyse doğrusal sürelere iner. Büyük sayı kütüphaneleri (Go'nun math/big paketi dâhil) sayı boyutuna göre bu algoritmalar arasında seçim yapar.

Kısa sınav

Kısa sınav

Böl ve fethet stratejisinin üç adımı nedir?

T(n) = 2T(n/2) + O(n) bağıntısının çözümü nedir?

Böl ve fethet, örtüşen alt problemler olduğunda neden verimsizdir?

Quickselect'in beklenen karmaşıklığı nedir?

Hızlı üs alma a^n'i kaç çarpmayla hesaplar?

Karatsuba algoritması klasik çarpmadan nasıl daha hızlıdır?

Özet

  • Böl ve fethet üç adımdan oluşur: problemi aynı türden alt problemlere böl, özyinelemeli çöz, sonuçları birleştir.
  • Kalıbın işe yaraması için alt problemler bağımsız olmalı ve birleştirme ucuz olmalıdır.
  • Karmaşıklık, yineleme bağıntısıyla ifade edilir; özyineleme ağacını çizmek en sezgisel yöntemdir.
  • Master teoremi T(n) = aT(n/b) + f(n) biçimindeki bağıntıları üç duruma göre çözer.
  • Hızlı üs alma O(log n) çarpma yapar; modüler sürümü kriptografinin temelidir.
  • Quickselect, k'ıncı en küçük elemanı beklenen O(n) sürede bulur; yalnızca bir tarafa iner.
  • Var olan bir böl-ve-fethet algoritmasına ek bilgi toplamak güçlü bir tekniktir; ters çevrim sayma bunun örneğidir.
  • Alt problemler örtüşüyorsa böl ve fethet üstel sürede çalışır; dinamik programlamaya geçilmelidir.
  • Her problem için böl ve fethet en iyi çözüm değildir; maksimum alt dizi probleminde doğrusal çözüm daha iyidir.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.