一、開篇說明
1.本文原文來自於leetcode上的演算法題Implement Trie的解決方案.
2.原文地址
3.新手獻醜了,希望大家輕拍~(微笑)
二、原文在這
1.問題描述
演算法題:
通過編寫插入、查詢、判斷開頭等方法完成一個trie樹。
例子:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // returns true
trie.search("app"); // returns false
trie.startsWith("app"); // returns true
trie.insert("app");
複製程式碼
提示:
- 你可以假設所有的輸入都是由小寫字母a-z組成的。
- 所有的輸入string陣列都不為空。
總結:
這篇文章是寫給中等水平的讀者的,將會介紹資料結構trie(字首樹)和其中的常見操作。
2.解決方法:
2.1應用:
trie(字首樹)是一種樹形資料結構,常常用來在字串的資料集中檢索一個關鍵詞。目前,trie資料結構已經被高效地應用在了很多領域:
(1)自動填充
(2)拼寫檢查
(3)IP路由(最長的路由匹配) (4)九鍵輸入法的預測文字 (5)完成文字遊戲
有很多其他的資料結構如,平衡樹,hash表都能夠在一個string的資料集中查詢單詞,但是我們為什麼要使用trie呢?雖然hash表對於找到一個關鍵詞(key)只需要O (1)的時間複雜度,但是在下列操作中,它就表現得不是很高效了。
- 找到擁有共同字首的所有關鍵詞(key)。
- 根據字典序列舉所有字串
trie優於hash表的另外一個原因是,但hash表的資料規模擴大後,將會出現很多的hash碰撞,因此查詢時間複雜度將會提高到O (n),n 是插入的關鍵詞的個數。而相比於hash表,trie在儲存很多具有共同字首的關鍵詞時需要的空間更少。在這個例子裡trie只需要O (m)的時間複雜度(m 是關鍵詞的長度)。而在平衡樹裡查詢一個關鍵詞,則需要花費O (m logn)
2.2 trie節點結構
trie是一個有根樹,它的節點有以下幾個欄位:
- 與子節點間最多有R個連線,每個連線對應到R個字母中的一個。這個R個字母來自於字母表。在這篇文章中,我們假設R是26,即26個小寫的拉丁字母。
- 一個布林值isEnd,說明該布林值說明當前節點是否是一個關鍵詞的結尾,否則就只是該關鍵詞的字首。
java編寫的trie節點
class TrieNode {
// R links to node children
private TrieNode[] links;
private final int R = 26;
private boolean isEnd;
public TrieNode() {
links = new TrieNode[R];
}
public boolean containsKey(char ch) {
return links[ch -'a'] != null;
}
public TrieNode get(char ch) {
return links[ch -'a'];
}
public void put(char ch, TrieNode node) {
links[ch -'a'] = node;
}
public void setEnd() {
isEnd = true;
}
public boolean isEnd() {
return isEnd;
}
}
複製程式碼
2.3 trie中最常見的操作——新增和查詢關鍵詞
(1)新增關鍵詞到trie中
我們通過遍歷trie來插入關鍵詞。我們從根節點開始,搜尋和關鍵詞第一個字母對應的連線,這裡一般有兩種情況:
- 如果連線存在,那麼我們就順著這個連線往下移到下一子層,接著搜尋關鍵詞的下一個字母對應的連線。
- 如果連線不存在,那麼我們就新建一個trie節點,對應著現在的關鍵詞字母,建立與父節點的連線。
我們重複這個步驟,直到處理完關鍵詞的最後一個字母,然後標記最後的節點為結束節點。演算法結束。
java編寫的插入方法
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// Inserts a word into the trie.
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char currentChar = word.charAt(i);
if (!node.containsKey(currentChar)) {
node.put(currentChar, new TrieNode());
}
node = node.get(currentChar);
}
node.setEnd();
}
}
複製程式碼
複雜度分析:
- 時間複雜度O(m),m 是關鍵詞的長度。在演算法的每一次迴圈中,我們要麼檢查節點要麼新建一個節點,直到該關鍵詞的最後一個字母。所以,這隻需要進行m次操作。
- 空間複雜度O(m)。最糟糕的情況是,新插入的關鍵詞和已經存在trie的關鍵詞沒有共同的字首,因此我們必須插入m個新的節點,因此需要O(m)空間複雜度。
(2)在trie中搜尋關鍵詞
每一個關鍵詞在trie中都可以被一條從根節點到子節點的路徑所表示。我們將根據關鍵詞的第一個字母從根節點開始搜尋,然後檢查節點上的每一個連線對應的字母,一般有兩種情況:
- 存在對應關鍵詞字母的連線,我們將從該連線移動到下一個節點,然後搜尋關鍵詞的下一個字母對應的連線。
- 對應的連線不存在,如果此時已經遍歷到了關鍵詞的最後一個字母,則把當前的節點標記為結束節點,然後返回true。當然還有另外兩種情況,我們會返回false:
- 關鍵詞的字母沒有遍歷完,但沒有辦法接著在trie中找到根據關鍵詞字母的形成的路徑,所以trie中不存在該關鍵詞。
- 關鍵詞的字母的已經遍歷完了,但當前的節點不是結束節點,因此搜尋的關鍵詞只是trie中的某一個關鍵字的字首。
class Trie {
...
// search a prefix or whole key in trie and
// returns the node where search ends
private TrieNode searchPrefix(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char curLetter = word.charAt(i);
if (node.containsKey(curLetter)) {
node = node.get(curLetter);
} else {
return null;
}
}
return node;
}
// Returns if the word is in the trie.
public boolean search(String word) {
TrieNode node = searchPrefix(word);
return node != null && node.isEnd();
}
}
複製程式碼
複雜度分析:
- 時間複雜度:O(m)。在演算法的每一步都是搜尋關鍵詞的下一個字母,因此在最差的情況下,演算法需要執行m步。
- 空間複雜度:O(1)。
(3)在trie中搜尋關鍵詞的字首
這個方法和我們在trie中用來搜尋關鍵詞的方法很類似。我們從根節點開始移動,直到關鍵詞字首的每個字母都被搜尋到了,或者,沒有辦法在trie中根據關鍵詞的當前字母找到接下去的路徑。這個方法和前面提到的搜尋關鍵詞的唯一不同在於,當我們遍歷到關鍵詞字首的最後一個字母時,我們總是返回true,我們不需要考慮當前的節點是否有結束標誌,因為我們只是搜尋關鍵詞的字首,而不是整個關鍵詞。
java編寫的搜尋關鍵詞字首的方法
class Trie {
...
// Returns if there is any word in the trie
// that starts with the given prefix.
public boolean startsWith(String prefix) {
TrieNode node = searchPrefix(prefix);
return node != null;
}
}
複製程式碼
複雜度分析:
- 時間複雜度:O(m)
- 空間複雜度:O(1)
3. 應用問題
這裡有一些非常合適應大家去練習的問題,這些問題都能用trie資料結構解決。
- 新增和搜尋單詞(資料結構設計)——一個非常直接的trie的應用。
- 搜尋單詞2——和boggle這個遊戲有點像
這篇分析出自@elmirap