İçeriğe geç
academia.sh

Ders 07 / 23

İki İşaretçi

Sıralı dizide karşılıklı tarama; ön koşul bozulduğunda ortaya çıkan 25 yanlış yanıt ve ön koşulu sağlamanın adım bedeli.

İçindekiler

Önceki konu bir algoritmanın doğruluğunun olasılıkla ifade edilebileceğini gösterdi: beklenen başarım bir dağılımdır, tek koşum bir örnektir. Bu konu farklı bir belirsizlik kaynağıyla açılıyor ve bu kaynak olasılıksal değil yapısaldır. Bir problem çözme kalıbı seçmek bir hızlanma satın almak değil, bir ön koşulu kabul etmektir.

Ön koşul sağlandığı sürece kalıp hem doğru hem ucuzdur. Sağlanmadığında kalıp durmaz, uyarmaz, yavaşlamaz — yanlış yanıt verir, ve yanlışlığı çıktısına bakarak anlaşılmaz. Bu yüzden konunun sekiz dersinin sekizi de aynı çerçeveyi kurar: bir kâhin (kaba kuvvet, her zaman doğru, her zaman pahalı), bir kalıp, ve ikisinin ayrıldığı girdi sayısı. İlk kalıp iki işaretçidir; ön koşulu tek cümledir: dizi sıralı olmalı.

Problem, Kâhin ve Kalıp

Problem şudur: bir dizide toplamı hedefe eşit iki ayrı konum var mı. Kâhin bütün çiftleri dener; n değerli dizide n(n1)/2n(n-1)/2 çift vardır ve kâhin gerektiğinde hepsine bakar. Kalıp iki işaretçiyi dizinin iki ucuna koyar, toplam hedeften küçükse soldakini sağa, büyükse sağdakini sola kaydırır.

PK1. Ölçü adımdır, süre değildir. Sayaç her karşılaştırmayı bir adım sayar ve gerçek zaman hiçbir yerde ölçülmez. PK2. Dağarcık belirlenimci bir üreteçten gelir; tohum 20260218. Aynı tohum aynı 40 diziyi verir. PK3. Her dizi 12 değer taşır, değerler −9 ile 20 arasındadır. PK4. Kâhin kaba kuvvettir ve her zaman doğru sayılır. Kalıbın doğruluğu ancak kâhinle karşılaştırılarak iddia edilir. PK5. Ön koşulu sağlayan öbek, aynı 40 dizinin sıralanmış biçimidir; başka bir üreteçten gelmez. İki öbeğin tek farkı sıradır.

TOHUM, UZUNLUK, DAGARCIK = 20260218, 12, 40


def uretec(tohum):
    d = tohum

    def sonraki(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return sonraki


def dagarcik(tohum=TOHUM, n=DAGARCIK, uzunluk=UZUNLUK):
    r = uretec(tohum)
    return [{"no": i + 1, "dizi": [r(30) - 9 for _ in range(uzunluk)]}
            for i in range(n)]


class Sayac:
    def __init__(self):
        self.adim = 0

    def say(self, n=1):
        self.adim += n


def kahin_ciftler(dizi, hedef, s):
    """Butun ciftleri dener. Her zaman dogru, her zaman pahali."""
    for i in range(len(dizi)):
        for j in range(i + 1, len(dizi)):
            s.say()
            if dizi[i] + dizi[j] == hedef:
                return True
    return False


def kalip_iki_isaretci(dizi, hedef, s):
    """ONKOSUL: dizi sirali olmali."""
    sol, sag = 0, len(dizi) - 1
    while sol < sag:
        s.say()
        t = dizi[sol] + dizi[sag]
        if t == hedef:
            return True
        if t < hedef:
            sol += 1
        else:
            sag -= 1
    return False


def olc(kume, hedef):
    ayrilan, ak, ah = [], 0, 0
    for k in kume:
        s1, s2 = Sayac(), Sayac()
        a = kalip_iki_isaretci(k["dizi"], hedef, s1)
        b = kahin_ciftler(k["dizi"], hedef, s2)
        ak, ah = ak + s1.adim, ah + s2.adim
        if a != b:
            ayrilan.append(k["no"])
    return {"ayrilan": len(ayrilan), "ilk_ayrilan": ayrilan[:6],
            "kalip_adim": ak, "kahin_adim": ah, "oran": round(ah / ak, 2)}


K = dagarcik()
S = [dict(k, dizi=sorted(k["dizi"])) for k in K]
print("dagarcik:", len(K), "dizi x", UZUNLUK, "deger | kendiliginden sirali:",
      sum(1 for k in K if k["dizi"] == sorted(k["dizi"])))
for ad, kume in (("on_kosul saglaniyor", S), ("on_kosul bozuk    ", K)):
    print(f"  {ad}", olc(kume, 11))
dagarcik: 40 dizi x 12 deger | kendiliginden sirali: 0
  on_kosul saglaniyor {'ayrilan': 0, 'ilk_ayrilan': [], 'kalip_adim': 154, 'kahin_adim': 972, 'oran': 6.31}
  on_kosul bozuk     {'ayrilan': 25, 'ilk_ayrilan': [1, 2, 3, 6, 8, 9], 'kalip_adim': 372, 'kahin_adim': 866, 'oran': 2.33}

Üç sayı yan yana duruyor. Ön koşul sağlandığında kalıp 40 girdinin 40’ında kâhinle aynı yanıtı veriyor ve 154 adım harcıyor; kâhin 972 adım harcıyor, oran 6,31. Ön koşul bozulduğunda ayrılan girdi 25 oluyor ve oran 2,33’e düşüyor.

İkinci satırın en önemli tarafı ayrılan girdi sayısı değil, ikisinin birlikte olmasıdır. Yanlışlık ucuz da değildir: kalıbın adımı 154’ten 372’ye çıkıyor, çünkü sırasız dizide işaretçiler doğru çifti bulamadan uçlarda buluşuyor ve kalıp erken çıkamıyor. Hızlanma 6,31 kattan 2,33 kata düşerken doğruluk da gidiyor.

Ayrılan Girdide Ne Oluyor

Ayrılmanın nedeni tek bir yerdedir. Kalıp, “toplam küçükse soldakini büyüt” kuralını uygularken soldaki değerin sağa doğru büyüdüğünü varsayar. Sıralı olmayan dizide bu varsayım yanlıştır; işaretçi bir kez yanlış yöne kaydırıldığında atlanan konumlara bir daha dönülmez.

def uretec(tohum):
    d = tohum

    def sonraki(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return sonraki


def dagarcik(tohum):
    r = uretec(tohum)
    return [[r(30) - 9 for _ in range(12)] for _ in range(40)]


def kahin_ciftler(dizi, hedef):
    for i in range(len(dizi)):
        for j in range(i + 1, len(dizi)):
            if dizi[i] + dizi[j] == hedef:
                return True, (dizi[i], dizi[j])
    return False, None


def kalip_iki_isaretci(dizi, hedef):
    sol, sag = 0, len(dizi) - 1
    while sol < sag:
        t = dizi[sol] + dizi[sag]
        if t == hedef:
            return True
        if t < hedef:
            sol += 1
        else:
            sag -= 1
    return False


ilk = dagarcik(20260218)[0]
print("girdi 1  :", ilk)
print("  kalip  :", kalip_iki_isaretci(ilk, 11))
print("  kahin  :", kahin_ciftler(ilk, 11))
print("  sirali :", sorted(ilk), "-> kalip", kalip_iki_isaretci(sorted(ilk), 11))
print()
print("tohum      hedef  on_kosul     ayrilan/40")
for tohum in (20260218, 20260219):
    for hedef in (11, 25):
        K = dagarcik(tohum)
        for ad, kume in (("saglaniyor", [sorted(d) for d in K]), ("bozuk     ", K)):
            ayrilan = sum(1 for d in kume
                          if kalip_iki_isaretci(d, hedef) != kahin_ciftler(d, hedef)[0])
            print(f"{tohum}  {hedef:5d}  {ad}  {ayrilan:8d}"
                  f"    oran {ayrilan / 40:.4f}")
girdi 1  : [-8, -5, 2, -1, 2, 5, 4, 1, 16, 17, 6, -1]
  kalip  : False
  kahin  : (True, (-5, 16))
  sirali : [-8, -5, -1, -1, 1, 2, 2, 4, 5, 6, 16, 17] -> kalip True

tohum      hedef  on_kosul     ayrilan/40
20260218     11  saglaniyor         0    oran 0.0000
20260218     11  bozuk             25    oran 0.6250
20260218     25  saglaniyor         0    oran 0.0000
20260218     25  bozuk             22    oran 0.5500
20260219     11  saglaniyor         0    oran 0.0000
20260219     11  bozuk             24    oran 0.6000
20260219     25  saglaniyor         0    oran 0.0000
20260219     25  bozuk             16    oran 0.4000

Birinci girdide kâhin (-5, 16) çiftini buluyor; kalıp False döndürüyor. Aynı dizi sıralandığında kalıp da True döndürüyor. Girdi değişmedi, yalnız sırası değişti — kalıbın yanıtını değiştiren şey verinin içeriği değil, ön koşulun sağlanıp sağlanmadığıdır.

PK6. Ayrılan girdi sayısı 40 üzerindendir. 40 girdide 1 ayrılma 0,0250’dir; 1 girdilik fark ölçülmemiş sayılır, 3 ve üzeri anlamlıdır. PK7. İkinci dağarcık 20260219 tohumundan gelir ve yalnız oranın büyüklük düzenini sınamak için kullanılır.

İkinci dağarcıkta hedef 11 için ayrılan girdi 24, birincide 25. Hedef 25 için 16 ve 22. Dört ölçümün dördünde de oran 0,40 ile 0,63 arasında, yani aynı büyüklük düzeninde; sonuç dağarcığa bağlı değildir. Ön koşulun sağlandığı dört satırda ayrılan girdi sıfırdır ve bu da dağarcıktan bağımsızdır.

Ön koşulu Sağlamanın Bedeli

Buraya kadarki ölçüm bir soruyu açıkta bırakıyor: dizi sıralı değilse sıralanabilir. O hâlde kalıp yine kullanılabilir. Ama sıralamanın kendisi bir adım harcar ve bu adım kalıbın hanesine yazılmalıdır.

PK8. Ön koşulu sağlamanın bedeli ölçülürken sıralama karşılaştırmalı bir yordamla yapılır ve karşılaştırmaları adım olarak sayılır. Sıralama yordamlarının kendisi Algoritmalar kursunda ölçüldü; burada tekrarlanmaz, yalnız adımı sayılır.

def uretec(tohum):
    d = tohum

    def sonraki(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return sonraki


def dagarcik(tohum=20260218):
    r = uretec(tohum)
    return [[r(30) - 9 for _ in range(12)] for _ in range(40)]


class Sayac:
    def __init__(self):
        self.adim = 0

    def say(self):
        self.adim += 1


def kahin_ciftler(dizi, hedef, s):
    for i in range(len(dizi)):
        for j in range(i + 1, len(dizi)):
            s.say()
            if dizi[i] + dizi[j] == hedef:
                return True
    return False


def kalip_iki_isaretci(dizi, hedef, s):
    sol, sag = 0, len(dizi) - 1
    while sol < sag:
        s.say()
        t = dizi[sol] + dizi[sag]
        if t == hedef:
            return True
        if t < hedef:
            sol += 1
        else:
            sag -= 1
    return False


def sirala_sayarak(dizi, s):
    """Onkosulu saglamanin bedeli. Karsilastirmalar adim olarak sayilir."""
    a = list(dizi)
    for i in range(1, len(a)):
        j = i
        while j > 0:
            s.say()
            if a[j - 1] <= a[j]:
                break
            a[j - 1], a[j] = a[j], a[j - 1]
            j -= 1
    return a


print("hedef  dogru  yalniz kalip  siralama dahil  kahin   oran")
for hedef in (11, 25):
    yalniz, dahil, kahin, dogru = 0, 0, 0, 0
    for dizi in dagarcik():
        sa, sk, sh = Sayac(), Sayac(), Sayac()
        y = kalip_iki_isaretci(sirala_sayarak(dizi, sa), hedef, sk)
        h = kahin_ciftler(dizi, hedef, sh)
        yalniz += sk.adim
        dahil += sa.adim + sk.adim
        kahin += sh.adim
        dogru += (y == h)
    print(f"{hedef:5d}  {dogru:2d}/40  {yalniz:12d}  {dahil:14d}  {kahin:5d}"
          f"  {kahin / dahil:5.2f}")
hedef  dogru  yalniz kalip  siralama dahil  kahin   oran
   11  40/40           154            1737    866   0.50
   25  40/40           352            1935   1564   0.81

Doğruluk geri geldi: 40/40. Ama oran 1’in altına düştü. Hedef 11 için sıralama dahil toplam 1737 adım, kâhin 866 adım; kalıp kâhinin iki katı iş yapıyor. Hedef 25 için 1935’e karşı 1564, oran 0,81.

Bu, konunun ikinci iddiasının ilk ödemesidir: hızlandırma bazen hızlandırmaz. Bu büyüklükte bir girdide iki işaretçi, ön koşulu kendisi sağlamak zorunda kaldığında kaba kuvvetten pahalıdır. Kalıbın kazandığı yer, sıralamanın bir kez yapılıp çok kez sorgulandığı ya da verinin zaten sıralı geldiği durumdur. Dizi uzadıkça denge değişir — sıralama nlognn \log n, kâhin n2n^2 büyür — ama bu dağarcıkta 12 değer, dengeyi kaba kuvvetin lehine bırakacak kadar küçüktür.

Adım Sayısı Doğruluk Hakkında Bir Şey Söylemez

Yukarıdaki iki ölçümde ayrılma, adımda bir değişiklikle birlikte geldi: 154’ten 372’ye. Bu bir kural değildir ve buna güvenmek tehlikelidir. Aynı kalıbın sayan biçimi bunu gösterir. Problem şimdi “toplamı hedeften küçük kaç çift var” sorusudur; kalıp, sağ uçta bir çift sayıldığında aradaki bütün çiftlerin de sayılacağını kullanır.

PK9. Sayan biçimde kalıp erken çıkamaz; her koşumda işaretçiler tam olarak n−1 adım atar. Adım sayısı bu yüzden girdinin içeriğinden bağımsızdır.

def uretec(tohum):
    d = tohum

    def sonraki(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return sonraki


def dagarcik(tohum=20260218):
    r = uretec(tohum)
    return [[r(30) - 9 for _ in range(12)] for _ in range(40)]


class Sayac:
    def __init__(self):
        self.adim = 0

    def say(self):
        self.adim += 1


def kahin_kucuk_cift(dizi, hedef, s):
    """Toplami hedeften kucuk cift sayisi. Butun ciftler denenir."""
    say = 0
    for i in range(len(dizi)):
        for j in range(i + 1, len(dizi)):
            s.say()
            if dizi[i] + dizi[j] < hedef:
                say += 1
    return say


def kalip_kucuk_cift(dizi, hedef, s):
    """ONKOSUL: dizi sirali olmali. Sag ucta bir cift sayilirsa aradakiler de sayilir."""
    sol, sag, say = 0, len(dizi) - 1, 0
    while sol < sag:
        s.say()
        if dizi[sol] + dizi[sag] < hedef:
            say += sag - sol
            sol += 1
        else:
            sag -= 1
    return say


print("on_kosul     ayrilan/40  kalip  kahin   oran")
for ad, hazirla in (("saglaniyor", sorted), ("bozuk     ", list)):
    ayrilan, ak, ah = 0, 0, 0
    for dizi in dagarcik():
        d = hazirla(dizi)
        s1, s2 = Sayac(), Sayac()
        a = kalip_kucuk_cift(d, 6, s1)
        b = kahin_kucuk_cift(d, 6, s2)
        ak, ah = ak + s1.adim, ah + s2.adim
        ayrilan += (a != b)
    print(f"{ad}  {ayrilan:8d}  {ak:5d}  {ah:5d}  {ah / ak:5.2f}")
on_kosul     ayrilan/40  kalip  kahin   oran
saglaniyor         0    440   2640   6.00
bozuk             39    440   2640   6.00

İki satırın adım sütunları birebir aynı: kalıp 440, kâhin 2640, oran 6,00. Ayrılan girdi sütunu ise 0’dan 39’a çıkıyor — 40 girdinin 39’unda kalıp yanlış bir sayı döndürüyor. Kalıp yine bir tam sayı veriyor, yine altı kat hızlı, yine hiçbir uyarı üretmiyor.

Bu, konunun üçüncü iddiasının neden gerekli olduğunu gösteriyor. Adım sayısı bir başarım ölçüsüdür ve doğruluk hakkında hiçbir şey söylemez. Kaba kuvvet burada bir “yavaş seçenek” değil, 39 yanlış yanıtı görünür kılan tek araçtır. Kâhin olmasaydı bu tablonun iki satırı ayırt edilemezdi.

Üç Sayı

Ölçüt Kâhin Kalıp Ayrılan girdi
Ön koşul sağlanıyor (hedef 11) 972 adım 154 adım 0/40
Ön koşul bozuk (hedef 11) 866 adım 372 adım 25/40
Ön koşulu sağlayarak (hedef 11) 866 adım 1737 adım 0/40
Sayan biçim, ön koşul bozuk 2640 adım 440 adım 39/40

Üç satır üç ayrı karar noktasıdır. Birinci satır kalıbın vaadidir. İkinci satır ön koşul denetlenmediğinde ne olduğunu gösterir: yanıtların yüzde altmış ikisi bozuk ve hızlanma üçte iki oranında erimiş. Üçüncü satır ön koşulu kendi elinle sağlamanın faturasıdır.

Kalıbın bir denetim eklenerek güvenli kılınabileceği düşünülebilir: dizinin sıralı olup olmadığına bakılır, değilse kâhine dönülür. Bu denetim n−1 karşılaştırmadır, yani 40 dizi için 440 adım. Denetim doğruluğu kurtarır ama hızlanmayı kurtarmaz: sırasız girdide iş yine kâhine kalır. Ölçülmesi gereken şey denetimin maliyeti değil, girdilerin kaçının ön koşulu sağladığıdır. Bu dağarcıkta o sayı sıfırdır.

Özet

  • Bir kalıp seçmek bir ön koşulu kabul etmektir; iki işaretçinin ön koşulu dizinin sıralı olmasıdır.
  • Ön koşul sağlandığında kalıp 40 girdinin 40’ında kâhinle aynı yanıtı veriyor ve 972 yerine 154 adım harcıyor; oran 6,31.
  • Ön koşul bozulduğunda kalıp 25 girdide kâhinden ayrılıyor ve hızlanma 2,33 kata düşüyor; yanlışlık ucuz değildir.
  • İkinci dağarcıkta ayrılan girdi 24; oran aynı büyüklük düzeninde kaldığı için sonuç dağarcığa bağlı değildir.
  • Ön koşulu sıralayarak sağlamak doğruluğu geri getiriyor ama toplam adımı 1737’ye çıkarıyor ve kalıbı kâhinden pahalı kılıyor.

Sonraki Adım

İki işaretçi diziyi iki uçtan sıkıştırıyordu ve ön koşulu sıraydı. Sonraki kalıp işaretçileri aynı yönde tutar ve aralarındaki bölgeyi bir pencere gibi büyütüp küçültür; kazancı, pencere kaydıkça toplamı sıfırdan hesaplamak yerine artımlı güncellemesinden gelir. Ön koşulu da farklıdır ve sıraya hiç bakmaz: hiçbir değer negatif olmamalıdır. Sonraki ders bu ön koşulun bozulduğu 10 girdiyi kâhinle sayacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat