Trie (drzewo prefiksowe)
Ostatnia aktualizacja
Trie (czytaj „traj”), czyli drzewo prefiksowe, przechowuje zbiór napisów według ich znaków: każda krawędź ma etykietę w postaci znaku, a ścieżka od korzenia tworzy prefiks. Słowa o wspólnym prefiksie korzystają z tych samych węzłów, więc „car”, „card” i „care” używają tej samej ścieżki c-a-r. Naciśnij Odtwórz powyżej i zobacz, jak słowa są wstawiane znak po znaku i rozgałęziają się tylko tam, gdzie się różnią.
Wyszukiwanie przechodzi przez jeden węzeł na znak, więc znalezienie słowa o długości m zajmuje O(m) bez względu na to, ile słów zawiera trie. Dzięki temu trie świetnie nadaje się do autouzupełniania, sprawdzania pisowni i wyszukiwania po prefiksie.
Złożoność czasowa i pamięciowa
| Operacja | Złożoność | Uwagi |
|---|---|---|
| Wstawianie | O(m) | m = długość słowa |
| Wyszukiwanie | O(m) | Jeden krok na znak |
| Zapytanie o prefiks | O(m) | Przejście do węzła prefiksu |
| Pamięć | O(total chars) | Wspólne prefiksy przechowywane tylko raz |
Krok po kroku (wstawianie)
| Krok | Co się dzieje |
|---|---|
| 1 | Zacznij od korzenia. |
| 2 | Dla każdego znaku słowa szukaj pasującej krawędzi do dziecka. |
| 3 | Jeśli istnieje, przejdź po niej (korzystając ze wspólnego prefiksu). |
| 4 | Jeśli nie, utwórz nowy węzeł potomny dla tego znaku. |
| 5 | Po ostatnim znaku oznacz ten węzeł jako koniec słowa. |
Przykład krok po kroku
Wstawiamy ["car", "card", "care"] do pustego trie:
| Krok | Struktura | Działanie |
|---|---|---|
Wstaw car | root → c → a → r✓ | Nie ma pasujących dzieci, więc utwórz c, a, r i oznacz r jako koniec słowa. |
Wstaw card | root → c → a → r✓ → d✓ | Użyj istniejącej ścieżki c-a-r, potem utwórz jedno nowe dziecko d i oznacz je jako koniec słowa. |
Wstaw care | root → c → a → r✓ → {d✓, e✓} | Użyj c-a-r, odgałęź się od r nowym dzieckiem e i oznacz e jako koniec słowa. |
Szukaj care | root → c → a → r → e✓ | Przejdź przez c, a, r, e; ostatni węzeł jest oznaczony jako koniec słowa, więc care jest w trie. |
Szukaj ca | root → c → a | Ścieżka istnieje, ale a nie jest oznaczony jako koniec słowa, więc ca jest prefiksem, a nie zapisanym słowem. |
Kiedy używać trie
| Używaj, gdy | Unikaj, gdy |
|---|---|
| Potrzebujesz zapytań o prefiks lub autouzupełniania w zbiorze napisów. | Wyszukujesz tylko całe klucze: tablica mieszająca jest szybsza i lżejsza. |
| Wiele zapisanych słów ma wspólne prefiksy, więc węzły są współdzielone. | Klucze są długie i rzadko się pokrywają, co marnuje jeden węzeł na znak. |
| Chcesz otrzymywać klucze w kolejności posortowanej przez przejście drzewa. | Pamięci jest mało: wskaźniki na dzieci w każdym węźle dodają spory narzut. |
| Koszt wyszukiwania ma zależeć od długości klucza, a nie od rozmiaru danych. | Alfabet jest ogromny (np. cały Unicode), a dzieci są przechowywane gęsto. |
Trie (Prefix Tree): kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Trie (Prefix Tree) w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Trie (Prefix Tree): kod (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): kod (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): kod (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): kod (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): kod (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: najczęstsze pytania
Do czego służy trie?
Jaka jest złożoność czasowa trie?
O(m), gdzie m to długość słowa lub prefiksu, niezależnie od liczby zapisanych słów. Ceną jest pamięć: trie może zajmować dużo miejsca, choć wspólne prefiksy są przechowywane tylko raz.Czym różni się trie od tablicy mieszającej?
O(1), ale nie odpowie na zapytania o prefiks. Trie jest nieco wolniejsze przy pojedynczym wyszukiwaniu, za to naturalnie obsługuje wyszukiwanie po prefiksie, przechodzenie w kolejności i autouzupełnianie, dlatego to ono jest wybierane w takich zastosowaniach.Kiedy użyć trie zamiast drzewa BST?
O(m) względem długości klucza, a zrównoważone drzewo BST kosztuje O(m log n), bo każde porównanie przechodzi przez napis, a porównań jest log n. Drzewo BST jest lepszym wyborem, gdy klucze nie są napisami albo gdy narzut pamięci ma większe znaczenie niż szybkość wyszukiwania po prefiksie.Jak oznacza się koniec słowa w trie?
card węzeł dla car istnieje, ale powinien być zgłoszony jako słowo tylko wtedy, gdy car też zostało wstawione.