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