Bölüm 5.3: Trie Veri Yapısı
Giriş
Öğrenme Çıktıları
Trie'nin kök, düğüm, kenar ve kelime sonu işaretinden oluşan yapısını açıklamak; bir kelime kümesinin trie'sini çizmek.
Ekleme, arama, önek sorgusu, otomatik tamamlama ve silme işlemlerini Java ile yazmak ve adım adım izlemek.
İşlemlerin O(L) zamanını ve O(N · M) belleğini gerekçelendirmek; trie'yi hash tablosu ve BST ile karşılaştırmak.
Sıkıştırılmış trie, sonek trie'si ve Aho–Corasick'in hangi sorunu çözdüğünü açıklamak.
Trie Kavramı
Trie (Önek Ağacı, Prefix Tree)
- Dizgileri (string) hızlı arama yapılabilecek biçimde saklayan ağaç benzeri bir veri yapısıdır.
- Kök düğüm boştur; kökten aşağı her adım bir karakteri temsil eder.
- Kelimeler, kökten başlayan yollarla saklanır; ortak önekler (prefix) tek kez saklanır.
- Kullanım alanları: otomatik tamamlama, yazım denetimi, IP yönlendirme.
Trie Kavramı
Neden Trie?
Bir sözlükte "an" ile başlayan tüm kelimeleri bulmak istiyoruz. Farklı yapılar bu soruya nasıl yanıt verir?
Trie Kavramı
Temel Bileşenler
kelimeSonu (end of word): bu düğümde bir kelimenin bittiğini belirtir.Renkli düğüm "an" önekini temsil eder; "an" bir kelime değildir (işaretsiz), ama "and" ve "ant"in önekidir.
Trie Kavramı
Örnek: Yedi Kelimelik Trie
| Kelimeler |
|---|
| ab · aba · abc |
| ad |
| ba · bad · bag |
7 kelime, toplam 18 karakter → yalnızca 9 düğüm (+ kök).
kelimeSonu işareti şarttır.Trie Kavramı
Örnek: Değer Taşıyan Trie
| Anahtar | Değer | Anahtar | Değer |
|---|---|---|---|
| A | 15 | tea | 3 |
| i | 11 | ted | 4 |
| in | 5 | ten | 12 |
| inn | 9 | to | 7 |
kelimeSonu yerine düğümde bir değer saklanır. "t" ve "te" değer taşımaz; yalnızca önektir. Büyük harf "A", 26 küçük harflik dizi yerine daha geniş bir alfabe (ör. HashMap) gerektirir.Uygulama
Trie Düğüm Sınıfı
public class TrieDugumu { TrieDugumu[] cocuk; // 'a'..'z' boolean kelimeSonu; // kelime biter mi? public TrieDugumu() { kelimeSonu = false; cocuk = new TrieDugumu[26]; } }
- Her düğüm 26 çocuk referansı tutar; başlangıçta hepsi
null. - Karakter düğümde saklanmaz: hangi karakter olduğu, düğümün ebeveyninin hangi indisinde durduğundan anlaşılır.
kelimeSonu: kökten buraya gelen yol bir kelime mi?
and, ant, dad, do eklendikten sonra kökün cocuk dizisi: 26 hücreden yalnızca 2'si dolu (⁄ = null).
Uygulama
Karakterden İndise: ch − 'a'
| ch | Unicode | ch − 'a' |
|---|---|---|
| 'a' | 97 | 0 |
| 'd' | 100 | 3 |
| 'n' | 110 | 13 |
| 'o' | 111 | 14 |
| 't' | 116 | 19 |
| 'z' | 122 | 25 |
- Java'da
charbir sayıdır;ch - 'a'küçük harfi 0..25 aralığına eşler. - Çocuğa erişim dizi indisiyle O(1)'dir:
dugum.cocuk[ch - 'a']. - "and" için sırasıyla
cocuk[0],cocuk[13],cocuk[3]kullanılır.
'A' - 'a' = −32, 'ç' - 'a' = 134 → ArrayIndexOutOfBoundsException. Büyük harf ve Türkçe karakter için girdi dönüştürülmeli ya da HashMap kullanılmalı.Ekleme
Ekleme (Insert)
public void ekle(String kelime) { TrieDugumu dugum = kok; for (char ch : kelime.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) dugum.cocuk[i] = new TrieDugumu(); dugum = dugum.cocuk[i]; } dugum.kelimeSonu = true; }
- Kökten başlanır.
- Kelimenin her karakteri için ilgili çocuğa bakılır:
cocuk[ch − 'a']. - Çocuk
nullise yeni düğüm oluşturulur; varsa (ortak önek) mevcut düğüm kullanılır. - O çocuğa ilerlenir.
- Karakterler bitince son düğümde
kelimeSonu = trueyapılır.
Ekleme · Adım adım
Ekleme Adım Adım: and, ant, dad, do
Başlangıçta trie yalnızca kökten oluşur; kökün 26 çocuğunun hepsi null, kelimeSonu = false.
"and" ekleniyor, karakter 'a': cocuk[0] null → yeni düğüm oluşturulur ve ona ilerlenir.
"and" ekleniyor, karakter 'n': cocuk[13] null → yeni düğüm oluşturulur ve ona ilerlenir.
"and" ekleniyor, karakter 'd': cocuk[3] null → yeni düğüm oluşturulur ve ona ilerlenir.
Karakterler bitti: son düğümde kelimeSonu = true (çift çember). "and" eklendi. Kökten itibaren cocuk[0] → cocuk[13] → cocuk[3] yolu kuruldu.
"ant" ekleniyor, karakter 'a': cocuk[0] dolu → mevcut "a" düğümüne ilerlenir (ortak önek).
"ant" ekleniyor, karakter 'n': cocuk[13] dolu → mevcut "an" düğümüne ilerlenir (ortak önek).
"ant" ekleniyor, karakter 't': cocuk[19] null → yeni düğüm oluşturulur ve ona ilerlenir.
Karakterler bitti: son düğümde kelimeSonu = true (çift çember). "ant" eklendi. Yalnızca 1 yeni düğüm gerekti: "an" öneki paylaşıldı.
"dad" ekleniyor, karakter 'd': cocuk[3] null → yeni düğüm oluşturulur ve ona ilerlenir.
"dad" ekleniyor, karakter 'a': cocuk[0] null → yeni düğüm oluşturulur ve ona ilerlenir.
"dad" ekleniyor, karakter 'd': cocuk[3] null → yeni düğüm oluşturulur ve ona ilerlenir.
Karakterler bitti: son düğümde kelimeSonu = true (çift çember). "dad" eklendi.
"do" ekleniyor, karakter 'd': cocuk[3] dolu → mevcut "d" düğümüne ilerlenir (ortak önek).
"do" ekleniyor, karakter 'o': cocuk[14] null → yeni düğüm oluşturulur ve ona ilerlenir.
Karakterler bitti: son düğümde kelimeSonu = true (çift çember). "do" eklendi. Son durum: 4 kelime, 11 karakter, kök hariç 8 düğüm.
Ekleme · Kodu adım adım çalıştır
ekle("ant"): Kodu Adım Adım İzle
Trie'de yalnızca "and" var. dugum kökten başlar.
Karakter 'a' → i = 'a' − 'a' = 0.
dugum.cocuk[0] null değil → yeni düğüm gerekmez.
dugum artık "a" düğümü.
Karakter 'n' → i = 'n' − 'a' = 13.
dugum.cocuk[13] null değil → yeni düğüm gerekmez.
dugum artık "an" düğümü.
Karakter 't' → i = 't' − 'a' = 19.
dugum.cocuk[19] == null → koşul doğru.
Yeni bir TrieDugumu oluşturulup cocuk[19]'e bağlanır (kelimeSonu = false).
dugum artık "ant" düğümü.
Döngü bitti: dugum.kelimeSonu = true. "ant" eklendi; yalnızca 1 düğüm oluşturuldu.
public void ekle(String kelime) { TrieDugumu dugum = kok; for (char ch : kelime.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) dugum.cocuk[i] = new TrieDugumu(); dugum = dugum.cocuk[i]; } dugum.kelimeSonu = true; }
Arama
Arama (Search)
public boolean ara(String kelime) { TrieDugumu dugum = kok; for (char ch : kelime.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) return false; dugum = dugum.cocuk[i]; } return dugum.kelimeSonu; }
- Kökten başlanır.
- Her karakter için ilgili dala (çocuğa) gidilir.
- Dal yoksa (
null) kelime trie'de yoktur.
- Dal yoksa (
- Kelimenin sonuna gelince
kelimeSonudenetlenir:true→ kelime bulundu;false→ kelime yok; yalnızca var olan bir kelimenin önekidir.
Arama · Kodu adım adım çalıştır
ara("dad"): Kodu Adım Adım İzle
Trie: and, ant, dad, do. dugum kökten başlar.
Karakter 'd' → i = 3.
cocuk[3] null değil → dal var, devam.
dugum = "d".
Karakter 'a' → i = 0.
cocuk[0] null değil → dal var, devam.
dugum = "da".
Karakter 'd' → i = 3.
cocuk[3] null değil → dal var, devam.
dugum = "dad".
Kelime bitti; dugum.kelimeSonu = true → true döner (kelime bulundu).
public boolean ara(String kelime) { TrieDugumu dugum = kok; for (char ch : kelime.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) return false; dugum = dugum.cocuk[i]; } return dugum.kelimeSonu; }
Arama
Aramanın Üç Olası Sonucu
kelimeSonu = false: "da" yalnızca önek.cocuk[6] ('g') null: dal yok, erken çıkış.Kesikli kırmızı çember: aranan ama trie'de bulunmayan düğüm.
Arama
Önek Sorgusu: onekVarMi (startsWith)
public boolean onekVarMi(String onek) { TrieDugumu dugum = kok; for (char ch : onek.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) return false; dugum = dugum.cocuk[i]; } return true; // ara: dugum.kelimeSonu }
| Sorgu | ara | onekVarMi |
|---|---|---|
| "dad" | true | true |
| "da" | false | true |
| "an" | false | true |
| "dog" | false | false |
Arama · Kodu adım adım çalıştır
onekVarMi("da") ve onekVarMi("dog")
Trie'de and, ant, dad, do var (halkalı düğüm = kelime sonu). Önce onekVarMi("da"): dugum köke konur.
ch = 'd', i = 3: cocuk[3] dolu → dugum o çocuğa iner ("d").
ch = 'a', i = 0: cocuk[0] dolu → dugum o çocuğa iner ("da").
Önek bitti ve yol kesilmedi → true. "da" düğümü işaretsiz olduğu için ara("da") false verirdi; önek sorgusu kelime sonuna bakmaz.
İkinci çağrı onekVarMi("dog"): dugum yeniden köke konur.
ch = 'd', i = 3: cocuk[3] dolu → dugum o çocuğa iner ("d").
ch = 'o', i = 14: cocuk[14] dolu → dugum o çocuğa iner ("do").
ch = 'g', i = 6: "do" düğümünün cocuk[6]'sı null → false. "do" bir kelime olsa da "dog" ile başlayan hiçbir kelime yok.
public boolean onekVarMi(String onek) { TrieDugumu dugum = kok; for (char ch : onek.toCharArray()) { int i = ch - 'a'; if (dugum.cocuk[i] == null) return false; dugum = dugum.cocuk[i]; } return true; // ara: dugum.kelimeSonu }
Arama
Önek Sayacı: onekSayisi O(P)
void ekle(String kelime) { TrieDugumu d = kok; d.gecen++; for (char ch : kelime.toCharArray()) { int i = ch - 'a'; if (d.cocuk[i] == null) d.cocuk[i] = new TrieDugumu(); d = d.cocuk[i]; d.gecen++; // bir kelime daha geçti } d.kelimeSonu = true; } int onekSayisi(String onek) { TrieDugumu d = kok; for (char ch : onek.toCharArray()) { d = d.cocuk[ch - 'a']; if (d == null) return 0; } return d.gecen; }
| Sorgu | sonuç |
|---|---|
| onekSayisi("an") | 2 |
| onekSayisi("da") | 1 |
| onekSayisi("x") | 0 |
TrieDugumu'na int gecen alanı eklenir. Sayaçsız aynı soru alt ağacı gezmeyi (O(P + S · M)) gerektirirdi; bedeli düğüm başına bir int. Silmede yol boyunca sayaçlar azaltılır.Arama
Joker Karakterli Arama: "d.d"
boolean joker(TrieDugumu d, String s, int j) { if (d == null) return false; if (j == s.length()) return d.kelimeSonu; char ch = s.charAt(j); if (ch != '.') return joker(d.cocuk[ch - 'a'], s, j + 1); for (int i = 0; i < 26; i++) if (joker(d.cocuk[i], s, j + 1)) return true; return false; }
| Desen | sonuç | eşleşen |
|---|---|---|
| "d.d" | true | dad |
| ".n." | true | and |
| ".o" | true | do |
| "b.." | false | b dalı yok |
| "...." | false | 4 harfli yok |
- Trie: and, ant, dad, do. Çağrı
joker(kok, desen, 0). - Normal harf aramayı tek yola indirir; '.' her dolu dalı dener (geri izleme). İlk eşleşmede
trueyukarı taşınır. - En kötü durum k joker için 26k dal, ama boş dallar hemen döner: gerçek maliyet trie'de var olan yollarla sınırlıdır. Hash tablosu bu sorguyu ancak tüm kelimeleri tarayarak yanıtlar.
Arama
Yazım Denetimi: Trie Üzerinde Düzenleme Uzaklığı
- Soru: sözlükte "dod" kelimesine düzenleme uzaklığı (ekle / sil / değiştir) en çok 1 olan kelimeler hangileri?
- Her kelimeyle ayrı ayrı uzaklık hesaplamak yerine trie derinlik öncelikli gezilir; her düğüm, ebeveyninin DP satırından tek bir satır üretir.
- Ortak önekin satırı bir kez hesaplanır: "d" satırı hem dad hem do için kullanılır.
- Budama: satırın en küçüğü k'yı aşarsa alt ağaçtaki hiçbir kelime k içinde olamaz; "an" satırının en küçüğü 2 > 1, and ve ant'a hiç inilmez.
| düğüm | "" d o d | son | karar |
|---|---|---|---|
| a | 1 1 2 3 | — | devam |
| an | 2 2 2 3 | — | buda (min 2) |
| d | 1 0 1 2 | — | devam |
| da | 2 1 1 2 | — | devam |
| dad | 3 2 2 1 | 1 | öner |
| do | 2 1 0 1 | 1 | öner |
Satırın son hücresi, düğümün kelimesi ile "dod" arasındaki uzaklıktır.
Arama · Kodu adım adım çalıştır
Otomatik Tamamlama: tamamla("te")
Önce dugumBul("te") kökten t → e inerek "te" düğümünü bulur (önek sorgusu gibi).
"te" düğümünden başlayarak alt ağaç derinlik öncelikli gezilir.
"te" bir kelime değil (kelimeSonu = false) → listeye eklenmez.
i = 0: cocuk[0] ('a') dolu → yol = "tea".
Özyinelemeli çağrı: topla("tea").
"tea" işaretli → sonuc'a eklenir. Çocuğu yok; döngü 26 indisi boş geçer.
Çağrı döndü; son harf silinir (yol = "te") → geri izleme (backtracking).
i = 3: cocuk[3] ('d') dolu → yol = "ted".
Özyinelemeli çağrı: topla("ted").
"ted" işaretli → sonuc'a eklenir. Çocuğu yok; döngü 26 indisi boş geçer.
Çağrı döndü; son harf silinir (yol = "te") → geri izleme (backtracking).
i = 13: cocuk[13] ('n') dolu → yol = "ten".
Özyinelemeli çağrı: topla("ten").
"ten" işaretli → sonuc'a eklenir. Çocuğu yok; döngü 26 indisi boş geçer.
Çağrı döndü; son harf silinir (yol = "te") → geri izleme (backtracking).
Döngü bitti; tamamla("te") → [tea, ted, ten]. İndis sırası (a < d < n) sayesinde sonuçlar alfabetik sıradadır.
List<String> tamamla(String onek) {
List<String> sonuc = new ArrayList<>();
TrieDugumu d = dugumBul(onek);
if (d != null)
topla(d, new StringBuilder(onek), sonuc);
return sonuc;
}
void topla(TrieDugumu d, StringBuilder yol,
List<String> sonuc) {
if (d.kelimeSonu) sonuc.add(yol.toString());
for (int i = 0; i < 26; i++)
if (d.cocuk[i] != null) {
yol.append((char) ('a' + i));
topla(d.cocuk[i], yol, sonuc);
yol.setLength(yol.length() - 1);
}
}Arama · Kodu adım adım çalıştır
enUzunOrtakOnek(): Kod İzleme
Trie: kar, kart, karton. Kökten tek çocuklu ve işaretsiz düğümler boyunca inilir.
Kök işaretsiz; çocuk sayılır: yalnız 'k' dolu → sayi = 1, tek = 10.
'k' eklenir, d = "k". Bu düğümde de tek çocuk ('a', tek = 0) var.
'a' eklenir, d = "ka"; tek çocuk 'r' (tek = 17).
'r' eklenir, d = "kar". "kar" bir kelime → döngü biter: tek çocuğu olsa da "kar" kelimesi daha uzun öneki paylaşmaz.
"kar" döner. Trie'de yalnız kart ve karton olsaydı inişe "kart"a kadar devam edilir, orada kelimeSonu durdururdu: "kart".
String enUzunOrtakOnek() {
StringBuilder sb = new StringBuilder();
TrieDugumu d = kok;
while (!d.kelimeSonu) {
int tek = -1, sayi = 0;
for (int i = 0; i < 26; i++)
if (d.cocuk[i] != null) { sayi++; tek = i; }
if (sayi != 1) break; // dal ya da yaprak
sb.append((char) ('a' + tek));
d = d.cocuk[tek];
}
return sb.toString();
}Arama
Deneyin: Trie Üzerinde İşlemler
Yalnızca a–z küçük harfler (en fazla 10 harf, 14 kelime). Çift çember: kelime sonu · turuncu: son ziyaret edilen düğüm · kesikli kırmızı: bulunamayan dal. Enter = ekle.
Silme
Silme (Delete): Üç Durum
kelimeSonu = false yapılır, düğüm silinmez.Cevabı göster
Yalnızca son 'p' düğümünün kelimeSonu işareti kaldırılır. Düğüm silinemez, çünkü "apple" onun altından devam eder (durum 1).
Silme · Adım adım
Silme Adım Adım: app, apple, apt
Başlangıç: app, apple, apt (kök hariç 6 düğüm). Aşağıdaki iki senaryo bu trie'den başlar.
sil("app"): a → p → p yolu izlenir; "app" düğümü işaretli, yani kelime var.
kelimeSonu = false. Düğümün çocuğu ('l') olduğu için silinmez → durum 1. "apple" hâlâ bulunur.
sil("apple") (yeniden ilk trie'den): yol a-p-p-l-e izlenir; "apple" bulundu.
'e' düğümünün işareti kalkar; çocuğu yok → ebeveyn onu siler (cocuk[4] = null).
'l' düğümü artık çocuksuz ve işaretsiz → o da silinir.
"app" düğümü işaretli (başka bir kelime) → silme burada durur. Sonuç: app, apt; kök hariç 4 düğüm (durum 2).
Silme
Silme: Özyinelemeli Kod
// true: ebeveyn d'yi silebilir boolean sil(TrieDugumu d, String k, int j) { if (j == k.length()) { // kelime bitti if (!d.kelimeSonu) return false; d.kelimeSonu = false; return bosMu(d); // çocuksuz mu? } int i = k.charAt(j) - 'a'; TrieDugumu c = d.cocuk[i]; if (c == null) return false; // kelime yok if (sil(c, k, j + 1)) { d.cocuk[i] = null; // çocuğu sil return !d.kelimeSonu && bosMu(d); } return false; }
- Çağrı:
sil(kok, kelime, 0); kökün dönüş değeri yok sayılır. - Önce kelimenin sonuna inilir, silme kararları dönüşte (aşağıdan yukarı) verilir.
- Düğüm, ancak çocuğu yoksa ve işaretsizse silinir.
bosMu(d): 26 çocuğun hepsi null mı? (O(M)).- Zaman: O(L · M) dizi ile; çocuk sayacı tutulursa O(L).
Silme · Kodu adım adım çalıştır
sil(kok, "dad", 0)
sil(kök, "dad", 0): kelimenin ilk karakteri 'd' → i = 3, c = kökün 'd' çocuğu. Silme kararları dönüşte verilecek; önce aşağı inilir.
sil(d, "dad", 1): 'a' → i = 0, c = "da" düğümü. Özyinelemeli çağrı bir düzey daha iner.
sil(da, "dad", 2): 'd' → i = 3, c = "dad" düğümü.
j = 3 = k.length(): kelime bitti. "dad" işaretli olduğu için kelimeSonu = false yapılır.
bosMu(dad) = true: düğümün hiç çocuğu yok → true döner; ebeveyn bu düğümü silebilir.
sil(da) true döndü → d.cocuk[3] = null: "dad" düğümü koparıldı.
"da" işaretsiz ve artık çocuksuz → true: bu düğüm de silinebilir.
sil(d) true döndü → d.cocuk[0] = null: "da" düğümü koparıldı.
"d" işaretsiz ama "do" için 'o' çocuğu var → bosMu false → false. Silme burada durur.
Kökte sil(d) false döndü → kök bir şey silmez, false döner (dönüş değeri yok sayılır). Kalan kelimeler: and, ant, do.
// true: ebeveyn d'yi silebilir boolean sil(TrieDugumu d, String k, int j) { if (j == k.length()) { // bitti if (!d.kelimeSonu) return false; d.kelimeSonu = false; return bosMu(d); // çocuksuz? } int i = k.charAt(j) - 'a'; TrieDugumu c = d.cocuk[i]; if (c == null) return false; if (sil(c, k, j + 1)) { d.cocuk[i] = null; // çocuğu sil return !d.kelimeSonu && bosMu(d); } return false; }
Analiz
Karmaşıklık
| İşlem | Zaman | Açıklama |
|---|---|---|
| ekle(kelime) | O(L) | Her karakter için bir adım; en fazla L yeni düğüm |
| ara(kelime) | O(L) | Dal yoksa daha erken biter |
| onekVarMi(onek) | O(P) | P: önek uzunluğu |
| tamamla(onek) | O(P + S · M) | S: önek düğümünün alt ağacındaki düğüm sayısı |
| sil(kelime) | O(L · M) | bosMu 26 hücreyi tarar; çocuk sayacıyla O(L) |
Analiz
Bellek: Sayısal Örnek
Trie: and, ant, dad, do
| Toplam karakter | 3 + 3 + 3 + 2 = 11 |
| Düğüm sayısı N | 1 (kök) + 8 = 9 |
| Çocuk referansı | 9 × 26 = 234 |
| Dolu referans | 8 (kenar sayısı) |
| Boş (null) referans | 226 (≈ %97) |
Analiz
Avantajlar ve Dezavantajlar
Avantajlar
- Hızlı önek (prefix) tabanlı arama: otomatik tamamlama.
- Kelime arama O(L) zaman (L: kelime uzunluğu).
- Çakışan önekler tek kez saklanır: bellek paylaşımı.
- Gezinme kelimeleri sözlük sırasında verir; hash çakışması yoktur.
Dezavantajlar
- Yüksek bellek tüketimi, özellikle büyük alfabelerde.
- Alan karmaşıklığı O(N · M) (N: düğüm sayısı, M: alfabe boyutu).
- Ortak öneki az olan kümelerde (ör. rastgele anahtarlar) kazanç yoktur.
- Çok sayıda küçük nesne: önbellek (cache) dostu değildir.
Analiz
Çocukları Saklamanın Yolları
| Gösterim | Çocuğa erişim | Düğüm başına bellek | Not |
|---|---|---|---|
TrieDugumu[26] dizi | O(1) | M referans | Basit, hızlı; seyrek düğümlerde israf |
HashMap<Character, TrieDugumu> | O(1) ort. | çocuk sayısı kadar | Her alfabe (Türkçe, Unicode) çalışır; sıra korunmaz |
TreeMap / sıralı liste | O(log M) | çocuk sayısı kadar | Alfabetik gezinme korunur |
| Üçlü arama trie'si (TST) | O(log M) ort. | 3 referans | Her düğümde küçük / eşit / büyük bağlantısı |
dugum.cocuk[i] yerine dugum.cocuk.get(ch) gibi bir çağrı gelir. Seçim: alfabe boyutu, bellek ve sıralı çıktı ihtiyacına göre yapılır.Analiz
Trie mi, Hash Tablosu mu, BST mi?
| İşlem (n kelime, uzunluk L) | Trie | Hash tablosu | Dengeli BST |
|---|---|---|---|
| Tam kelime arama | O(L) | O(L) ortalama | O(L · log n) |
| Önek sorgusu | O(P) | desteklenmez | O(P · log n) |
| Önekle başlayan k kelime | O(P + S · M) | tüm tabloyu tara | O(P · log n + k · L) |
| Sıralı listeleme | DFS, O(N · M) | sıralama gerekir | kök ortada, O(n) |
| En kötü durum | O(L) | O(n · L) (çakışma) | O(L · log n) |
| Bellek | yüksek | düşük | düşük |
Hash tablosu da kelimenin hash değerini hesaplamak için L karakterin tümünü okur; bu yüzden tam aramada ikisi aynı mertebededir. Trie'nin farkı önek ve sıra sorgularındadır. S: önek düğümünün alt ağacındaki düğüm sayısı.
Analiz
Trie ile Sözlük Sırası: Dizgileri Sıralamak
- Girdi sırası: dog, ant, do, and, dad. Hepsi trie'ye eklenir.
- Kökten önce-kök (preorder) gezilir; çocuklar 'a' → 'z' sırasıyla ziyaret edilir, işaretli düğümde kelime yazılır.
- Çıktı: and, ant, dad, do, dog. Düğüm, çocuklarından önce yazıldığı için önek, uzantısından önce gelir (do < dog).
- Toplam karakter sayısı N ve sabit alfabe için ekleme O(N), gezinme O(N·M) (M = 26 çocuk denetimi): karşılaştırmalı sıralamanın O(n log n) karşılaştırmasına ve her karşılaştırmanın O(L) maliyetine gerek kalmaz.
- Aynı fikir “burst sort” ve MSD basamak sıralamasının dizgi sürümlerinin temelidir.
Uygulamalar ve Türevler
Kullanım Alanları
Uygulamalar ve Türevler · Kodu adım adım çalıştır
enUzunEslesme(): İkili Trie ile Yönlendirme
İkili trie (alfabe {0, 1}); bazı düğümler bir rota taşır. adres = "0110": d = kök, enIyi = null.
k = 0: kökte rota yok. Bit '0' → d = "0".
k = 1: "0" düğümünde A var → enIyi = A. Bit '1' → d = "01".
k = 2: "01"de B → enIyi = B (daha uzun önek kazanır). d = "011".
k = 3: "011"de C → enIyi = C. Bit '0' dalı yok → d = null, C döner.
Yeni adres "1001": k = 0, kökte rota yok; d = "1".
k = 1: "1"de D → enIyi = D. '0' dalı yok → D döner. Özel rota yoksa genel olan kullanılır.
"1101": iniş 1 → 11 → 110. "11" ara düğümdür (rotası yok), enIyi D olarak kalır.
k = 3: "110"da E → enIyi = E; '1' dalı yok → E. Adım sayısı adres uzunluğuyla sınırlı.
String enUzunEslesme(String adres) {
IkiliDugum d = kok;
String enIyi = null;
for (int k = 0; k < adres.length(); k++) {
if (d.rota != null) enIyi = d.rota; // daha uzun önek
d = d.cocuk[adres.charAt(k) - '0'];
if (d == null) return enIyi;
}
return d.rota != null ? d.rota : enIyi;
}Uygulamalar ve Türevler
İkili Trie ile En Büyük XOR Çifti
- Soru: [3, 10, 5, 25, 2, 8] içinde a ⊕ b'yi en büyük yapan çift? Kaba kuvvet n² çift dener.
- Sayılar, en anlamlı bitten başlayarak 5 bitlik ikili trie'ye eklenir.
- Her x için kökten inilirken her bitte ters bit dalı seçilir (varsa): o bit sonuçta 1 olur. Üst bitteki 1, alttaki bütün bitlerin toplamından değerlidir; bu yüzden açgözlü seçim doğrudur.
- x = 5 = 00101 için istenen 11010: 1 ✓ (25), 1 ✓, 0 ✓, 1 yok → 0, 0 yok → 1: varılan 11001 = 25, 5 ⊕ 25 = 28.
| Yöntem | zaman | n = 10⁵, B = 30 |
|---|---|---|
| Tüm çiftler | O(n²) | ≈ 5·10⁹ işlem |
| İkili trie | O(n·B) | ≈ 6·10⁶ adım |
Uygulamalar ve Türevler
Üçlü Arama Trie'si (Ternary Search Trie)
- Her düğüm bir karakter ve üç bağlantı taşır:
sol(küçük),orta(eşit, sonraki karaktere geç),sag(büyük). - Bir düğümün sol/sağ zinciri, trie'deki “kardeş” çocuklar için küçük bir ikili arama ağacıdır.
- Kelime sonu düğümde işaretlenir (halka). Ekleme sırası şekli değiştirir: önce eklenen kelime “omurga” olur.
| kar, kal, at, ve, kas | düğüm | bağlantı |
|---|---|---|
| 26 öğeli dizili trie | 10 | 10 × 26 = 260 |
| TST | 9 | 9 × 3 = 27 |
Uygulamalar ve Türevler · Kodu adım adım çalıştır
Üçlü Arama Trie'sinde ara("kas") ve ara("al")
TST: kar, kal, at, ve, kas. ara("kas"): d = kök ('k'), i = 0.
ch = 'k' = d.c ve son karakter değil → orta bağlantı, i = 1. d = 'a'.
ch = 'a' eşit → orta, i = 2. d = 'r'.
ch = 's' > 'r' → sağ bağlantı; i değişmez (aynı karakter, kardeş aranıyor). d = 's'.
ch = 's' eşit ve i = 2 son indis → d.son döner: true. 3 karakter için 4 düğüm ziyaret edildi.
ara("al"): ch = 'a' < 'k' → sol, d = 'a' (at kelimesinin dalı).
'a' eşit, son karakter değil → orta, i = 1, d = 't'.
ch = 'l' < 't' → sol; 't'nin sol bağlantısı null → döngü biter, false.
boolean ara(String s) { TstDugum d = kok; int i = 0; while (d != null) { char ch = s.charAt(i); if (ch < d.c) d = d.sol; else if (ch > d.c) d = d.sag; else if (i == s.length() - 1) return d.son; else { d = d.orta; i++; } } return false; }
Uygulamalar ve Türevler
Sıkıştırılmış Trie (Radix / Patricia)
- Tek çocuklu ve işaretsiz düğüm zincirleri tek bir kenarda birleştirilir; kenar artık bir alt dizgi taşır ("an", "ad").
- Her iç düğümün en az 2 çocuğu (ya da kelime sonu işareti) olduğundan düğüm sayısı O(n) olur (n: kelime sayısı).
- Patricia (Morrison, 1968) bu fikrin ikili alfabe sürümüdür; IP yönlendirme tablolarında kullanılır.
Uygulamalar ve Türevler
Sonek Trie'si (Suffix Trie)
T = "abab" metninin tüm sonekleri bir trie'ye eklenir:
abab · bab · ab · b
- P, T'nin bir alt dizgisi ⇔ P, bir sonekin öneki ⇔ P kökten başlayan bir yol.
- Sorgu:
onekVarMi(P)→ O(m) (m = |P|), metnin uzunluğundan bağımsız. Örn. "ba" var, "bb" yok. - Bedel: n uzunluklu metin için O(n²) düğüm olabilir.
- Sonek ağacı (suffix tree) = sıkıştırılmış sonek trie'si: O(n) düğüm, Ukkonen algoritmasıyla O(n) zamanda kurulur.
Uygulamalar ve Türevler · Kodu adım adım çalıştır
sonekTrie("abab")
t = "abab": 4 sonek (abab, bab, ab, b) sırayla eklenecek. Trie yalnızca kökten oluşuyor.
s = 0: sonek "abab" eklenmeye başlar; d = kök. s = 0, j = 0: 'a' dalı yok → yeni düğüm, d oraya iner ("a").
s = 0, j = 1: 'b' dalı yok → yeni düğüm, d oraya iner ("ab").
s = 0, j = 2: 'a' dalı yok → yeni düğüm, d oraya iner ("aba").
s = 0, j = 3: 'b' dalı yok → yeni düğüm, d oraya iner ("abab").
Sonek "abab" bitti → son düğüm işaretlenir (halka).
s = 1: sonek "bab" eklenmeye başlar; d = kök. s = 1, j = 1: 'b' dalı yok → yeni düğüm, d oraya iner ("b").
s = 1, j = 2: 'a' dalı yok → yeni düğüm, d oraya iner ("ba").
s = 1, j = 3: 'b' dalı yok → yeni düğüm, d oraya iner ("bab").
Sonek "bab" bitti → son düğüm işaretlenir (halka).
s = 2: "ab" yolu abab eklenirken zaten oluşmuştu; yeni düğüm açılmadan inilir ve "ab" işaretlenir.
s = 3: "b" yolu da var; yalnızca işaret eklenir. Ortak önekler bir kez saklandı: kök dahil 8 düğüm.
Sonek trie'si hazır. "ba" kökten bir yol → alt dizgi; "bb" yolu yok → alt dizgi değil. Sorgu O(m).
TrieDugumu sonekTrie(String t) {
TrieDugumu kok = new TrieDugumu();
for (int s = 0; s < t.length(); s++) {
TrieDugumu d = kok;
for (int j = s; j < t.length(); j++) {
int i = t.charAt(j) - 'a';
if (d.cocuk[i] == null)
d.cocuk[i] = new TrieDugumu();
d = d.cocuk[i];
}
d.kelimeSonu = true; // sonek bitti
}
return kok;
}Uygulamalar ve Türevler
Aho–Corasick: Birçok Örüntüyü Tek Geçişte Aramak
- Örüntüler (he, she, his, hers) bir trie'ye eklenir; metin bir kez soldan sağa okunur.
- Hata bağlantısı (kesikli): düğümün dizgisinin trie'de bulunan en uzun özel soneki. "she" → "he", "sh" → "h", "hers" → "s".
- Dal yoksa hata bağlantısıyla geri çekilinir; KMP önek tablosunun çok örüntülü karşılığıdır.
- Çıktı kümesi hata zinciri boyunca birleşir: "she" düğümü he'yi de bildirir.
| Metin "ushers" | durum | bulunan |
|---|---|---|
| u, s, h | kök, s, sh | — |
| e (3. indis) | she | she, he |
| r | she → he → her | — |
| s (5. indis) | hers | hers |
Zaman O(n + m + z): n metin, m örüntü uzunlukları toplamı, z eşleşme sayısı (Aho ve Corasick, 1975). Virüs imzası tarama ve yasaklı kelime süzgeçlerinde kullanılır.
Uygulamalar ve Türevler
Trie Ailesi: Hangi Soruya Hangi Yapı?
| Yapı | Sakladığı | Güçlü olduğu soru | Sorgu |
|---|---|---|---|
| Standart trie | kelime kümesi | tam kelime, önek, otomatik tamamlama | O(L) |
| Önek sayaçlı trie | + düğümden geçen kelime sayısı | “bu önekle kaç kelime var?” | O(P) |
| Sıkıştırılmış trie | tek çocuklu zincirler birleşik | uzun ortak önekli anahtarlar, bellek | O(L) |
| Üçlü arama trie'si | düğüm başına 3 bağlantı | büyük alfabe, az bellek | O(L + log n) ort. |
| İkili trie | bit dizileri (IP, tam sayı) | en uzun önek eşleşmesi, en büyük XOR | O(B) |
| Sonek trie'si / dizisi | tüm sonekler (dizi: O(n) bellek) | “P, T'nin alt dizgisi mi?” | O(m) / O(m log n) |
| Aho–Corasick | örüntü trie'si + hata bağlantıları | birçok örüntüyü metinde aynı anda bulmak | O(n + z) |
Özet
Özet
| Konu | Hatırlanacaklar |
|---|---|
| Yapı | boş kök; her kenar bir karakter; kelime = kökten yol; ortak önekler paylaşılır |
| Düğüm | TrieDugumu[26] cocuk + boolean kelimeSonu; indis = ch − 'a' |
| Ekleme | yoksa düğüm oluştur, varsa ilerle; sonda işaretle — O(L) |
| Arama | dal yoksa false; sonda kelimeSonu döner — O(L) |
| Önek | aynı yol, işaret denetimi yok — O(P); tamamla = önek düğümünden DFS |
| Silme | işareti kaldır; çocuksuz ve işaretsiz düğümleri aşağıdan yukarı sil |
| Bellek | O(N · M); seyrek düğümlerde HashMap / TST / sıkıştırılmış trie |
| Türevler | radix/Patricia trie, sonek trie'si → sonek ağacı, sonek dizisi |
Özet
Etkileşimli Simülatörler
Özet
Sık Yapılan Hatalar
kelimeSonu'nu unutmakAramada yalnızca yolun varlığına bakmak her öneki kelime sayar: "da" bulunmuş gibi görünür.'A' − 'a' = −32, 'ş' − 'a' = 254: dizi sınırı aşılır. Girdi küçük harfe çevrilmeli ya da HashMap kullanılmalıdır.Özet
Örnek Problemler
Kelimeler sırayla eklenir: car, cart, care, cat, dog (26 öğeli dizili trie).
- Trie'yi çizin. Kök dahil kaç düğüm vardır?
ara("ca")veonekVarMi("ca")ne döndürür?tamamla("car")sonucunu sırasıyla yazın.sil("car")ve (ilk trie'den)sil("cart")kaç düğüm siler?- Toplam kaç çocuk referansı
null'dır? - Aynı beş kelimenin sıkıştırılmış trie'si kök dahil kaç düğümlüdür?
Cevapları göster
- c-a-r(•)-t(•), car-e(•), ca-t(•); d-o-g(•) → kök hariç 9, kök dahil 10 düğüm (17 karakter).
ara("ca")= false ("ca" işaretsiz);onekVarMi("ca")= true.- [car, care, cart] — DFS önce düğümün kendisini, sonra çocukları indis sırasıyla ('e' < 't') verir.
sil("car"): 0 düğüm (yalnızca işaret kalkar);sil("cart"): 1 düğüm ('t'), "car" işaretli olduğu için durur.- 10 × 26 = 260 referans, 9'u dolu (kenar sayısı) → 251 null.
- 7: kök, "ca", r(•), t(•) (cart), e(•) (care), t(•) (cat), "dog"(•); c–a ve d–o–g zincirleri tek düğüme iner.
Özet
Alıştırmalar
int kelimeSayisi(TrieDugumu d): trie'deki kelime sayısını bulun.int onekSayisi(String onek): bu önekle başlayan kelime sayısını O(P)'de döndürün.String enUzunOrtakOnek(): trie'deki tüm kelimelerin en uzun ortak önekini bulun.- Çocukları
HashMap<Character, TrieDugumu>ile saklayan, Türkçe karakterleri destekleyen trie yazın. - A, to, tea, ted, ten, i, in, inn anahtarlarının trie'sinde kök dahil kaç düğüm vardır?
- Sonek trie'si ile "abab" içinde "bab" ve "bb" alt dizgilerini arayın; kaç karşılaştırma yapılır?
Cevaplar / ipuçları
- DFS:
(d.kelimeSonu ? 1 : 0)+ tüm dolu çocuklarınkelimeSayisitoplamı. O(N · M). - Düğüme
int gecensayacı eklenir;ekleher geçtiği düğümde sayacı artırır. Önek düğümünün sayacı = cevap. - Kökten inilir: düğümün tek çocuğu varken ve
kelimeSonufalse iken ilerlenir; geçilen karakterler cevaptır. cocuk.get(ch)/cocuk.computeIfAbsent(ch, x -> new TrieDugumu()); indis hesabı kalkar.- A, t, to, te, tea, ted, ten, i, in, inn = 10 düğüm + kök = 11.
- "bab": b → a → b yolu var → bulundu (3 adım). "bb": b'den sonra 'b' dalı yok → yok (2 adım).
Özet
Kaynaklar
- Sedgewick ve Wayne, Algorithms, 4. baskı, 2011 (Bölüm 5.2: Tries).
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022.
- E. Fredkin, "Trie Memory", Communications of the ACM 3(9), 1960.
- D. R. Morrison, "PATRICIA — Practical Algorithm To Retrieve Information Coded in Alphanumeric", Journal of the ACM 15(4), 1968.
- U. Manber ve G. Myers, "Suffix Arrays: A New Method for On-Line String Searches", SIAM Journal on Computing 22(5), 1993.
- D. Gusfield, Algorithms on Strings, Trees, and Sequences, Cambridge University Press, 1997.