在作用戶 query 理解的過程當中,有許多須要使用詞典來"識別"的過程。在此期間,就避免不了使用 Trie 樹這一數據結構。java
所以今天咱們來深刻的學習一下 Trie 樹相關的理論知識,而且動手編碼實現。node
下面的定義引自維基百科。後端
在計算機科學中,trie,又稱前綴樹或字典樹,是一種有序樹,用於保存關聯數組,其中的鍵一般是字符串。與二叉查找樹不一樣,鍵不是直接保存在節點中,而是由節點在樹中的位置決定。一個節點的全部子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串。通常狀況下,不是全部的節點都有對應的值,只有葉子節點和部份內部節點所對應的鍵纔有相關的值。
一個簡單的 Trie 結構以下圖所示:數組
從上面的圖中,咱們能夠發現一些 Trie 的特性。微信
一般在實現的時候,會在節點結構中設置一個標誌,用來標記該結點處是否構成一個單詞(關鍵字), 或者存儲一些其餘相關的值。數據結構
能夠看出,Trie 樹的關鍵字通常都是字符串,並且 Trie 樹把每一個關鍵字保存在一條路徑上,而不是一個結點中。另外,兩個有公共前綴的關鍵字,在 Trie 樹中前綴部分的路徑相同,因此 Trie 樹又叫作前綴樹(Prefix Tree)。ide
Trie 樹的每一個節點的子節點,是一堆單字符的集合,咱們能夠很方便的進行對全部字符串進行字典序的排序工做。只須要將字典序先序輸出,輸出全部子節點時按照字典序遍歷便可。因此 Trie 樹又叫作字典樹。函數
Trie 樹的核心思想就是:用空間來換時間,利用字符串的公共前綴來下降查詢時間的開銷以達到提升效率的目的。學習
固然,在大數據量的狀況下,Trie 樹的空間也未必會大於哈希表。只要經過共享前綴節省的空間可以 Cover 對象的額外開銷。
Trie 的強大之處就在於它的時間複雜度,插入和查詢的效率很高,都爲O(N)
,其中 N 是待插入/查詢的字符串的長度,而與 Trie 中保存了多少個元素無關。
關於查詢,會有人說 hash 表時間複雜度是O(1)
不是更快?可是,哈希搜索的效率一般取決於 hash 函數的好壞,若一個壞的 hash 函數致使不少的衝突,效率並不必定比 Trie 樹高。
而 Trie 樹中不一樣的關鍵字就不會產生衝突。它只有在容許一個關鍵字關聯多個值的狀況下才有相似 hash 碰撞發生。
此外,Trie 樹不用求 hash 值,對短字符串有更快的速度。由於一般,求 hash 值也是須要遍歷字符串的。
也就是說,從理論上來說,Trie 樹的時間複雜度是穩定的,而 hash 表的時間複雜度是不穩定的,取決於 hash 函數的好壞,也和存儲的字符串集有關係。
而從工業應用上來說,我的推薦:若是你不須要用到 Trie 樹前綴匹配的特性,直接用 hash 表便可。
緣由有如下幾點:
做爲一個工程師,我學習一個東西最重要的地方就是了解他的應用場景,全部只存在於書本上而沒有成熟應用的技術,我都淺嘗輒止。
在學習 Trie 樹時,我也花了不少時間來查找,記錄它的應用場景,列舉在此處,若是各位同窗有其餘的應用場景,不妨留言你們討論。
K-V 存儲及檢索
這是 Trie 樹嘴原始樸素的使用方法,也就是須要和 hash 表進行競爭的地方。
詞頻統計
咱們能夠修改 Trie 樹的實現,將每一個節點的 是否在此構成單詞
標誌位改爲此處構成的單詞數量
. 這樣咱們能夠用它進行搜索場景常見的詞頻統計。
固然這個需求 hash 表也是能夠實現的。
字典序排序
將全部待排序集合逐個加入到 Trie 樹中,而後按照先序遍歷輸出全部值。在遍歷某個節點的全部子節點的時候,按照字典序進行輸出便可。
前綴匹配
例如:找出一個字符串集合中全部以 ab 開頭的字符串。咱們只須要用全部字符串構造一個 trie 樹,而後輸出以$a->b->$開頭的路徑上的關鍵字便可。
trie 樹前綴匹配經常使用於搜索提示。好比各類搜索引擎上的 自動聯想後半段功能。
最長公共前綴
查找一組字符串的最長公共前綴,只須要將這組字符串構建成 Trie 樹,而後從跟節點開始遍歷,直到出現多個節點爲止(即出現分叉)。
做爲輔助結構
做爲其餘數據結構的輔助結構,如後綴樹,AC 自動機等
首先實現 Trie 樹的節點:
package com.huyan.trie; import java.util.*; /** * Created by pfliu on 2019/12/06. */ public class TNode { /** * 當前節點字符 */ private char c; /** * 當前 節點對應數字 */ int count = 0; private TNode[] children; private static int hash(char c) { return c; } @Override public String toString() { return "TNode{" + "c=" + c + ", count=" + count + ", children=" + Arrays.toString(children) + '}'; } TNode(char c) { this.c = c; } /** * 將 給定字符 添加到給定列表中。 * @param nodes 給定的 node 列表 * @param c 給定字符 * @return 插入後的節點 */ private static TNode add(final TNode[] nodes, char c) { int hash = hash(c); int mask = nodes.length - 1; for (int i = hash; i < hash + mask + 1; i++) { int idx = i & mask; if (nodes[idx] == null) { TNode node = new TNode(c); nodes[idx] = node; return node; } else if (nodes[idx].c == c) { return nodes[idx]; } } return null; } /** * 將 當前節點 放入到給定的 節點列表中。 * 用於 resize 的時候轉移節點列表 * @param nodes 節點列表 * @param node 給定節點 */ private static void add(final TNode[] nodes, TNode node) { int hash = hash(node.c); int len = nodes.length - 1; for (int i = hash; i < hash + len + 1; i++) { int idx = i & len; if (nodes[idx] == null) { nodes[idx] = node; return; } else if (nodes[idx].c == node.c) { throw new IllegalStateException("Node not expected for " + node.c); } } throw new IllegalStateException("Node not added"); } /** * 將 給定字符 插入到當前節點的子節點中。 * @param c 給定字符 * @return 插入後的節點 */ TNode addChild(char c) { // 初始化子節點列表 if (children == null) { children = new TNode[2]; } // 嘗試插入 TNode node = add(children, c); if (node != null) return node; // resize // 轉移節點列表到新的子節點列表中 TNode[] tmp = new TNode[children.length * 2]; for (TNode child : children) { if (child != null) { add(tmp, child); } } children = tmp; return add(children, c); } /** * 查找當前節點的子節點列表中,char 等於給定字符的節點 * @param c 給定 char * @return 對應的節點 */ TNode findChild(char c) { final TNode[] nodes = children; if (nodes == null) return null; int hash = hash(c); int len = nodes.length - 1; for (int i = hash; i < hash + len + 1; i++) { int idx = i & len; TNode node = nodes[idx]; if (node == null) { return null; } else if (node.c == c) { return node; } } return null; } }
而後實現 Trie 樹。
package com.huyan.trie; import java.util.*; /** * Created by pfliu on 2019/12/06. */ public class Trie { /** * 根節點 */ final private TNode root = new TNode('\0'); /** * 添加一個詞到 Trie * * @param word 待添加詞 * @param value 對應 value */ public void addWord(String word, int value) { if (word == null || word.length() == 0) return; TNode node = root; for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); // 當前 char 添加到 trie 中,並拿到當前 char 對應的那個節點 node = node.addChild(c); } node.count = value; } /** * 查找 word 對應的 int 值。 * * @param word 給定 word * @return 最後一個節點上存儲的 int. */ public int get(String word) { TNode node = root; for (int i = 0; i < word.length(); i++) { node = node.findChild(word.charAt(i)); if (node == null) { return 0; } } return node.count; } private int get(char[] buffer, int offset, int length) { TNode node = root; for (int i = 0; i < length; i++) { node = node.findChild(buffer[offset + i]); if (node == null) { return 0; } } return node.count; } /** * 從給定字符串的 offset 開始。 * 查找最大匹配的第一個 int 值。 * * @param str 給定字符串 * @param offset 開始查找的偏移量 * @return 第一個匹配的字符串德最後一個節點的 int 值。 */ public String maxMatch(String str, int offset) { TNode node = root; int lastMatchIdx = offset; for (int i = offset; i < str.length(); i++) { char c = str.charAt(i); node = node.findChild(c); if (node == null) { break; } else if (node.count != 0) { lastMatchIdx = i; } } return lastMatchIdx == offset ? null : str.substring(offset, lastMatchIdx + 1); } /** * 從給定字符串的 offset <b>反向</b>開始。 * 查找最大匹配的第一個 int 值。 * * @param str 給定字符串 * @param offset 開始查找的偏移量 * @return 第一個匹配的字符串德最後一個節點的 int 值。 */ public int maxMatchBack(String str, int offset) { TNode node = root; int lastMatchIdx = offset; for (int i = offset; i >= 0; i--) { char c = str.charAt(i); node = node.findChild(c); if (node == null) { break; } else if (node.count != 0) { lastMatchIdx = i; } } return offset - lastMatchIdx + 1; } /** * 從給定字符串的 offset 開始。檢查 length 長度。 * 查找最大匹配的第一個 int 值。 * * @param buffer 給定字符串 * @param offset 開始查找的偏移量 * @return 第一個匹配的字符串德最後一個節點的 int 值。 */ public int maxMatch(char[] buffer, int offset, int length) { TNode node = root; int lastMatchIdx = offset; for (int i = offset; i < offset + length; i++) { char c = buffer[i]; node = node.findChild(c); if (node == null) { break; } else if (node.count != 0) { lastMatchIdx = i; } } return lastMatchIdx - offset + 1; } public static void main(String[] args) { Trie trie = new Trie(); for (String s : Arrays.asList("呼延", "呼延二十")) { trie.addWord(s, 1); } String input = "延十在寫文章"; System.out.println(trie.maxMatch(input, 0)); } }
代碼中基本上實現了 Trie 的基本功能,可是對 trie 的應用方法有不少,好比匹配前綴,好比求最長匹配前綴的長度等。這些就不一一實現了。
https://www.cnblogs.com/huang...
https://zh.wikipedia.org/wiki...
完。
最後,歡迎關注個人我的公衆號【 呼延十 】,會不按期更新不少後端工程師的學習筆記。
也歡迎直接公衆號私信或者郵箱聯繫我,必定知無不言,言無不盡。
<h4>ChangeLog</h4>
2019-05-19 完成
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十