Android程序員面試會遇到的算法系列:html
隔了三個月,終於下定決心繼續更新了,仍是想把 關於算法這部分寫完整,此次我會開始介紹一些數據結構的用法,原本想說更新一個關於並查集的問題的。可是思前想後,仍是字典樹的實際用例更多一些,例子也容易讓人理解。因此此次我要詳細的講一下Trie-> AKA 字典樹這個數據結構的用法和一些實際的例子。後端
首先Trie這個單詞是一個新的詞彙,中文的翻譯通常說是字典樹,或者叫前綴樹(由於用這個樹結構能夠經過前綴搜索數據),中文的翻譯就比較容易讓人理解。可是有趣的是這個單詞的英文發音卻頗有爭議,若是你在Google上搜索Trie Pronunciation
,你會發現你們都在糾結於究竟是發Tree的音仍是Try的音。數組
固然,這些都不是重點,隨便哪一個我以爲你們都能理解,反正千萬別到時候由於某些同窗和你認知的發音不同就去鄙視人家。。。
華麗麗的分割線
那麼說會重點,通俗的來說,字典樹是一種能夠方便人們經過前綴查找單詞的一種數據結構,它把具備相同前綴的單詞合併到一塊兒,節省了存儲空間(和直接用HashMap存相比),同時也能夠作前綴查找或者自動補全。
舉個例子,我就搬來維基百科的例子
假設咱們要收錄一下幾個單詞Tea, Ted, Ten,Inn。那麼咱們會以如下的方式把這些單詞逐一錄入樹種:
- 建立初始節點Root, 當前插入節點爲根節點
- 先準備錄入Tea,分解Tea的字母T, E, A,先把第一個字母T放入根節點下面。而且把當前插入節點改成T
- 在T也就是當前插入節點下面放入E,當前插入節點改成E
- 在E也就是當前插入節點下面放入A,當前插入節點改成A,可是單詞全部字母都輸入結束了,所以在A上面打個tag,標記該節點是某個單詞的結束字母。
第一個單詞錄入完畢
- 準備錄入Ted,當前插入節點爲根節點。分解單詞Ted爲T,E,D.準備插入第一個字母T
- 先檢查根節點下面有沒有T這個字母。答案是有,那麼不須要再插入一個T了,直接將插入節點改成T。
- 第二個輸入字母爲E,一樣的,T下面也有一個E,也不須要再插入E了,直接將插入節點改成E.
- 第三個輸入字母爲D,此次當前的插入節點E下面沒有D,那麼咱們插入D,當前插入節點也改成D
- 由於TED輸入完畢,因此在D上面打一個tag,標記標記該節點是某個單詞的結束字母。
剩下的字母同理可得
在討論代碼的實現以前,咱們先看看字典樹的實際應用:
字典樹的最經典的應用確定就是自動補全了,好比你在Google或者百度進行搜索的時候
谷歌將不少熱門的搜索單詞用字典樹的方式存儲在後端,當Web網頁搜索單詞的前綴的時候,後端查找到該前綴最後一個節點的位置,將下面的子節點所有排列出來返回給前端。
好比用以上的例子,當用戶搜索TE的時候,定位到最後一個節點E,將E節點下面的節點返回而且拼湊出一個完整的字符串,TED和TEA。
還有好比說當咱們在Android Studio按下:alt+control+o
,而且搜索文件的時候:
當咱們搜索Main的時候,顯示以Main開頭的文件。
這個功能也能夠用字典樹實現(固然這是能夠,不必定就是用字典樹實現,事先聲明省得誤人子弟,由於作索引還能夠用B樹來作,相似數據庫,這裏不展開,感興趣的能夠看阮一峯大神的關於數據庫原理的文章)
請自動忽略我粉紅色的鍵盤。。。。
好比說咱們在通信錄搜索185,app會自動返回以185開頭的號碼的聯繫人,這個道理也是同樣的,只不過樹的內容再也不是單詞,而是數字罷了。
有同窗問,貌似通信錄app不僅能夠搜索號碼吧,應該照理來講搜索名字也是能夠,好比根據姓來搜索。
這個答案很簡單,再根據名字作另一個字典樹不就好了。
這裏我用一個Leetcode的題目做爲參考,代碼是我本身實現的。
Leetcode 字典樹
/** * * 實現字典樹,訣竅就是用什麼數據結構保存children,我的偏向用hashmap,由於用數組寫起來也麻煩並且會浪費多餘的空間。26個字符不必定全都用獲得。 * */
class TrieNode {
// Initialize your data structure here.
char c;
HashMap<Character, TrieNode> children = new HashMap<Character, TrieNode>();
boolean hasWord;
public TrieNode() {
}
public TrieNode(char c) {
this.c = c;
}
}
public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// Inserts a word into the trie.
public void insert(String word) {
TrieNode cur = root;
//curChildren 對應咱們講的當前插入節點,全部要插入的節點都要放在當前插入節點的子節點裏面
HashMap<Character, TrieNode> curChildren = root.children;
char[] wordArray = word.toCharArray();
//循環遍歷此次要插入的單詞,從第一個字母開始
for (int i = 0; i < wordArray.length; i++) {
char wc = wordArray[i];
//若是當前插入節點有這個字符對應的子節點的話,直接把該子節點變成當前插入節點
if (curChildren.containsKey(wc)) {
cur = curChildren.get(wc);
} else {
//若是沒有的話,建立一個。
TrieNode newNode = new TrieNode(wc);
curChildren.put(wc, newNode);
cur = newNode;
}
curChildren = cur.children;
//若是該節點是插入單詞的最後一個字符,打一個tag,表面這是一個單詞的結尾、
if (i == wordArray.length - 1) {
cur.hasWord = true;
}
}
}
// Returns if the word is in the trie.
//有時候雖然該單詞做爲前綴,存在於字典樹中,可是卻沒有這個單詞。好比我搜索
//TE。這個前綴存在,可是並無這個單詞,E字母對應的節點沒有tag
public boolean search(String word) {
if (searchWordNodePos(word) == null) {
return false;
} else if (searchWordNodePos(word).hasWord)
return true;
else
return false;
}
// Returns if there is any word in the trie
// that starts with the given prefix.
public boolean startsWith(String prefix) {
if (searchWordNodePos(prefix) == null) {
return false;
} else
return true;
}
//搜索制定單詞在字典樹中的最後一個字符對應的節點。這個方法是搜索的關鍵
public TrieNode searchWordNodePos(String s) {
TrieNode cur = root;
char[] sArray = s.toCharArray();
for (int i = 0; i < sArray.length; i++) {
char c = sArray[i];
if (cur.children.containsKey(c)) {
cur = cur.children.get(c);
} else {
return null;
}
}
return cur;
}
}
複製代碼
這部分是簡單的搜索和生成字典樹,可是並無實現如何返回字符串。其實返回字符串就是暴力搜索的一個過程,把最後一個節點一下的全部子節點組合起來。這裏可使用咱們以前講到的深度優先的方法(其實就至關於找一個樹的全部path的過程,參考Leetcode 257. Binary Tree Paths))。
感興趣的同窗能夠先本身實現一下。
這裏最後貼上個人實現
public ArrayList<String> getAutoCompletionStrings(String preflix){
TrieNode lastNode = searchWordNodePos(preflix);
if( lastNode == null ){
return new ArrayList<String>();
}
else{
ArrayList<String> list = new ArrayList<String>();
trieHelper(list,"",lastNode);
return list;
}
}
private void trieHelper(ArrayList<String> result, String current, TrieNode node) {
if (node != null && node.hasWord) {
result.add(current);
} else if(node != null){
Set<Entry<Character,TrieNode>> set = node.children.entrySet();
for( Entry<Character,TrieNode> entry : set ){
trieHelper(result, current+entry.getValue().c , entry.getValue());
}
}
return;
}
複製代碼