--- tags: data structure --- # 資料結構筆記 - Trie 如果今天想要實作如 google 搜尋列中的自動補完功能,通常會先有一個字典結構存放關鍵字, Trie 是一種樹狀結構也稱為字典樹,用以在大量字串中快速找尋是否存在該字串。此外,Trie 也有人叫前綴樹,因為它是由字串間相同的前綴文字組成。 一個典型的 Trie 如下: ![](https://i.imgur.com/UCHr7N5.png) 如果我們順著 Root 往下走,每次隨著節點走到 Leaf 時都可以得到一個字串,如上圖我們可以拼出 *CAR*、*CAT*、*DOT* 3 個字串。其中 Trie 的 Root 節點是不存放文字的。 所以今天如果要確認某個字串是否存在於我們的字典裡,只要依序將該字串從頭開始從 Trie 的 Root 出發,只要相同,我們就取出下個文字看看是否和下個子節點相同,一直到該字串沒有文字可以取出來比對就表示存在,如果中途有不同的子節點或者是已經走到 Leaf 但該字串尚未比對完就表示不存在字典中。 比方今天我們用 *CA* 比對,先從 *C* 開始,第一步有節點 **C** 和 *C* 相同,所以我們接著取出 *A* 和 **C** 的子節點比對,而 **C** 的子節點是 **A** 也相同,這時我們要比對的字串已經到尾巴了,所以表示 *CA* 存在於字典中。 如果是用 *CARD* 比對,一樣從 *C* 開始,和節點 **C** 相同,接著 *A* 和 **C** 的子節點 **A** 相同,然後 *R* 和 **A** 的子節點 **R** 相同,但此時 **R** 沒有子節點已經走到 Leaf,但還有 *D* 尚未比對,所以我們知道 *CARD* 不存在字典中。 實作時的 `Node` 內部以 Java 為例: ```java= class Node { Map<Character, Node> children; boolean isWord; } ``` `children` 存放子節點文字和其 reference,而 `isWord` 標示走到這個節點時是否已經組成之前插入過的文字。 ###### Java ```java= import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; class Node { Map<Character, Node> children = new HashMap<>(); boolean isWord = false; } public class Trie { private Node root; public Trie() { root = new Node(); } public void insert(String word) { if (word == null || word.length() == 0) { return; } Node currentNode = root; for (int i = 0; i < word.length(); i++) { Character c = word.charAt(i); Map<Character, Node> children = currentNode.children; // 如果子節點有此字元,則不需新增節點 if (children.get(c) != null) { currentNode = children.get(c); } else { // 否則就在目前的節點下新增此字元的子節點 currentNode = new Node(); children.put(c, currentNode); } } // 遍歷到最後一個字元時,將該節點的 isWord 標示為 true // 表示如果從 root 走到這個節點時,目前組合的字串是之前插入過的 currentNode.isWord = true; } public boolean search(String word) { if (word == null || word.length() == 0) { return false; } if (root.children.size() == 0) { return false; } // 從 root 出發 Node currentNode = root; for (int i = 0; i < word.length(); i++) { // 取出這個字元對應的子節點 currentNode = currentNode.children.get(word.charAt(i)); // 如果沒有子節點表示 Trie 中沒有這個字串 if (currentNode == null) { return false; } } // 遍歷到最後一個字元時則檢查該節點是否標為 isWord // 如果為 true 表示之前有插入過相同的字串 // 如果為 false 表示只是剛好前綴和欲找尋的字串相同 return currentNode.isWord; } public void display() { Node currentNode = root; display("", currentNode); } // 使用遞迴遍歷整個 Trie,顯示目前儲存的字串 private void display(String word, Node currentNode) { if (currentNode.isWord) { System.out.println("含有: " + word); } // 如果已經到 leaf, 則回到上一層, 所以組好的字串要減去最後一個字 if (currentNode.children == null || currentNode.children.size() == 0) { word = word.substring(0, word.length() - 1); return; } Set<Character> keySet = currentNode.children.keySet(); Iterator<Character> iterator = keySet.iterator(); while (iterator.hasNext()) { Character c = iterator.next(); word += c; display(word, currentNode.children.get(c)); // 執行到這裡表示這個 Node 的子樹已經遍歷完, 所以要換下個 Node // 因此目前的字串要減掉這次遍歷到的 Node 包含的字, 也就是最後一個字 word = word.substring(0, word.length() - 1); } } public static void main(String[] args) { Trie trie = new Trie(); trie.insert("test"); trie.insert("MyPhone"); trie.insert("測試中文"); trie.insert("te"); trie.display(); System.out.println("含有 te? " + trie.search("te")); System.out.println("含有 test? " + trie.search("test")); System.out.println("含有 My? " + trie.search("My")); System.out.println("含有 myPhone? " + trie.search("myPhone")); System.out.println("含有 測試中文? " + trie.search("測試中文")); System.out.println("含有 測試? " + trie.search("測試")); } } ``` ###### 輸出結果 ``` 含有: te 含有: test 含有: 測試中文 含有: MyPhone 含有 te? true 含有 test? true 含有 My? false 含有 myPhone? false 含有 測試中文? true 含有 測試? false ``` 參考資料: 1. https://leetcode.com/articles/implement-trie-prefix-tree/#