Bölüm 5: Dizgi Algoritmaları
Giriş
Öğrenme Çıktıları
Metin, örüntü, kayma, önek ve sonek kavramlarını tanımlamak; dizgi eşleştirme problemini biçimsel olarak ifade etmek.
Kaba kuvvet, KMP, Boyer-Moore ve Rabin-Karp algoritmalarını bir örnek üzerinde adım adım çalıştırmak ve Java ile yazmak.
LPS tablosu, kötü karakter tablosu ve kayan hash'in zaman kazancını açıklamak; en iyi, ortalama ve en kötü durumları karşılaştırmak.
RLE ve LZW ile kayıpsız sıkıştırma yapmak, sıkıştırılmış veriyi geri açmak ve sıkıştırma oranını hesaplamak.
Giriş
Dizgi Algoritmaları
- Metinlerle dolu bir dünyada yaşıyoruz: e-postalar, mesajlar, sosyal medya paylaşımları, haber metinleri…
- Bilgisayarlarımızda her gün sayısız metinle karşılaşıyoruz. Peki bu metinler nasıl düzenlenir ve analiz edilir?
- Dizgi (string) algoritmaları metinlerde arama, değiştirme ve karşılaştırma gibi işlemleri gerçekleştirir.
- Bu sunumun iki ana konusu: dizgi eşleştirme (bir örüntüyü metinde bulmak) ve dizgi sıkıştırma (metni daha az yerde saklamak).
Editörlerde Ctrl+F, grep, arama motorları, virüs imzası tarama, intihal tespiti, DNA dizilerinde gen arama, ZIP/GIF/PNG gibi dosya biçimleri.
Giriş
Temel Kavramlar ve Gösterim
- Alfabe Σ: kullanılan karakterler kümesi (ör. {a, b, …, z}, {A, C, G, T}).
- Metin (text) T[0..n−1] ve örüntü (pattern) P[0..m−1], genellikle m ≤ n.
- P, T içinde s kaymasında (shift) geçer ⇔ T[s..s+m−1] = P. Geçerli kaymalar 0 ≤ s ≤ n − m.
- Önek (prefix): P[0..k−1]; sonek (suffix): P[m−k..m−1]. Uygun (proper) önek/sonek, dizginin kendisine eşit olmayandır.
- Örnek: "ababd" için önekler a, ab, aba, abab; sonekler d, bd, abd, babd.
Dizgi eşleştirme problemi: P'nin geçtiği tüm (ya da ilk) s değerlerini bulmak.
Giriş
Bölümün Haritası
Kaba Kuvvet
Kaba Kuvvet (Naive) Algoritması
- Metin içinde belirli bir örüntüyü arayan en basit algoritmadır; "naive" (saf) adı bu yüzdendir.
- İşleyiş:
- Her s = 0, 1, …, n − m kayması için örüntü metnin altına hizalanır.
- Karakterler soldan sağa karşılaştırılır; ilk uyuşmazlıkta durulur.
- m karakterin hepsi eşleşirse s rapor edilir; aksi hâlde örüntü 1 konum kaydırılır.
- Ön işleme ve ek bellek gerektirmez.
En kötü durum: (n − m + 1) · m karşılaştırma → O(n · m).
En iyi durum: her hizada ilk karakter uyuşmaz → O(n).
n: metin uzunluğu, m: örüntü uzunluğu.
Kaba Kuvvet
Örnek: "THIS IS A SIMPLE EXAMPLE"
Her satır bir kayma: yeşil eşleşen, kırmızı uyuşmayan karşılaştırma. SIMPLE, 11. hizada (s = 10) bulunur; toplam 18 karşılaştırma yapılır (n = 24, m = 6).
Kaba Kuvvet · Kodu adım adım çalıştır
"HELLO WORLD" İçinde "LO": Kod İzleme
n = 11, m = 2. Örüntü i = 0, 1, …, 9 kaymalarında denenecek.
t[0] = 'H' ≠ p[0] = 'L' → uyuşmazlık; örüntü 1 sağa kayar.
t[1] = 'E' ≠ p[0] = 'L' → uyuşmazlık; örüntü 1 sağa kayar.
t[2] = 'L' ile p[0] = 'L' eşleşti → j++.
t[3] = 'L' ≠ p[1] = 'O' → uyuşmazlık; örüntü 1 sağa kayar.
t[3] = 'L' ile p[0] = 'L' eşleşti → j++.
t[4] = 'O' ile p[1] = 'O' eşleşti → j++.
j == m: örüntü i = 3 konumunda bulundu. Toplam 6 karşılaştırma.
int kabaKuvvet(String t, String p) { int n = t.length(), m = p.length(); for (int i = 0; i <= n - m; i++) { int j = 0; while (j < m && t.charAt(i + j) == p.charAt(j)) j++; if (j == m) return i; } return -1; }
Kaba Kuvvet
Kaba Kuvvet: En İyi, Ortalama, En Kötü
| Durum | Ne zaman? | Karşılaştırma | Zaman |
|---|---|---|---|
| En iyi | P[0] metnin hiçbir karakteriyle eşleşmez (ör. P = "xyz", T = "aaaa…") | n − m + 1 | O(n) |
| Ortalama | Rastgele metin, büyük alfabe: uyuşmazlık genellikle ilk 1–2 karakterde | ≈ n · σ/(σ−1) | O(n) |
| En kötü | T = "aaaaaaaaaa", P = "aaab": her hizada m karşılaştırma | (n − m + 1) · m | O(n · m) |
KMP
Knuth-Morris-Pratt (KMP) Algoritması
- Metin içinde belirli bir örüntüyü bulmak için kullanılır.
- Donald Knuth, Vaughan Pratt ve (bağımsız olarak) James H. Morris tarafından geliştirilmiş, 1977'de birlikte yayımlanmıştır.
- Temel fikir: uyuşmazlık olduğunda örüntünün zaten eşleşmiş kısmı hakkında bildiklerimizi kullanmak; metin işaretçisi asla geri gitmez.
- Bu bilgi örüntüden önceden hesaplanan LPS tablosunda (önek fonksiyonu, π) saklanır.
Ön işleme: O(m)
Arama: O(n)
Toplam: O(n + m), en kötü durumda da
Ek bellek: O(m)
KMP
KMP: İşleyiş
KMP
LPS Tablosu (Önek Fonksiyonu)
lps[q] = P[0..q] dizgisinin, kendisine eşit olmayan ve aynı zamanda soneki olan en uzun önekinin uzunluğu.
- lps[0] = 0 (tek karakterin uygun öneki yok).
- Örnek: Metin "ababcababcabababd", örüntü "ababd".
- Örüntü tablosu: a: 0, b: 0, a: 1, b: 2, d: 0.
| q | P[q] | lps[q] | Gerekçe (P[0..q]) |
|---|---|---|---|
| 0 | a | 0 | "a" — uygun önek yok |
| 1 | b | 0 | "ab" — a ≠ b |
| 2 | a | 1 | "aba" — "a" hem önek hem sonek |
| 3 | b | 2 | "abab" — "ab" hem önek hem sonek |
| 4 | d | 0 | "ababd" — d ile biten önek yok |
KMP · Kodu adım adım çalıştır
LPS Tablosunu Kurmak: "ACABACACD"
lps[0] = 0 her zaman. k = 0: şu an eşleşen önek uzunluğu.
p[0] = 'A' ≠ p[1] = 'C' → k = 0, lps[1] = 0.
p[0] = 'A' = p[2] = 'A' → k = 1, lps[2] = 1.
p[1] = 'C' ≠ p[3] = 'B' → geri dön: k = lps[0] = 0.
p[0] = 'A' ≠ p[3] = 'B' → k = 0, lps[3] = 0.
p[0] = 'A' = p[4] = 'A' → k = 1, lps[4] = 1.
p[1] = 'C' = p[5] = 'C' → k = 2, lps[5] = 2. "AC" hem önek hem sonek.
p[2] = 'A' = p[6] = 'A' → k = 3, lps[6] = 3. "ACA" hem önek hem sonek.
p[3] = 'B' ≠ p[7] = 'C' → geri dön: k = lps[2] = 1.
p[1] = 'C' = p[7] = 'C' → k = 2, lps[7] = 2. "AC" hem önek hem sonek.
p[2] = 'A' ≠ p[8] = 'D' → geri dön: k = lps[1] = 0.
p[0] = 'A' ≠ p[8] = 'D' → k = 0, lps[8] = 0.
Sonuç: lps = [0, 0, 1, 0, 1, 2, 3, 2, 0]. Her q için en fazla sabit sayıda iş (amortize) → O(m).
int[] lpsHesapla(String p) { int m = p.length(), k = 0; int[] lps = new int[m]; for (int q = 1; q < m; q++) { while (k > 0 && p.charAt(k) != p.charAt(q)) k = lps[k - 1]; if (p.charAt(k) == p.charAt(q)) k++; lps[q] = k; } return lps; }
KMP
KMP Aramasının Üç Evresi
KMP
KMP Örneği: "ABCDABD" Aranıyor
lps("ABCDABD") = [0, 0, 0, 0, 1, 2, 0]. yeşil eşleşen, kırmızı uyuşmayan, mavi LPS sayesinde bilinen (tekrar karşılaştırılmayan) karakterler.
s = 0: ABC eşleşti, t[3] = 'A' ≠ p[3] = 'D'. q = 3 → q = lps[2] = 0; örüntü 3 kayar, t[3] yeniden p[0] ile karşılaştırılır.
s = 3: ABCDAB eşleşti, t[9] = 'A' ≠ 'D'. q = 6 → q = lps[5] = 2: "AB" zaten eşleşmiş bilinir, örüntü 4 kayar.
s = 7: AB yeniden karşılaştırılmaz. t[9] = 'A' ≠ p[2] = 'C' → q = lps[1] = 0.
s = 9: ABCDAB eşleşti, t[15] = 'C' ≠ 'D'. q = 6 → q = lps[5] = 2; örüntü 4 kayar.
s = 13: AB bilinir; CDABD da eşleşir → örüntü 13. konumda bulundu. Metin işaretçisi hiç geri gitmedi.
Toplam 24 karakter karşılaştırması (n = 21); kaba kuvvet aynı metinde 37 karşılaştırma yapar.
KMP · Kodu adım adım çalıştır
kmpAra("ababcababcabababd", "ababd")
n = 17, m = 5, lps = [0, 0, 1, 2, 0], q = 0.
p[0] = t[0] = 'a' → q = 1.
p[1] = t[1] = 'b' → q = 2.
p[2] = t[2] = 'a' → q = 3.
p[3] = t[3] = 'b' → q = 4.
p[4] = 'd' ≠ t[4] = 'c' → q = lps[3] = 2 (i yerinde kalır).
p[2] = 'a' ≠ t[4] = 'c' → q = lps[1] = 0 (i yerinde kalır).
p[0] = 'a' ≠ t[4] = 'c' → q = 0 kalır, i ilerler.
p[0] = t[5] = 'a' → q = 1.
p[1] = t[6] = 'b' → q = 2.
p[2] = t[7] = 'a' → q = 3.
p[3] = t[8] = 'b' → q = 4.
p[4] = 'd' ≠ t[9] = 'c' → q = lps[3] = 2 (i yerinde kalır).
p[2] = 'a' ≠ t[9] = 'c' → q = lps[1] = 0 (i yerinde kalır).
p[0] = 'a' ≠ t[9] = 'c' → q = 0 kalır, i ilerler.
p[0] = t[10] = 'a' → q = 1.
p[1] = t[11] = 'b' → q = 2.
p[2] = t[12] = 'a' → q = 3.
p[3] = t[13] = 'b' → q = 4.
p[4] = 'd' ≠ t[14] = 'a' → q = lps[3] = 2 (i yerinde kalır).
p[2] = t[14] = 'a' → q = 3.
p[3] = t[15] = 'b' → q = 4.
p[4] = t[16] = 'd' → q = 5.
q == m → i − m + 1 = 12 döndürülür. Metin yalnızca bir kez tarandı.
int kmpAra(String t, String p) { int n = t.length(), m = p.length(); int[] lps = lpsHesapla(p); int q = 0; // eşleşen uzunluk for (int i = 0; i < n; i++) { while (q > 0 && p.charAt(q) != t.charAt(i)) q = lps[q - 1]; if (p.charAt(q) == t.charAt(i)) q++; if (q == m) return i - m + 1; } return -1; }
KMP
KMP: Neden O(n + m)?
- Dış döngü i'yi n kez artırır; i hiç azalmaz.
- q her adımda en fazla 1 artar → toplam artış ≤ n.
whileiçindeki her geri çekilme q'yu en az 1 azaltır; q negatif olamaz → toplam geri çekilme ≤ toplam artış ≤ n.- Arama: en fazla 2n karşılaştırma → O(n). Aynı amortize argüman LPS için O(m) verir.
Artıları
- En kötü durumda bile doğrusal
- Metinde geri gitmez: akış (stream) verisine uygun
Eksileri
- Pratikte Boyer-Moore'dan genelde yavaş
- Küçük alfabede (DNA) atlama kazancı sınırlı
Boyer-Moore
Boyer-Moore Algoritması
- Metin içinde belirli bir örüntüyü arar; Robert S. Boyer ve J Strother Moore tarafından 1977'de geliştirilmiştir.
- Örüntü soldan sağa kaydırılır ama karakterler sağdan sola karşılaştırılır.
- Uyuşmazlıkta iki kuraldan büyük olan kadar kaydırılır: kötü karakter ve iyi sonek.
- Metindeki karakterlerin çoğu hiç okunmadan atlanabilir: uzun örüntü ve geniş alfabede (doğal dil metni) çok hızlıdır;
grepve editörlerin aramaları bu ailedendir.
En iyi: O(n/m) — her denemede ilk karşılaştırma uyuşmaz ve m atlanır.
Ortalama: alt-doğrusal, pratikte O(n)'den hızlı.
En kötü: yalnız kötü karakterle O(n · m); iyi sonek + Galil kuralıyla O(n + m).
Boyer-Moore
Boyer-Moore: İşleyiş
- Ön işleme: örüntüden kaydırma tabloları kurulur: her karakterin örüntüdeki son görülme konumu (kötü karakter) ve her sonek için kaydırma miktarı (iyi sonek).
- Arama: örüntü metnin altına hizalanır; karşılaştırma örüntünün son karakterinden başlayıp sola doğru ilerler.
- Eşleşme kontrolü: tüm karakterler eşleşirse konum rapor edilir.
- Kötü karakter kuralı: uyuşmayan metin karakterinin örüntüdeki en sağdaki konumu dikkate alınarak kaydırma yapılır.
- İyi sonek kuralı: eşleşmiş sonekin örüntü içinde başka bir yerde bulunup bulunmadığına göre kaydırma yapılır.
Boyer-Moore
Kötü Karakter (Bad Character) Kuralı
- p[j] ≠ t[s + j] olsun; c = t[s + j] kötü karakter.
- c örüntüde j'nin solunda geçiyorsa: en sağdaki c, t[s + j]'nin altına gelecek şekilde kaydır.
- c örüntüde hiç yoksa (son = −1): örüntüyü c'nin tamamen ötesine kaydır (j + 1).
- Formül: s += max(1, j − son[c]). c, j'nin sağında son kez geçiyorsa j − son[c] ≤ 0 olur; bu yüzden en az 1.
int[] sonGorulme(String p) { int[] son = new int[256]; Arrays.fill(son, -1); for (int i = 0; i < p.length(); i++) son[p.charAt(i)] = i; return son; }
Boyer-Moore
İyi Sonek (Good Suffix) Kuralı
- Uyuşmazlıktan önce eşleşen sonek u = P[j+1..m−1] metinde bulunmuştur: "iyi sonek".
- Durum 1: u örüntüde başka yerde (önünde farklı bir karakterle) geçiyorsa, o geçiş metindeki u'nun altına gelecek şekilde kaydır.
- Durum 2: yoksa, u'nun bir sonekine eşit olan en uzun P önekini hizala.
- Durum 3: hiçbiri yoksa m kaydır.
- Tablo O(m) zamanda önceden hesaplanır (KMP'nin LPS fikrine benzer, sonekler için).
"AB" eşleşti, t[2] = 'A' ≠ p[2] = 'B'. Kötü karakter: son[A] = 3 > j → yalnız 1. İyi sonek: "AB" p[1..2]'de de var (önünde 'C' ≠ 'B') → 2 kaydır.
Boyer-Moore
Örnek: "LO LOELLO O HELLO" İçinde "HELLO"
| c | H | E | L | O | diğer |
|---|---|---|---|---|---|
| son[c] | 0 | 1 | 3 | 4 | −1 |
Kural: s += max(1, j − son[c]). yeşil eşleşen, kırmızı kötü karakter.
Hiza 1 (s = 0): 2 karakter eşleşti, t[2] = '␣' ≠ p[2] = 'L'. '␣' örüntüde yok → j − (−1) = 3 → 3 kaydır.
Hiza 2 (s = 3): t[7] = 'L' ≠ p[4] = 'O'. son['L'] = 3 → j − 3 = 1 → 1 kaydır.
Hiza 3 (s = 4): 4 karakter eşleşti, t[4] = 'O' ≠ p[0] = 'H'. son['O'] = 4 ≥ j → en az 1 → 1 kaydır.
Hiza 4 (s = 5): t[9] = '␣' ≠ p[4] = 'O'. '␣' örüntüde yok → j − (−1) = 5 → 5 kaydır.
Hiza 5 (s = 10): t[14] = 'L' ≠ p[4] = 'O'. son['L'] = 3 → j − 3 = 1 → 1 kaydır.
Hiza 6 (s = 11): t[15] = 'L' ≠ p[4] = 'O'. son['L'] = 3 → j − 3 = 1 → 1 kaydır.
Hiza 7 (s = 12): 5 karakterin hepsi sağdan sola eşleşti → HELLO, s = 12'de bulundu. Toplam 7 hiza, 17 karşılaştırma.
Boyer-Moore
Horspool Basitleştirmesi ve Kaydırma Tablosu
- Horspool (1980): uyuşmazlık nerede olursa olsun, kaydırmayı hizanın son karakterinin altındaki metin karakterine göre yap.
- Kaydırma tablosu: kayd[c] = m − 1 − (c'nin P[0..m−2] içindeki en sağ konumu); yoksa m.
- HELLO: H → 4, E → 3, L → 1, O → 5 (son karakter tabloya katılmaz; O başka yerde yok), diğer → 5.
- Aynı örnekte 5 hiza yeter (temel kötü karakter kuralıyla 7).
| Hiza | s | Karşılaştırma | son kar. | kaydır |
|---|---|---|---|---|
| 1 | 0 | t[2] = '␣' ≠ p[2] | 'O' | 5 |
| 2 | 5 | t[9] = '␣' ≠ p[4] | '␣' | 5 |
| 3 | 10 | t[14] = 'L' ≠ p[4] | 'L' | 1 |
| 4 | 11 | t[15] = 'L' ≠ p[4] | 'L' | 1 |
| 5 | 12 | tümü eşleşti | — | bulundu |
Boyer-Moore
Örnek: Son Görülme Tablosu ile Arama
Örüntü abacab, son görülme: a = 4, b = 5, c = 3, * = −1. Hücre üstündeki sayı: karşılaştırma sayacı.
Hiza 1 (s = 0): t[5] = 'a' ≠ p[5] = 'b'. j − son['a'] = 5 − 4 = 1 → 1 kaydır.
Hiza 2 (s = 1): t[4] = 'a' ≠ p[3] = 'c'. son['a'] = 4 ≥ j = 3 → 1 → 1 kaydır.
Hiza 3 (s = 2): t[7] = 'a' ≠ p[5] = 'b'. j − son['a'] = 5 − 4 = 1 → 1 kaydır.
Hiza 4 (s = 3): t[8] = 'd' ≠ p[5] = 'b'. 'd' örüntüde yok → j + 1 = 6 → 6 kaydır.
Hiza 5 (s = 9): t[14] = 'a' ≠ p[5] = 'b'. j − son['a'] = 5 − 4 = 1 → 1 kaydır.
Hiza 6 (s = 10): 6 karşılaştırmanın hepsi eşleşti → eşleşme, s = 10. Tüm geçişler arandığı için 1 kaydırılıp devam edilir.
Hiza 7 (s = 11): t[16] = 'a' ≠ p[5] = 'b'. j − son['a'] = 5 − 4 = 1 → 1 kaydır.
Hiza 8 (s = 12): t[17] = 'a' ≠ p[5] = 'b'. j − son['a'] = 5 − 4 = 1 → 1 kaydır.
Hiza 9 (s = 13): t[16] = 'a' ≠ p[3] = 'c'. son['a'] = 4 ≥ j = 3 → 1 → 1 kaydır.
Hiza 10 (s = 14): t[18] = 'b' ≠ p[4] = 'a'. son['b'] = 5 ≥ j = 4 → 1 → 1 kaydır.
10 hiza, toplam 20 karşılaştırma (n = 20, m = 6): 'd' gibi örüntüde olmayan bir karakter 6 konumluk atlama sağlar.
Boyer-Moore · Kodu adım adım çalıştır
boyerMoore("merhaba naber", "abe")
n = 13, m = 3. Son görülme tablosu kuruldu; s = 0.
p[2] = 'e' ≠ t[2] = 'r'; son['r'] = -1 → s += max(1, 2 − (-1)) = 3.
p[2] = 'e' ≠ t[5] = 'b'; son['b'] = 1 → s += max(1, 2 − (1)) = 1.
p[2] = 'e' ≠ t[6] = 'a'; son['a'] = 0 → s += max(1, 2 − (0)) = 2.
p[2] = 'e' ≠ t[8] = 'n'; son['n'] = -1 → s += max(1, 2 − (-1)) = 3.
p[2] = t[11] = 'e' → j-- .
p[1] = t[10] = 'b' → j-- .
p[0] = t[9] = 'a' → j-- .
j < 0 → örüntü s = 9 konumunda bulundu ("naber").
int boyerMoore(String t, String p) { int n = t.length(), m = p.length(); int[] son = sonGorulme(p); int s = 0; while (s <= n - m) { int j = m - 1; while (j >= 0 && p.charAt(j) == t.charAt(s + j)) j--; if (j < 0) return s; s += Math.max(1, j - son[t.charAt(s + j)]); } return -1; }
Boyer-Moore
Boyer-Moore: Analiz ve Örnek
- En iyi: metin karakterleri örüntüde yok → her hizada 1 karşılaştırma, m atlama: ≈ n/m karşılaştırma.
- En kötü (yalnız kötü karakter): T = aaaa…a, P = baaa…a → her hizada m karşılaştırma, 1 kaydırma: O(n · m).
- Örüntü uzadıkça hızlanır (kaba kuvvet ve KMP'nin tersine).
- Küçük alfabede (DNA, ikili veri) kötü karakter kuralı zayıftır; iyi sonek kuralı önem kazanır.
Metin: abccbaabccbaabcbcabbabcabc
Desen: abcbcabbabcabc (m = 14)
Son görülme tablosu: a: 11, b: 12, c: 13
Sonuç: desen 12. konumda bulunur.
Rabin-Karp
Rabin-Karp Algoritması
- Metin içinde belirli bir örüntüyü bulmak için kullanılır; Michael O. Rabin ve Richard M. Karp tarafından 1987'de geliştirilmiştir.
- Karakterleri tek tek karşılaştırmak yerine örüntünün ve her m uzunluklu metin penceresinin hash (özet) değerini karşılaştırır.
- Kayan hash (rolling hash): pencere bir sağa kayınca yeni hash, eskisinden O(1)'de hesaplanır.
- Hash eşitliği eşleşmeyi garanti etmez: sahte eşleşme (spurious hit) olabilir → karakter bazında doğrulama gerekir.
En iyi: hash'ler nadiren çakışır → O(n + m)
Ortalama: pratikte O(n + m)
En kötü: her pencerede çakışma (sahte eşleşme), sürekli karakter kontrolü → O(n · m)
Rabin-Karp
Rabin-Karp: İşleyiş ve Kayan Hash
- Hash: örüntünün ve metnin ilk penceresinin hash değerleri hesaplanır.
- Eşleşme kontrolü: hash değerleri eşleşen alt dizgeler aday kabul edilir.
- Doğrulama: aday pencere karakter bazında örüntüyle karşılaştırılır.
- Kaydırma ve yeniden hesaplama: eşleşme yoksa pencere bir sağa kaydırılır, hash güncellenir.
- Tekrarlama: tüm metin boyunca adımlar tekrarlanır.
h(t[s..s+m−1]) = (t[s]·Bm−1 + t[s+1]·Bm−2 + … + t[s+m−1]) mod Q
Kaydırma: hs+1 = (B · (hs − t[s] · Bm−1) + t[s+m]) mod Q — çıkan karakter düşülür, kalanlar bir basamak sola kayar, giren karakter eklenir.
Rabin-Karp
Basit Örnek: Toplam Hash ve Sahte Eşleşme
Oyuncak hash: harf değerlerinin toplamı. "VUATS" metninde "TS" aranıyor.
Pencere "VU": 5 + 1 = 6 ≠ 5 → hash farklı, karakter karşılaştırması yapılmaz.
Pencere "UA": 1 + 4 = 5 = 5 → hash eşit ama "UA" ≠ "TS": sahte eşleşme (spurious hit); doğrulama boşa gider.
Pencere "AT": 4 + 2 = 6 ≠ 5 → geç. Yeni hash = eski − çıkan (U=1) + giren (T=2): O(1).
Pencere "TS": 2 + 3 = 5 → hash eşit, karakter karşılaştırması da tutar: eşleşme (s = 3).
Toplam hash zayıftır (harflerin sırasına duyarsız: "ST" de 5 verir). Bu yüzden konum ağırlıklı polinom hash ve büyük asal Q kullanılır.
Rabin-Karp · Kodu adım adım çalıştır
rabinKarp("abracadabra", "cad")
n = 11, m = 3, B = 256, Q = 101. h = 256² mod 101 = 88 (çıkan karakterin ağırlığı).
ph = (99·256² + 97·256 + 100) mod 101 = 11; ilk pencere "abr" için th = 4.
Pencere "abr": th = 4 ≠ 11 → karakterlere bakılmaz. Kaydır: 'a' çıkar, 'a' girer → yeni th = 30.
Pencere "bra": th = 30 ≠ 11 → karakterlere bakılmaz. Kaydır: 'b' çıkar, 'c' girer → yeni th = 17.
Pencere "rac": th = 17 ≠ 11 → karakterlere bakılmaz. Kaydır: 'r' çıkar, 'a' girer → yeni th = 41.
Pencere "aca": th = 41 ≠ 11 → karakterlere bakılmaz. Kaydır: 'a' çıkar, 'd' girer → yeni th = 11.
th = 11 = ph → esit karakter karşılaştırması: "cad" = "cad" → s = 4 döndürülür.
int rabinKarp(String t, String p) { int n = t.length(), m = p.length(); int B = 256, Q = 101, h = 1; for (int i = 0; i < m - 1; i++) h = (h * B) % Q; // B^(m-1) mod Q int ph = 0, th = 0; for (int i = 0; i < m; i++) { ph = (B * ph + p.charAt(i)) % Q; th = (B * th + t.charAt(i)) % Q; } for (int s = 0; s <= n - m; s++) { if (ph == th && esit(t, p, s)) return s; if (s < n - m) { th = (B * (th - t.charAt(s) * h) + t.charAt(s + m)) % Q; if (th < 0) th += Q; } } return -1; }
Rabin-Karp
Kayan Pencere ve Analiz
n − m + 1 = 7 pencere; her biri bir öncekinden O(1)'de.
- Hash hesapları: O(m) + (n − m) · O(1).
- Q büyük bir asal seçilirse sahte eşleşme olasılığı ≈ 1/Q → beklenen süre O(n + m).
- Kötü Q veya kötü niyetli girdi: her pencere aday → O(n · m).
- Güçlü yanı: aynı uzunlukta çok sayıda örüntü tek geçişte aranabilir (hash'ler bir kümeye konur): intihal tespiti, belge benzerliği.
- Ayrıca 2B örüntü (görüntü içinde blok) aramaya genellenebilir.
Eşleştirme Özeti
Dizgi Eşleştirme Algoritmalarının Karşılaştırması
| Algoritma | Temel fikir | Ön işleme | Ortalama | En kötü | Ek bellek |
|---|---|---|---|---|---|
| Kaba kuvvet | Her konumu dener | yok | O(n)* | O(n · m) | O(1) |
| KMP | LPS tablosu, metinde geri gitmez | O(m) | O(n + m) | O(n + m) | O(m) |
| Boyer-Moore | Sağdan sola, kötü karakter + iyi sonek | O(m + σ) | alt-doğrusal | O(n · m)** | O(m + σ) |
| Rabin-Karp | Kayan hash karşılaştırması | O(m) | O(n + m) | O(n · m) | O(1) |
* rastgele metin ve geniş alfabe için; düzenli (tekrarlı) metinde O(n · m)'ye yaklaşır. ** yalnız kötü karakter kuralıyla; iyi sonek + Galil kuralıyla O(n + m). σ: alfabe boyutu.
Sıkıştırma
Dizgi Sıkıştırma Algoritmaları
- Amaç: metni (veya veriyi) daha az bitle saklamak/iletmek.
- Kayıpsız (lossless) sıkıştırma: açıldığında orijinal veri aynen elde edilir. RLE ve LZW kayıpsızdır (metin, program, PNG/GIF).
- Kayıplı (lossy) sıkıştırma: algıda önemsiz ayrıntılar atılır (JPEG, MP3); metin için uygun değildir.
- Sıkıştırma oranı = sıkıştırılmış boyut / orijinal boyut (küçük olması iyi).
RLE
Sıralı Sıkıştırma Kodlaması (RLE)
- Run Length Encoding: ardışık tekrar eden karakterleri (koşu, run) sayı + karakter çiftiyle değiştirir.
- Adımlar: metin soldan sağa taranır → ardışık tekrarlar sayılır → sayı ve karakter birleştirilir → sıkıştırılmış metin olarak saklanır.
- Metin tek bir kez tarandığı için zaman O(n).
- Basit ve etkili; ancak tekrar az ise etkisiz, hatta zararlıdır: "ABCD" → "1A1B1C1D" (iki kat!).
- Kullanım: faks (siyah-beyaz satırlar), BMP/PCX/TIFF, basit simgeler.
Metin: AAAAABBBCCCCDDD (15 karakter)
Sıkıştırılmış: 5A3B4C3D (8 karakter)
Oran: 8/15 ≈ 0,53
RLE · Kodu adım adım çalıştır
rleSikistir("AAAAABBBCCCCDDD")
n = 15, sonuc boş.
Yeni koşu: c = 'A', sayac = 1 (i = 0).
while döngüsü 4 kez döndü: sayac = 5, i = 4. sonuc'a "5A" eklenir.
Yeni koşu: c = 'B', sayac = 1 (i = 5).
while döngüsü 2 kez döndü: sayac = 3, i = 7. sonuc'a "3B" eklenir.
Yeni koşu: c = 'C', sayac = 1 (i = 8).
while döngüsü 3 kez döndü: sayac = 4, i = 11. sonuc'a "4C" eklenir.
Yeni koşu: c = 'D', sayac = 1 (i = 12).
while döngüsü 2 kez döndü: sayac = 3, i = 14. sonuc'a "3D" eklenir.
Sonuç: "5A3B4C3D" — 15 karakter yerine 8.
String rleSikistir(String s) {
StringBuilder sonuc = new StringBuilder();
int n = s.length();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
int sayac = 1;
while (i + 1 < n &&
s.charAt(i + 1) == c) {
sayac++;
i++;
}
sonuc.append(sayac).append(c);
}
return sonuc.toString();
}RLE
RLE'nin Bayt Düzeyinde Kodlanması
Metin: aaaaaaaaaabbbbbbececececececdddddddddddddddecb (46 bayt)
| Sayı–karakter çiftleri | (10, a)(6, b)(1, e)(1, c)(1, e)(1, c)(1, e)(1, c)(1, e)(1, c)(1, e)(1, c)(1, e)(1, c)(15, d)(1, e)(1, c)(1, b) |
| Bayraksız RLE (her koşu 2 bayt) | 10 97 06 98 01 101 01 99 01 101 01 99 01 101 01 99 01 101 01 99 01 101 01 99 01 101 01 99 15 100 01 101 01 99 01 98 → 36 bayt |
| Bayraklı RLE (bayrak = 255) | 255 10 97 255 06 98 101 99 101 99 101 99 101 99 101 99 101 99 255 15 100 101 99 98 → 24 bayt |
255 sayı karakter üçlüsüyle yazar, diğer karakterleri olduğu gibi bırakır (255 değerinin kendisi de kaçış ile yazılmalıdır).RLE
İkili (Siyah-Beyaz) Görüntüde RLE
- Her satır soldan sağa taranır: W = beyaz, B = siyah, ardından koşu uzunluğu.
- 42 piksel → 18 koşu (her satır ayrı kodlanır). Yalnızca iki renk olduğu için renk adı bile yazılmayabilir: koşular dönüşümlü olarak beyaz/siyah kabul edilir (faks standardı bu fikri kullanır).
- Büyük düz alanlı görüntülerde (çizim, metin taraması) çok etkilidir; fotoğraflarda etkisizdir.
RLE
Piksel Düzeyinde RLE (RGB)
Mavi (0, 0, 255), siyah (0, 0, 0), sarı (255, 255, 0). Ham: 64 × 3 = 192 bayt. RLE: 33 koşu × (1 sayı + 3 renk) = 132 bayt.
| Sayı | R G B | Sayı | R G B | Sayı | R G B |
|---|---|---|---|---|---|
| 2 | 0 0 255 | 4 | 0 0 0 | 3 | 0 0 255 |
| 1 | 0 0 0 | 4 | 255 255 0 | 1 | 0 0 0 |
| 1 | 0 0 255 | 1 | 0 0 0 | 1 | 255 255 0 |
| 1 | 0 0 0 | 2 | 255 255 0 | 1 | 0 0 0 |
| 1 | 255 255 0 | 2 | 0 0 0 | 6 | 255 255 0 |
| 2 | 0 0 0 | 1 | 255 255 0 | 1 | 0 0 0 |
| 2 | 255 255 0 | 1 | 0 0 0 | 1 | 255 255 0 |
| 2 | 0 0 0 | 2 | 255 255 0 | 2 | 0 0 0 |
| 2 | 255 255 0 | 1 | 0 0 0 | 1 | 0 0 255 |
| 1 | 0 0 0 | 4 | 255 255 0 | 1 | 0 0 0 |
| 3 | 0 0 255 | 4 | 0 0 0 | 2 | 0 0 255 |
Tablo satır satır (soldan sağa, sonra alt satır) okunur; satır sonunda aynı renk devam ediyorsa koşu bir sonraki satıra taşar (ör. 3 mavi).
LZW
Lempel-Ziv-Welch (LZW) Sıkıştırma
- Sözlük tabanlı kayıpsız sıkıştırma: Lempel ve Ziv'in LZ78 yöntemini (1978) Terry Welch geliştirmiştir (1984).
- Tekrar eden dizgileri tanımlar ve onları sözlükteki kısa kodlarla (indeks) temsil eder.
- Sözlük dinamikdir: sıkıştırırken büyür; gönderilmez, açıcı aynı sözlüğü kendisi yeniden kurar.
- GIF, TIFF, PDF ve Unix
compressgibi biçimlerde kullanılır. - Sözlük büyüdükçe kod genişliği (bit) ve bellek artar, performans düşer: pratikte sözlük sınırlanır (GIF: 4096 kod = 12 bit) ve dolunca sıfırlanır.
Metin: ABABCABABCA
Başlangıç sözlüğü: A = 0, B = 1, C = 2
Sıkıştırılmış: 0 1 3 2 3 5 0
11 sembol yerine 7 kod.
LZW · Kodu adım adım çalıştır
lzw("ABABCABABCA")
Sözlük metindeki karakterlerle başlar: A = 0, B = 1, C = 2. w = "".
"A" sözlükte (0) → w = "A"; daha uzun bir eşleşme aranır.
"AB" sözlükte yok → w = "A" kodu 0 çıktıya; "AB" = 3 sözlüğe eklenir; w = "B".
"BA" sözlükte yok → w = "B" kodu 1 çıktıya; "BA" = 4 sözlüğe eklenir; w = "A".
"AB" sözlükte (3) → w = "AB"; daha uzun bir eşleşme aranır.
"ABC" sözlükte yok → w = "AB" kodu 3 çıktıya; "ABC" = 5 sözlüğe eklenir; w = "C".
"CA" sözlükte yok → w = "C" kodu 2 çıktıya; "CA" = 6 sözlüğe eklenir; w = "A".
"AB" sözlükte (3) → w = "AB"; daha uzun bir eşleşme aranır.
"ABA" sözlükte yok → w = "AB" kodu 3 çıktıya; "ABA" = 7 sözlüğe eklenir; w = "A".
"AB" sözlükte (3) → w = "AB"; daha uzun bir eşleşme aranır.
"ABC" sözlükte (5) → w = "ABC"; daha uzun bir eşleşme aranır.
"ABCA" sözlükte yok → w = "ABC" kodu 5 çıktıya; "ABCA" = 8 sözlüğe eklenir; w = "A".
Döngü bitti: kalan w = "A" kodu 0 eklenir. Çıktı: 0 1 3 2 3 5 0.
List<Integer> lzw(String s) {
Map<String, Integer> sozluk =
ilkSozluk(s); // A=0, B=1, C=2
String w = "";
List<Integer> kod = new ArrayList<>();
for (char c : s.toCharArray()) {
String wc = w + c;
if (sozluk.containsKey(wc)) {
w = wc;
} else {
kod.add(sozluk.get(w));
sozluk.put(wc, sozluk.size());
w = "" + c;
}
}
if (!w.isEmpty())
kod.add(sozluk.get(w));
return kod;
}LZW
LZW Açma (Decompression)
- Açıcı yalnızca başlangıç sözlüğünü bilir; sözlüğü sıkıştırıcıyla aynı sırada kendisi kurar.
- Her yeni kod k için: e = sözlük[k] yazılır; sözlüğe w + e[0] eklenir; w = e.
- Açıcı sözlükte bir adım geride kalır. k henüz tanımlı değilse (k = sonraki kod), e = w + w[0] olmak zorundadır (cScSc durumu).
- Çıktı: A·B·AB·C·AB·ABC·A = ABABCABABCA ✓
| Kod | Çıktı | Eklenen |
|---|---|---|
| 0 | A | — |
| 1 | B | 3 = AB |
| 3 | AB | 4 = BA |
| 2 | C | 5 = ABC |
| 3 | AB | 6 = CA |
| 5 | ABC | 7 = ABA |
| 0 | A | 8 = ABCA |
LZW
LZW Örneği: DNA Dizisi
Metin: ATGATCATGAG (11 sembol)
Başlangıç: A = 0, T = 1, G = 2, C = 3
Çıktı: 0 1 2 4 3 4 6 2 (8 kod)
- Her satır: sözlüğe eklenen yeni dizgi ve o anda çıktıya yazılan kod.
- "AT" ikinci kez görüldüğünde tek kodla (4) yazılır; "GA" (6) da öyle.
- Son w = "G" döngüden sonra çıktıya eklenir (2).
| Kod | Sözlüğe eklenen | Çıktı |
|---|---|---|
| 4 | AT | 0 |
| 5 | TG | 1 |
| 6 | GA | 2 |
| 7 | ATC | 4 |
| 8 | CA | 3 |
| 9 | ATG | 4 |
| 10 | GAG | 6 |
| — | (son w = G) | 2 |
LZW
ASCII Sözlüklü LZW ve Özel Durum
- Pratikte başlangıç sözlüğü 256 ASCII karakteridir (a = 97, b = 98, c = 99); yeni dizgiler 256'dan itibaren numaralanır.
- Metin:
aabaacababacb(13 karakter) → 9 kod: 97 97 98 256 99 257 261 99 98. - "aba" = 261, "ab" (257) yazılırken eklenir ve hemen bir sonraki kodda kullanılır.
- Açıcı 261'i okuduğunda sözlüğünde henüz 260'a kadar kod vardır → özel durum: e = w + w[0] = "ab" + "a" = "aba".
| Çıktı | Sözlüğe eklenen |
|---|---|
| 97 | 256 = aa |
| 97 | 257 = ab |
| 98 | 258 = ba |
| 256 | 259 = aac |
| 99 | 260 = ca |
| 257 | 261 = aba |
| 261 | 262 = abac |
| 99 | 263 = cb |
| 98 | (son w = b) |
LZW
LZ78: LZW'nin Atası
- LZ78 sözlüğü boş başlar; çıktı (indeks, karakter) çiftleridir: sözlükteki en uzun eşleşmenin indeksi (yoksa 0) + onu izleyen karakter.
- Metin:
aabaacabcabcb→ (0,a) (1,b) (1,a) (0,c) (2,c) (5,b). - Welch'in değişikliği (LZW): sözlüğü tüm tek karakterlerle başlatmak → çıktı yalnızca kodlardan oluşur, karakter yazmaya gerek kalmaz.
- LZ77 (kayan pencere) ailesi ise gzip/ZIP (DEFLATE) ve PNG'nin temelidir.
| Çıktı | Sözlük |
|---|---|
| (0, a) | 1 = a |
| (1, b) | 2 = ab |
| (1, a) | 3 = aa |
| (0, c) | 4 = c |
| (2, c) | 5 = abc |
| (5, b) | 6 = abcb |
LZW
RLE ve LZW Karşılaştırması
| RLE | LZW | |
|---|---|---|
| Yakaladığı fazlalık | aynı sembolün ardışık tekrarı | tekrar eden dizgiler (ardışık olmasa da) |
| Ek yapı | yok | sözlük (hash tablosu / trie) |
| Zaman | O(n) | O(n) (sözlük erişimi O(1) ortalama) |
| En kötü durum | tekrar yoksa çıktı 2 katına çıkar | kısa/rastgele veride kazanç az, kod genişliği büyür |
| Kullanım | faks, BMP/PCX, basit grafikler | GIF, TIFF, PDF, Unix compress |
Özet
Özet
| Konu | Hatırlanacaklar |
|---|---|
| Kaba kuvvet | her kaymayı soldan sağa dene; ön işleme yok — en kötü O(n · m) |
| KMP | lps[q] = en uzun uygun önek = sonek; uyuşmazlıkta q = lps[q − 1], i geri gitmez — O(n + m) |
| Boyer-Moore | sağdan sola; s += max(1, j − son[c]) ve iyi sonek; en iyi O(n/m), pratikte en hızlı |
| Rabin-Karp | polinom kayan hash, O(1) güncelleme; hash eşitse doğrula; beklenen O(n + m) |
| RLE | koşu → (sayı, sembol); O(n); tekrar yoksa büyütür; bayrak baytı ile iyileştirilir |
| LZW | w + c sözlükte varsa uzat, yoksa kod(w) yaz ve w + c'yi ekle; açıcı sözlüğü kendisi kurar (özel durum: w + w[0]) |
Özet
Etkileşimli Simülatörler
Özet
Sık Yapılan Hatalar
i < n − m yazmak son kaymayı (s = n − m) atlar; doğrusu i <= n − m.max(1, …) unutulursa örüntü geri gider ya da sonsuz döngü olur.% negatif sonuç verebilir: if (th < 0) th += Q.Özet
Örnek Problemler
- "aabaaab" örüntüsünün LPS tablosunu bulun.
- T = "aaaaaaaa" (n = 8), P = "aab" için kaba kuvvet kaç karşılaştırma yapar?
- Boyer-Moore (yalnız kötü karakter): T = "GCAATGCCTATGTGACC", P = "TATGTG". İlk kaydırma kaç olur?
- Taban 10, Q = 13, karakterler rakam: T = "31415", m = 3. h("314") verildiğinde h("141")'i kaydırma formülüyle bulun.
- "WWWWBBBWWWWWWB" dizgisini RLE ile kodlayın; oranı hesaplayın.
- A = 0, B = 1 sözlüğüyle "ABABABA" dizgisini LZW ile sıkıştırın.
Cevapları göster
- [0, 1, 0, 1, 2, 2, 3]
- 6 hiza × 3 = 18 (her hizada "aa" eşleşir, 'b' uyuşmaz); eşleşme yok.
- s = 0: p[5] = t[5] = 'G' ve p[4] = t[4] = 'T' eşleşir; p[3] = 'G' ≠ t[3] = 'A' → son['A'] = 1 → 3 − 1 = 2.
- h("314") = 314 mod 13 = 2; h' = (10 · (2 − 3 · 9) + 1) mod 13 = (−249) mod 13 = 11 = 141 mod 13 ✓ (10² mod 13 = 9).
- 4W3B6W1B → 8 karakter / 14 ≈ 0,57.
- AB = 2, BA = 3, ABA = 4 eklenir; çıktı 0 1 2 4 (açarken 4 özel durumdur: "AB" + "A").
Özet
Alıştırmalar
- KMP aramasını, tüm geçişleri bir listede döndürecek şekilde değiştirin.
- "abcbcabbabcabc" deseninin son görülme tablosunu ve "abccbaabccbaabcbcabbabcabc" metnindeki ilk kaydırmayı bulun.
- Rabin-Karp ile aynı uzunlukta k örüntüyü tek geçişte arayan bir yöntem tasarlayın.
rleAc(String s): çok basamaklı sayıları da doğru çözen RLE açıcıyı yazın.- 0 1 3 2 3 5 0 kodlarını (A = 0, B = 1, C = 2) LZW ile açın.
- Kaba kuvvetin en kötü durumunu üreten bir T, P çifti (n = 12, m = 4) verin; karşılaştırma sayısını yazın.
Cevaplar / ipuçları
- q == m olduğunda
liste.add(i − m + 1); q = lps[q − 1];ile devam edin. - a: 11, b: 12, c: 13. s = 0: p[13] = 'c' ≠ t[13] = 'b' → 13 − 12 = 1.
- Örüntü hash'lerini bir
HashSet'e koyun; her pencere hash'ini kümede arayın, bulunursa doğrulayın: beklenen O(n + k · m). - Rakamları
Character.isDigitile biriktiripsayi = sayi · 10 + rakam; harf gelince sayi kadar yazın. - A, B, AB, C, AB, ABC, A → ABABCABABCA.
- T = "aaaaaaaaaaaa", P = "aaab": 9 hiza × 4 = 36 karşılaştırma.
Özet
Kaynaklar
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022 (Bölüm 32: String Matching).
- Sedgewick ve Wayne, Algorithms, 4. baskı, 2011 (Bölüm 5.3 Substring Search, 5.5 Data Compression).
- Goodrich, Tamassia ve Goldwasser, Data Structures and Algorithms in Java, 6. baskı, 2014 (Bölüm 13).
- D. E. Knuth, J. H. Morris, V. R. Pratt, "Fast Pattern Matching in Strings", SIAM J. Computing 6(2), 1977.
- R. S. Boyer, J S. Moore, "A Fast String Searching Algorithm", Communications of the ACM 20(10), 1977.
- R. M. Karp, M. O. Rabin, "Efficient Randomized Pattern-Matching Algorithms", IBM J. Res. Dev. 31(2), 1987.
- T. A. Welch, "A Technique for High-Performance Data Compression", IEEE Computer 17(6), 1984.