Trie树进阶:Double-Array Trie原理及状态转移过程详解
前言:
? Trie樹本身就是一個很迷人的數(shù)據(jù)結(jié)構(gòu),何況是其改進(jìn)的方案。
? 在本博客中我會從DAT(Double-Array Tire)的原理開始,并結(jié)合其源代碼對DAT的狀態(tài)轉(zhuǎn)移過程進(jìn)行解析。如果因此你能從我的博客中有所收獲或啟發(fā),It's my pleasure.
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/49281865 -- Q-WHai?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?--轉(zhuǎn)載請注明出處
特別說明:
0.關(guān)于Trie樹的構(gòu)建及使用,請移步:http://blog.csdn.net/lemon_tree12138/article/details/49177509
1.本文參考:
(0)雙數(shù)組Trie樹(DoubleArrayTrie)Java實現(xiàn)-碼農(nóng)場
(1)基于雙數(shù)組Trie樹算法的字典改進(jìn)和實現(xiàn) - 期刊論文 - 道客巴巴
圖形展示及說明:
0.樸素Trie樹示意圖:
??
? 從上圖中可以看到,這樣的樹結(jié)構(gòu)是非常稀疏的。造成了資源的巨大浪費。
1.DAT節(jié)點示意圖:
??
? 這里"NULL"代表結(jié)束。
DAT原理說明:
0.簡介:
? 在學(xué)習(xí)DAT(Double-Array Trie)之前,如果你對Tire樹的了解還是處在一個模糊的狀態(tài),那么我想你現(xiàn)在可以移步到本人的另一篇博客《數(shù)據(jù)結(jié)構(gòu):字典樹的基本使用》,在對Trie樹有一個基本的了解之后,再來學(xué)習(xí)本文的內(nèi)容應(yīng)該會更加輕松自如(如果你對Trie樹已經(jīng)有了或淺或深的了解,那么可以直接看下面的內(nèi)容了)。
? DAT的本質(zhì)是一個有限自動機(jī)(因為博主在學(xué)習(xí)DAT之前對自動機(jī)的相關(guān)內(nèi)容也是一知半解,在學(xué)習(xí)DAT的過程,難免有一些痛苦。博主也緊追一下這方面的知識,也會在后面的博客中寫一些相關(guān)的博文).我們要構(gòu)建一些狀態(tài),用于狀態(tài)的自動轉(zhuǎn)移。顧名思義,在DAT中用的就是雙數(shù)組:base數(shù)組和check數(shù)組。雙數(shù)組的分工是:base負(fù)責(zé)記錄狀態(tài),用于狀態(tài)轉(zhuǎn)移;check負(fù)責(zé)檢查各個字符串是否是從同一個狀態(tài)轉(zhuǎn)移而來,當(dāng)check[i]為負(fù)值時,表示此狀態(tài)為字符串的結(jié)束。
? 你可能問一個這樣的問題:那么base數(shù)組和check數(shù)組是怎么來進(jìn)行狀態(tài)轉(zhuǎn)移呢?
? 請看下面關(guān)于DAT雙數(shù)組的計算過程。
1.DAT中雙數(shù)組的計算過程:
假定有字符串狀態(tài)s,當(dāng)前字符串狀態(tài)為t,假定t加了一個字符c就等于狀態(tài)tc,加了一個字符x等于狀態(tài)tx,那么有:
base[t] + c.code = base[tc]
base[t] + x.code = base[tx]
check[tc] = check[tx]
上面的幾個等式就是狀態(tài)base和它的轉(zhuǎn)移方程。
Double-Array Trie源碼解析:
0.特別說明:
? DAT中的節(jié)點信息如下:
private static class Node {int code;int depth;int left;int right; }? code: 代表節(jié)點字符的編碼。如:'a'.code = 97
? depth: 代表節(jié)點所在樹的深度。root.depth = 0
? left: 代表節(jié)點的子節(jié)點在字典中范圍的左邊界
? rigth:?代表節(jié)點的子節(jié)點在字典中范圍的右邊界
1.DAT的創(chuàng)建
? 和Trie樹一樣,DAT的創(chuàng)建只是創(chuàng)建Root的過程。如下:
public int build(List<String> _key) {key = _key;...resize(65536 * 32);...Node root_node = new Node();root_node.left = 0;root_node.right = keySize;root_node.depth = 0;...return error_;}
2.為節(jié)點parent生成子節(jié)點
? 在生成子節(jié)點的過程中,如果碰到parent='B',而'B'又是某一個key的結(jié)尾。該如何辦呢?
? 比如:比如若以"一舉"中的'舉'字符為parent,那么parent.depth = 2,"一舉".length = 2.
? 遇到這種情況,我們就需要對其進(jìn)行過濾操作,過程如下:
String tmp = key.get(i);int currCode = 0;if (tmp.length() != parent.depth) {currCode = (int) tmp.charAt(parent.depth) + 1;}完整過程:
private int fetch(Node parent, List<Node> siblings) {...int prevCode = 0;for (int i = parent.left; i < parent.right; i++) {if (key.get(i).length() < parent.depth) {continue;}String tmp = key.get(i);int currCode = 0;if (tmp.length() != parent.depth) {currCode = (int) tmp.charAt(parent.depth) + 1;}...if (currCode != prevCode || siblings.size() == 0) {Node tmp_node = new Node();tmp_node.depth = parent.depth + 1;tmp_node.code = currCode;tmp_node.left = i;if (siblings.size() != 0) {siblings.get(siblings.size() - 1).right = i;}siblings.add(tmp_node);}prevCode = currCode;}if (siblings.size() != 0) {siblings.get(siblings.size() - 1).right = parent.right;}return siblings.size();}
3.向Trie樹中插入子節(jié)點
? 在DAT的創(chuàng)建過程中,insert是關(guān)鍵部分。
? 在insert操作里,我們使用了遞歸的思路來解決問題。為什么要利用遞歸呢?因為在我們狀態(tài)轉(zhuǎn)移的過程中,父節(jié)點的base值需要依賴子返回的begin值,因為在DAT中,code[null] = 0,所以也可以認(rèn)為是依賴于子節(jié)點的check值,如此反復(fù),層層嵌套。關(guān)于這一點在下面的結(jié)構(gòu)圖展示中更容易體現(xiàn)。
(0)check的合法性檢查
? 之前我們說check數(shù)組是為了檢查各個字符串是否是從同一個狀態(tài)轉(zhuǎn)移而來,但是,要如何檢查呢?看下面這段代碼:
outer: while (true) {position++;if (check[position] != 0) {continue;} else if (first == 0) {...}begin = position - siblings.get(0).code; // 當(dāng)前位置離第一個兄弟節(jié)點的距離...for (int i = 1; i < siblings.size(); i++) {if (check[begin + siblings.get(i).code] != 0) {continue outer;}}break;}? 這里的position即在數(shù)組中的下標(biāo)。可以看到這是一個循環(huán)遍歷的過程,我們在一個合適的位置開始,逐步地嘗試check值是否合法,找到第一個合法的begin值即可。
? 而check[i]合法的條件就是check[i]是否為0。如果check[i]不為0,則說明此位置已經(jīng)被別的狀態(tài)占領(lǐng)了,需要更換到下一個位置。
(1)計算所有子節(jié)點的check值
for (int i = 0; i < siblings.size(); i++) {check[begin + siblings.get(i).code] = begin;}
(2)計算所有子節(jié)點的base值
private int insert(List<Node> siblings) {...for (int i = 0; i < siblings.size(); i++) {List<Node> new_siblings = new ArrayList<Node>();if (fetch(siblings.get(i), new_siblings) == 0) {base[begin + siblings.get(i).code] = (value != null) ? (-value[siblings.get(i).left] - 1) : (-siblings.get(i).left - 1);...} else {int h = insert(new_siblings);base[begin + siblings.get(i).code] = h;}}return begin;}在這一步中,大家可以很明顯地看到,這是一個遞歸的過程。我們需要獲得子節(jié)點的begin值。
采用遞歸之后,我們的DAT節(jié)點的狀態(tài)轉(zhuǎn)移過程
(3)整體的insert過程:
private int insert(List<Node> siblings) {...// check的合法性檢查...// 計算所有子節(jié)點的check值// 計算所有子節(jié)點的base值...}
DAT中雙數(shù)組的狀態(tài)轉(zhuǎn)移過程
4.前綴查詢
? 現(xiàn)在假設(shè)待查找字符串T="走廊里的壁畫",我們需要在之前的字典中查找所有是T前綴的字符串。我們要怎么做呢?
? 其實在上面的雙數(shù)組狀態(tài)轉(zhuǎn)移過程圖中,我們可以很清楚地找到一條滿足條件的路徑.如下:
??
關(guān)鍵代碼如下:
public List<Integer> commonPrefixSearch(String key, int pos, int len, int nodePos) {...int b = base[nodePos];...for (int i = pos; i < len; i++) {p = b;n = base[p];if (b == check[p] && n < 0) {result.add(-n - 1);}p = b + (int) (keyChars[i]) + 1;if (b == check[p]) {b = base[p];} else {return result;}}p = b;n = base[p];if (b == check[p] && n < 0) {result.add(-n - 1);}return result;}
5.關(guān)鍵詞智能提示:
? 在上面“前綴查詢”的例子中,我們的匹配字符串中比較長,在還沒到字符串的最后一位就遇到狀態(tài)停止標(biāo)志。而如果匹配字符串比較短,我就還可以做一些其他的事情了,比如常見的搜索引擎中關(guān)鍵詞智能提示。
? 過程就是在上一步的基礎(chǔ)上,把終止循環(huán)的條件修改為直到遇到一個狀態(tài)停步標(biāo)志.這樣我們就可以在遍歷整條路徑。
? 這個功能,在源碼中沒有涉及。而本文的目的是在于解釋DAT的原理和其狀態(tài)轉(zhuǎn)移的過程。所以,這里就暫不貼代碼了。不過,在后期的《搜索引擎:對用戶輸入有誤的關(guān)鍵詞進(jìn)行糾錯處理》博客中應(yīng)該會有所涉及。感興趣的朋友,可以關(guān)注下。
實現(xiàn)源碼下載:
http://download.csdn.net/detail/u013761665/9201933
總結(jié)
以上是生活随笔為你收集整理的Trie树进阶:Double-Array Trie原理及状态转移过程详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大数据算法:对5亿数据进行排序
- 下一篇: VHD(Virtual Hard Dis