Trie (עץ תחיליות)
עודכן לאחרונה
Trie (נהגה "טריי"), או עץ תחיליות (prefix tree), שומר קבוצת מחרוזות לפי התווים שלהן: כל קשת מסומנת בתו, ומסלול מהשורש מאיית תחילית. מילים שחולקות תחילית חולקות את אותם צמתים, כך ש-"car", "card" ו-"care" משתמשות כולן באותו מסלול c-a-r. לחצו על הפעלה למעלה כדי לראות מילים שנכנסות תו אחר תו ומתפצלות רק במקום שבו הן שונות.
מכיוון שחיפוש עובר צומת אחד לכל תו, חיפוש מילה באורך m לוקח זמן O(m) בלי קשר למספר המילים שב-trie. זה הופך את ה-trie לאידיאלי להשלמה אוטומטית, לבדיקת איות ולחיפוש לפי תחילית.
סיבוכיות זמן וזיכרון
| פעולה | סיבוכיות | הערות |
|---|---|---|
| הכנסה | O(m) | m = אורך המילה |
| חיפוש | O(m) | צעד אחד לכל תו |
| שאילתת תחילית | O(m) | הליכה עד לצומת של התחילית |
| זיכרון | O(total chars) | תחיליות משותפות נשמרות פעם אחת |
צעד אחר צעד (הכנסה)
| צעד | מה קורה |
|---|---|
| 1 | מתחילים בצומת השורש. |
| 2 | לכל תו במילה, מחפשים קשת בן תואמת. |
| 3 | אם היא קיימת, הולכים בה (ומשתמשים שוב בתחילית המשותפת). |
| 4 | אם לא, יוצרים צומת בן חדש עבור התו הזה. |
| 5 | אחרי התו האחרון, מסמנים את הצומת הזה כסוף מילה. |
דוגמה מפורטת
הכנסת ["car", "card", "care"] ל-trie ריק:
| צעד | מבנה | פעולה |
|---|---|---|
הכנסת car | root → c → a → r✓ | אין בנים תואמים, לכן יוצרים את c, a, r ומסמנים את r כסוף מילה. |
הכנסת card | root → c → a → r✓ → d✓ | משתמשים שוב במסלול הקיים c-a-r, ואז יוצרים בן חדש אחד d ומסמנים אותו כסוף מילה. |
הכנסת care | root → c → a → r✓ → {d✓, e✓} | משתמשים שוב ב-c-a-r, מתפצלים מ-r עם בן חדש e, ומסמנים את e כסוף מילה. |
חיפוש care | root → c → a → r → e✓ | הולכים דרך c, a, r, e; הצומת האחרון מסומן כסוף מילה, ולכן care קיימת. |
חיפוש ca | root → c → a | המסלול קיים אבל a לא מסומן כסוף מילה, ולכן ca היא תחילית ולא מילה שמורה. |
מתי להשתמש ב-trie
| כדאי כאשר | עדיף להימנע כאשר |
|---|---|
| צריך שאילתות תחילית או השלמה אוטומטית על קבוצת מחרוזות. | צריך רק חיפוש של מפתחות שלמים: טבלת גיבוב מהירה וקלה יותר. |
| מילים רבות שמורות חולקות תחיליות משותפות, כך שצמתים מנוצלים שוב. | המפתחות ארוכים ורק לעיתים רחוקות חופפים, וזה מבזבז צומת לכל תו. |
| רוצים לקבל את המפתחות בסדר ממוין באמצעות מעבר על העץ. | הזיכרון מוגבל: מצביעי הבנים בכל צומת מוסיפים תקורה משמעותית. |
| עלות החיפוש צריכה להיות תלויה באורך המפתח ולא בגודל אוסף הנתונים. | האלפבית ענק (למשל כל Unicode) והבנים נשמרים בצורה צפופה. |
קוד Trie (Prefix Tree)
מימוש נקי של Trie (Prefix Tree) שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Trie (Prefix Tree) ב-Python
1class TrieNode:2 def __init__(self):3 self.children = {}4 self.is_word = False5
6
7class Trie:8 def __init__(self):9 self.root = TrieNode()10
11 def insert(self, word):12 node = self.root13 for ch in word:14 node = node.children.setdefault(ch, TrieNode())15 node.is_word = True16
17 def search(self, word):18 node = self._walk(word)19 return node is not None and node.is_word20
21 def starts_with(self, prefix):22 return self._walk(prefix) is not None23
24 def _walk(self, s):25 node = self.root26 for ch in s:27 if ch not in node.children:28 return None29 node = node.children[ch]30 return node31
32
33trie = Trie()34for word in ["car", "card", "care", "dog"]:35 trie.insert(word)36
37print("search(card): ", trie.search("card"))38print("search(ca): ", trie.search("ca"))39print("starts_with(ca): ", trie.starts_with("ca"))40print("starts_with(do): ", trie.starts_with("do"))41print("starts_with(cat): ", trie.starts_with("cat"))קוד Trie (Prefix Tree) ב-JavaScript
1class TrieNode {2 constructor() {3 this.children = {};4 this.isEnd = false;5 }6}7
8class Trie {9 constructor() {10 this.root = new TrieNode();11 }12
13 insert(word) {14 let node = this.root;15 for (const ch of word) {16 if (!node.children[ch]) node.children[ch] = new TrieNode();17 node = node.children[ch];18 }19 node.isEnd = true;20 }21
22 // Walk the prefix; null when a character is missing23 findNode(prefix) {24 let node = this.root;25 for (const ch of prefix) {26 node = node.children[ch];27 if (!node) return null;28 }29 return node;30 }31
32 search(word) {33 const node = this.findNode(word);34 return node !== null && node.isEnd;35 }36
37 startsWith(prefix) {38 return this.findNode(prefix) !== null;39 }40}41
42const trie = new Trie();43for (const word of ["car", "card", "care", "dog"]) trie.insert(word);44console.log("search(\"card\"):", trie.search("card"));45console.log("search(\"ca\"):", trie.search("ca"));46console.log("startsWith(\"ca\"):", trie.startsWith("ca"));קוד Trie (Prefix Tree) ב-Java
1public class Main {2 static class Node {3 Node[] children = new Node[26];4 boolean isWord;5 }6
7 static Node root = new Node();8
9 static void insert(String word) {10 Node cur = root;11 for (char c : word.toCharArray()) {12 int i = c - 'a';13 if (cur.children[i] == null) cur.children[i] = new Node();14 cur = cur.children[i];15 }16 cur.isWord = true;17 }18
19 // Follow the path for s; null means no word has this prefix20 static Node walk(String s) {21 Node cur = root;22 for (char c : s.toCharArray()) {23 cur = cur.children[c - 'a'];24 if (cur == null) return null;25 }26 return cur;27 }28
29 static boolean search(String word) {30 Node node = walk(word);31 return node != null && node.isWord;32 }33
34 static boolean startsWith(String prefix) {35 return walk(prefix) != null;36 }37
38 public static void main(String[] args) {39 insert("code");40 insert("coder");41 insert("cool");42 System.out.println("search code: " + search("code"));43 System.out.println("search cod: " + search("cod"));44 System.out.println("startsWith cod: " + startsWith("cod"));45 System.out.println("startsWith cat: " + startsWith("cat"));46 }47}קוד Trie (Prefix Tree) ב-C++
1#include <iostream>2#include <string>3#include <unordered_map>4
5struct TrieNode {6 std::unordered_map<char, TrieNode*> children;7 bool isEnd = false;8};9
10struct Trie {11 TrieNode* root = new TrieNode();12
13 void insert(const std::string& word) {14 TrieNode* node = root;15 for (char c : word) {16 if (!node->children.count(c)) node->children[c] = new TrieNode();17 node = node->children[c];18 }19 node->isEnd = true;20 }21
22 // Follow the path for s; nullptr if a link is missing23 TrieNode* walk(const std::string& s) const {24 TrieNode* node = root;25 for (char c : s) {26 auto it = node->children.find(c);27 if (it == node->children.end()) return nullptr;28 node = it->second;29 }30 return node;31 }32
33 bool search(const std::string& word) const {34 TrieNode* node = walk(word);35 return node != nullptr && node->isEnd;36 }37
38 bool startsWith(const std::string& prefix) const {39 return walk(prefix) != nullptr;40 }41};42
43int main() {44 Trie trie;45 for (const std::string& word : {"car", "card", "care", "dog"}) {46 trie.insert(word);47 }48 std::cout << std::boolalpha;49 std::cout << "search(card): " << trie.search("card") << "\n";50 std::cout << "search(ca): " << trie.search("ca") << "\n";51 std::cout << "startsWith(ca): " << trie.startsWith("ca") << "\n";52 std::cout << "startsWith(dot): " << trie.startsWith("dot") << "\n";53 return 0;54}קוד Trie (Prefix Tree) ב-C
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5typedef struct TrieNode {6 struct TrieNode* children[26];7 bool isEnd;8} TrieNode;9
10TrieNode* newNode(void) {11 return calloc(1, sizeof(TrieNode)); // zeroed links and isEnd12}13
14void insert(TrieNode* root, const char* word) {15 for (; *word; word++) {16 int i = *word - 'a';17 if (root->children[i] == NULL) root->children[i] = newNode();18 root = root->children[i];19 }20 root->isEnd = true;21}22
23// Follow the path for s; NULL if a link is missing24TrieNode* walk(TrieNode* root, const char* s) {25 for (; *s; s++) {26 root = root->children[*s - 'a'];27 if (root == NULL) return NULL;28 }29 return root;30}31
32bool search(TrieNode* root, const char* word) {33 TrieNode* node = walk(root, word);34 return node != NULL && node->isEnd;35}36
37bool startsWith(TrieNode* root, const char* prefix) {38 return walk(root, prefix) != NULL;39}40
41int main(void) {42 TrieNode* root = newNode();43 const char* words[] = {"car", "card", "care", "dog"};44 for (int i = 0; i < 4; i++) insert(root, words[i]);45 printf("search(card): %s\n", search(root, "card") ? "true" : "false");46 printf("search(ca): %s\n", search(root, "ca") ? "true" : "false");47 printf("startsWith(ca): %s\n", startsWith(root, "ca") ? "true" : "false");48 printf("startsWith(dot): %s\n", startsWith(root, "dot") ? "true" : "false");49 return 0;50}שאלות נפוצות על trie
למה משמש trie?
מהי סיבוכיות הזמן של trie?
O(m), כאשר m הוא אורך המילה או התחילית, בלי תלות במספר המילים השמורות. המחיר הוא זיכרון: trie יכול לצרוך הרבה מקום, אף שתחיליות משותפות נשמרות פעם אחת בלבד.מה ההבדל בין trie לטבלת גיבוב?
O(1) בממוצע, אבל לא יכולה לענות על שאילתות תחילית. Trie איטי מעט יותר בכל חיפוש, אבל תומך באופן טבעי בחיפוש לפי תחילית, במעבר ממוין ובהשלמה אוטומטית, ולכן מעדיפים אותו במקרים האלה.מתי להשתמש ב-trie במקום בעץ חיפוש בינארי?
O(m) לכל פעולה לפי אורך המפתח, בעוד עץ חיפוש בינארי מאוזן עולה O(m log n), כי כל השוואה סורקת את המחרוזת ויש log n השוואות כאלה. עץ חיפוש בינארי הוא הבחירה הטובה יותר כשהמפתחות אינם מחרוזות או כשתקורת הזיכרון חשובה יותר ממהירות התחיליות.איך מטפלים בסוף מילה ב-trie?
card, הצומת של car קיים, אבל צריך לדווח עליו כמילה רק אם גם car הוכנסה.