<因爲本文從word中粘貼過來,故排版比較亂,可下載附件中的pdf版>java
引言node
在許多的信息檢索應用中,不少地方都須要之前綴匹配的方式來檢索輸入的字符串。好比:編譯器的詞法分析、目錄檢索、拼寫檢查、中文分詞的詞庫等天然語言處理相關的應用。爲了提升檢索的效率,咱們一般把字符串構建成Trie樹的形式。Trie樹的每一個結點是一個數組,數組中存儲着下一個結點的索引信息。如對K=set{baby,bachelor,badge,jar}創建Trie樹,結構以下圖:編程
從上圖中能夠看出,基於Trie樹對字符串進行檢索、刪除、插入是很是快的,可是空間開銷卻隨着字符種類數和結點個數成比例增加,並且這種增長是成指數的增加。爲了解決空間閒置問題,咱們最容易想到的壓縮方法就是把Trie樹結點中的數組換成鏈表,這樣就避免了數組中出現大量的NULL值狀況,通俗地說,就是用左孩子右兄弟的方式來維持Trie樹,以下圖:數組
可是這樣卻帶來了檢索效率的下降,特別是一個結點有多個子結點的時候,例如,圖中結點b的孩子結點a有{c,b,d}三個孩子,這樣在檢索badge的時候,就須要遍歷結點b-a-c-b-d-g-e(如上圖紅色的線條。)才能檢索到。數據結構
本文提出了一種新的壓縮方法,把Trie樹壓縮到兩個一維數組BASE和CHECK中,數組BASE和CHECK合稱Double-Array。在Double-Array中,非空的結點n經過BASE數組映射到CHECK數組中。也就是說,沒有任何兩個非空的結點會經過BASE數組映射到CHECK數組的同一個位置。Trie樹中的每條邊都能經過Double-Array以o(1)的複雜度檢索到。換句話說:若是一個字符串的長度爲k,那麼最多隻須要o(k)的複雜度就能夠完成檢索。當字符串的數量衆多時,使Double-Array中的空間儘量充分利用就變得十分重要了。爲了使Trie樹可以知足存儲海量字符串的需求,Double-Array中只儲存字符串中的前綴,而後把字符串的剩餘部分存儲到字符數組中,咱們把這個字符數組稱爲tail數組。經過Double-Array和tail數組的組合,就達到了儘量節約內存,同時又能區別出任意兩個字符串的目的。dom
詳細解說DATrie(Double-Array Trie)ide
Trie是一種樹形的數據結構。Trie中從根結點到葉子結點的每條路徑表明着存儲在Trie中的一個字符串(key)。這也就是說,路徑中鏈接兩個結點的邊表明着一個字符。這跟咱們一般瞭解到的把字符信息儲存到結點中的樹是不同的。爲了不相似於the 和then這樣的字符串在Trie中形成混淆,咱們引入一個特別的字符# 用來表示每一個字符串的結束。這樣插入the和then到Trie中時,其實是插入the# 和then# 。函數
爲了更清晰地解說Trie,咱們做以下的定義:性能
K 表明造成Trie的字符串集合。Trie由結點和鏈接結點的邊(arc)組成。結點由Double-Array的下標來標記,邊則是由字符來標記,若是一條邊從結點n到結點m被標記成a,那麼咱們能夠定義以下的函數g(n,a)=m 。學習
對於集合K中的一個字符串S在Trie中造成的一條路徑P,若是路徑P中有結點m知足g(n,a)=m ,使得在Trie中檢索S時,檢索到字符a就已經可以將字符串S與Trie中的其它字符串區別開來,那麼結點m稱爲separate node 。例如Figure 3中結點15、結點5、結點6、結點4都是separatenode。
從separatenode到葉子結點之間的字符串稱之爲結點m的 single string.用STR[m]表示。例如圖Figure3中str[5]、str[6]都是single string.從S中刪除singlestring後剩餘的部分稱爲tail . (這裏我沒沒理解清楚,只是以爲tail與single string是一個意思,就是STR[m])樹中只由從根結點到separatenode 之間的邊組成的部分稱爲reduced trie。Figure3就是reducedtrie的一個例子,用Double-Array和字符數組來存儲tail信息。TAIL數組中的問號標誌?用來表示廢棄的空間(原來存儲過信息,後來該位置不用了)。關於Double-Aarray 和TAIL的用法將會在插入操做和刪除操做中詳細解釋。
在Figure3 中,Double-Array和reducedtrie 的關係以下:(到這裏就很容易理解了,reduced trie表示的是一種結構,而Double-Array則表示reduced-trie這種結構的存儲方式)
第1、若是reduced trie中的一條邊知足g(n,a)=m,那麼對應到Double-Array中的實現則有:BASE[n]+a=m, CHECK[m]=n (對於邊的標識字符,有以下約定 #=1 a=2 b=3 c=4…. )
第2、若是結點m是seprate node,由此獲得的tail字符串STR[m]=b1b2....bh那麼有 BASE[m]<0 設p=-BASE[m],則TAIL[p]=b1 ,TAIL[p+1]=b2 …… TAIL[p+h-1]=bh
以上兩條關係將貫穿本文。
例如:
對於關係一:
Figure 3中根結點1到結點7 及字符b :有g(1,b)=7 -> BASE[1]+b = 4+3=7 (b=3) CHECK[7]=1
對於關係二:
Figure 3中結點5,BASE[5]=-1 ,對應到TAIL數組中p=-BASE[5]=1 TAIL[1]=h TAIL[2]=e TAIL[3]=l
那麼從根結點到結點5,到STR[m],咱們就能夠檢索到bachelor#這個字符串了。
實際上,有如下幾點是須要提出來的:
1、結點1永遠是Trie的根結點,因此Trie的插入和查找操做都是從BASE[1]開始。
2、CHECK[m]=n 所表達的意思是:結點m的父結點是n . 因此若是表述爲father[m]=n可能更清楚一些。
3、在Double-Array中,除CHECK[1]以外,若是CHECK[m]=0,則表示結點m是孤島,是可用的。Double-Array實際就就是經過g(n,a)=m把這些孤島鏈接成reduced trie這種樹形結構。這一點在理解trie的insertion操做時會有幫助。
字符串的查找
經過Double-Array對字符串進行查找是很是快捷的。例如:用Double-Array查找Figure3中的字符串bachelor# 。執行步驟以下:
步驟1:從根結點1開始,因爲咱們已經定義b=3,因此:
BASE[n]+a=BASE[1]+b=4+3=7
咱們也觀察到CHECK[7]=1 因此這是一條通路,能往下走。
步驟2:因爲BASE[1]=7>0,咱們繼續。用7做爲BASE數組新的下標,因爲bachelor#的第二個字符是a,因此
BASE[n]+a=BASE[7]+2=1+2=3 . 並且CHECK[3]=7
步驟3,4:按如上的方式繼續,因爲已經定義了c=4,咱們有:
BASE[3]+c=BASE[3]+4=1+4=5並且CHECK[5]=3
步驟5:BASE[5]=-1 ,表示剩下的字符串被存儲到了TAIL數組中。從TAIL[ -BASE[5]]=TAIL[1]開始,檢索剩下的字符串就能夠用最經常使用的字符串比較方法了。
反覆體會這個過程,咱們可以發現:在trie中檢索字符串只是直接地在數組中定位下一個節點。沒有相似於廣度或者深度優先搜索這樣的查找操做。這種方式使得字符串的查找變得十分直截了當。
實際上在編寫代碼的時候,查找操做仍是有一些細節須要注意的。這些細節就只能在代碼裏面體會了。
插入操做
DATrie的插入操做也是至關的直截了當。在插入過程當中,無外乎如下四種狀況:
Case 1 : 插入字符串時樹爲空。
Case 2: 插入字符串時無衝突。
Case 3: 插入字符串時出現不用修改現有BASE值的衝突,可是須要把TAIL數組中的字符信息展開到BASE數組中。(解決有公共前綴字符串的問題)
Case 4: 插入字符時出現須要修改現有BASE值的衝突。(位置佔用衝突)
衝突的出現意味着在double-array中兩個不一樣的字符經過g(n,a)獲得了一樣的m值,換話話說,兩個不一樣的父結點擁有了同一個孩子(表如今double-array中就是兩個字符爭奪數組中的同一個空間位置)。上述的四種狀況將經過在一棵空的Trie (Figure 4)中插入bachelor# (Case1) ;jar#(Case 2); badge#(Case 3) baby#(Case 4) 一一演示出來。咱們定義DA_SIZE表示double-array中CHECK數組的最大長度(BASE數組與CHECK數組大小相等),而且BASE數組和CHECK數組的長度能夠動態地增長,數組默認以0來填充。
Case 1 : 插入字符串時Trie樹爲空。
(樹爲空時,BASE[1]=1,CHECK[1]=0, TAIL數組的起始位置POS=1; 這其實也就是編碼時Trie的初始化。)
插入單詞bachelor#將按以下步驟進行;
步驟1:從double-array中BASE數組的下標1開始(也就是樹的根結點)b的值爲3,因此
BASE[1]+b=1+3=4, CHECK[4]=0≠1
步驟2:CHECK[4]=0表示結點4是separate node,因爲b已經經過g(1,’b’)=4這種方式存儲到double-array中了,因此單詞剩下的部分achelor#直接插入到TAIL數組中便可。
步驟3:賦值BASE[4]=-POS=-1 代表achelor#將從POS位置開始插入到TAIL數組中。
步驟4:賦值POS=1+length(‘achelor#’) =9,表示下一個字符串存儲到TAIL數組的起始位置。
Figure 5 顯示了插入bachelor#後reduced trie 和double-array的狀態。
Case 2: 插入字符串時無衝突。
按以下的步驟插入單詞jar#
步驟1:從double-array的BASE數組下標1開始,因爲已經定義j=11
BASE[1]+j=1+11=12 CHECK[12]=0≠1
步驟2:CHECK[12]=0表示結點12是空結點,能夠將剩餘的部分ar#直接插入到TAIL數組中去。插入的起始位置POS=9
步驟3:賦值BASE[12]=-POS=-9,同時賦值CHECK[12]=1 代表結點12是結點1的子結點。
步驟4:設置POS=12 , 表示下一個字符串存儲到TAIL數組的起始位置。
實際上經過插入bachelor#和jar#很難看出Case 1 和Case 2之間的不一樣,因此其實他們的不一樣也是隻概念上不一樣,而非操做上的不一樣。(因爲Case 1和Case 2的實現很是簡單,也無需糾結於此。咱們可認爲這是做者玩的文字遊戲)插入jar#後reduced trie和double-array的狀態如圖Figure 6所示:
爲了研究Case 3和Case 4兩種狀況,咱們須要定義一個函數X_CHECK(LIST)其功能是返回一個最小的整數q,q知足以下條件:q>0 而且對於LIST中的每個元素c,有CHECK[q+c]=0。(對於X_CHECK函數,咱們能夠分兩步理解,第一步:CHECK [m]=n表示結點m的父結點是n;第二步:設LIST={c1,c2,…cn},咱們可認爲q+c1,q+c2 … q+cn都是將要被領養的孩子,而這些孩子被領養必須有一個條件:沒有父親,而CHECK[q+c]=0即表示結點q+c沒有父結點)
Case 3:公共前綴衝突
(公共前綴衝突是我本身起的名字,上文已經交代過,這種衝突的特色是無需修改以有的結點位置,即BASE數組中的非零值,只是把TAIL數組中的字符「解壓縮」到double-array中)
經過插入單詞badge#,咱們能夠認識到這種衝突。
步驟1:從BASE[1]開始,因爲b=3,因此:
BASE[1]+b=1+3=4, CHECK[4]=1
CHECK[4]的非零值表示有一條邊從結點CHECK[4](也就是結點1)到到結點4,(就是Figure 6中的字符b所標識的邊)
步驟2:若是BASE[1]>0,咱們直接到下一個結點就能夠了,可是這裏:
BASE[1]=-1
BASE[1]的值爲負代表在trie中的查詢已經結束,咱們須要到TAIL數組中進行字符串比較。
步驟3:從pos=-BASE[1]=1做爲TAIL數組的起始位置,比較achelor#和待插入字符串的剩餘部分,也就是adge#。當兩個字符串的比較失敗,就用步驟4、5、6的方式把他們的公共前綴插入到double-array中。
步驟4:申明一個臨時變量TEMP,並把-BASE[1]保存到這個臨時變量中
TEMP à -BASE[1]
步驟5:計算字符串achelor#和adge#的公共前綴字符a的X_CHECK[{‘a’}]值:
CHECK[q+a]= CHECK[1+2]=CHECK[3]=0因此q=1
(q是從1開始遞增試出來的)
這樣1就做爲了BASE[4]新的候選值,CHECK[3]=0也代表結點3是能夠做爲結點4的子結點。這樣結點4和結點3就能夠經過g(4,a)=g(4,2)=3關係式,把字符a存儲到double-array中了。
步驟6:給BASE[4]賦新的值:
BASE[4]=1 , 同時賦值CHECK[BASE[4]+a]=CHECK[1+2]=CHECK[3]=4
這樣trie中就有一條新的邊誕生了,邊從結點4開始到結點3結束,邊的標識符號爲a。
注意:因爲本例中只有一個公共前綴,因此步驟5和步驟6沒有重複。可是若是有多個公共前綴,步驟5、6會重複執行屢次,直到公共前綴都處理完。
步驟7:爲了存儲剩下的字符串chelor#和dge#,咱們須要爲BASE[3]尋找新的候選值,使得字符c和字符d可以存儲到double-array中,其計算方法爲X_CHECK[{‘c’,’d’}]
對於’c’ : CHECK[q+’c’]=CHECK[1+4]=CHECK[5]=0 知足條件
對於’d’: CHECK[q+’d’]=CHECK[1+5]=CHECK[6]=0 知足條件
因此q=1, 賦值BASE[3]=1
步驟8:以字符串chelor#的首字符做爲參數,計算字符串在BASE和CHECK數組中的結點編號。經過該結點能夠在TAIL中定位到helor# 。
BASE[3]+’c’=1+4=5
BASE[5]=-TEMP=-1 ,CHECK[5]=3
經過BASE數組創建到TAIL數組的引用,經過CHECK數組肯定狀態3到狀態5的邊。
(這裏「狀態」與「結點」是同一個意思)
步驟9:把字符串的剩餘部分」helor#」存儲到TAIL數組中,其起始位置爲-BASE[5]=1,只是TAIL[7]和TAIL[8]兩個位置就變成空位了。(實際編程會有所不一樣)
步驟10:對於新插入字符串剩餘部分」dge#」:
BASE[3]+’d’=1+5=6 ;
BASE[6]=-POS=-12
CHECK[6]=3
而後把」ge#」存儲到TAIL數組中。
步驟11:最後更新POS爲下一次插入的起始位置,也就是TAIL中已用空間的的末尾。
POS=12+length[‘ge#’]=12+3=15
總的來講,當衝突發生了,字符串中產生衝突的公共前綴須要從TAIL數組中提取出來,而後存儲到double-array中。衝突字符串(包括新插入的字符串)在double-array中關聯的值(知足條件BASE[n]<0)都要轉移到最近的空結點位置。(參考Figure 7)
Case 4:搶佔位置引起的衝突
(如今進入到整篇文章最核心的地方了,也就是DATrie最難的地方)
就像Case 3 同樣,BASE數組中的值必須進行修改才能解決衝突。插入」baby#」的步驟演示以下:
步驟1:根結點在BASE數組的下標1位置,因此從BASE[1]開始。
對於baby#中前三個字符而言,BASE和CHECK中的值以下;
BASE[4]+’a’=1+2=3, CHECK[3]=4
BASE[3]+’b’=1+3=4, CHECK[4]=1≠3
CHECK[4]的計算結果出現先後不一致的現象代表有一個狀態沒有被考慮到。這也意味着結點1或者結點3的BASE值須要進行修改。(這裏能夠這樣理解:結點4做爲子結點被父結點1和父結點3爭奪,咱們有兩種方法解決衝突:結點1放棄孩子或者結點3放棄孩子。若是是結點1讓步,那麼就須要修改BASE[1],若是結點3讓步,那麼就須要修改BASE[3])
步驟2:申明變量TEMP_NODE1,並賦值TEMP_NODE1 = BASE[3]+’ b’=1+3=4
若是CHECK[4]=0,那麼直接把剩餘部分存儲到TAIL中就能夠了,可是事與願違。
步驟3:分別把從結點3和結點1引出的邊存儲到list中,經過Figure 7有:
list[3]={‘c’,’d’}
list[1]={‘b’,’j’}
步驟4:因爲咱們的目的是讓新插入的字符串與結點3進行關聯(實際上修改BASE[1]或者修改BASE[3]哪一種方案最優,是經過工做量來進行衡量的。由於修改BASE[n]同時須要修改BASE[n]的子結點位置,因此子結點數越少,工做量就越少),即字符’b’將給結點3帶來一個新子結點,因此從結點3引出的邊的個數須要加1。因此咱們:
Compare(length(list[3])+1,list[1]) =compare(3,2)
若是length(list[3]+1)<length(list[1]),那麼咱們就修改結點3的BASE值,可是因爲length(list[3]+1)≥length(list[1]) ,咱們修改結點1。
步驟5:申明變量TEMP_BASE,賦值
TEMP_BASE=BASE[1]=1
而且用X_CHECK計算BASE[1]新的候選值:
X_CHECK(‘b’): CHECK[q+’b’]=
CHECK[1+3]=CHECK[4]≠0
CHECK[2+3]=CHECK[5]≠0
CHECK[3+3]=CHECK[6]≠0
並且對於
X_CHECK(‘j’): CHECK[q+’j’]=
CHECK[4+11]=CHECK[15]=0知足條件
因此q=4合法,賦值 BASE[1]=4
步驟 6:對於’b’ ,把將被修改的狀態值存儲到臨時變量中:
TEMP_NODE1 TEMP_BASE+ ‘b’=1+3=4
TEMP_NODE2 BASE[1]+ ‘b’=4+3=7
把BASE值從舊的狀態更新到新的狀態:
BASE[TEMP_NODE2]---- BASE[TEMP_NODE1] 也就是
BASE[7]=BASE[4]=1
同時把CHECK值也更新
CHECK [TEMP_NODE2 ] =CHECK [7] ---- CHECK [4]=1
(這裏其實也好理解,對於結點1,原來的子結點爲4和12,BASE值更新後,子結點隨之變成7和15,把原來結點4和結佔12的BASE及CHECK值轉移到結點7和結點15就完成了子結點的更新了,若是子結點還有孩子,好比結點4的子結點爲3,那麼更新後,結點3的父結點將再也不是結點4,而變成結點7)
步驟7:因爲
BASE[TEMP_NODE1]=BASE[4]=1>0 <結點4有子結點>
把結點4的全部子結點的父結點更新爲結點7
CHECK [ BASE[4]+2] = CHECK [l+2]= CHECK [3] TEMP_NODE2 =7
步驟8:結點1的子結點從結點4變成結點7後,結點4已經空置了。因此須要進行標記:
BASE[4]=0
CHECK[4]=0
一樣地,對於’j’:<導向子結點12的邊>
步驟9:把將被修改的狀態值存儲到臨時變量中:
TEMP_NODE1 TEMP_BASE+ ‘j’=1+11=12
TEMP_NODE2 BASE[1]+ ‘j’=4+11=15
把BASE值從舊的狀態更新到新的狀態:
BASE[TEMP_NODE2] ----BASE[TEMP_NODE1] 也就是
BASE[15]=BASE[12]=-9
同時把CHECK值也更新
CHECK [ TEMP_NODE2 ]=CHECK [15] ---- CHECK [12]=1
步驟10:因爲
BASE[TEMP_NODE1]=BASE[12]=-9<0
即BASE[12]沒有子結點,因此只須要把結點12置空就能夠了。
BASE[12]=0
CHECK[12]=0
這樣的話由baby中的字符’b’產生的衝突就被解決了。最後,把新插入字符串的剩餘部分存儲到TAIL數組中就OK了。
步驟11:從產生衝突的那個結點(即結點3)開始,從新計算由字符’b’獲得的新結點,並把它存儲到臨時變量TEMP_NODE中
TEMP_NODE=BASE[3]+’b’=4
步驟12:把字符串在TAIL數組存儲的起始位置存儲到BASE數組中
BASE[TEMP_NODE]=BASE[4]=-POS=-15
步驟13:把字符串的剩餘部分存儲到TAIL數組中
TAIL[POS]=TAIL[15]+’y#’
步驟14:更新POS爲下一次插入的起始位置,也就是TAIL中已用空間的的末尾。
POS=15+length[‘y#’]=15+2=17
小結一下,當double-array中發生了位置佔用衝突,咱們須要修改產生衝突結點的父結點BASE值,對於這兩個父結點(對應到程序中就是pre和CHECK[cur]),具體修改哪個取決於其子結點的個數,子結點個數少的父結點將被修改。這樣衝突就能夠獲得解決,字符串也能順利地插入到trie中了。插入後的結果如Figure 3所示:
Trie的刪除操做
Trie的刪除操做也是很是簡單。把前面的插入熟悉後,本身看論文《An Efficient Implementation of TrieStructures》就能懂了,這裏也就不繼續解說了。
論文剩下的部分就是性能的評估,感興趣的能夠自行了解。最後有實現的僞代碼,仍是有必定的參考價值的。
其實,字符串處理的相關數據結構,無非是利用了字符串的前綴和後綴信息。好比SuffixArray(後綴數組)利用的是後綴信息,Trie樹,利用的是前綴信息。
理解DATrie樹,咱們應該認識到,DATrie是一種數據結構,理解數據結構,只須要理解數據結構對數據的」增刪改查」四種操做就能夠了。對於DATrie,其核心在於理解插入操做;而插入操做的難點在於理解BASE數組和CHECK數組,BASE數組和CHECK數組的難點在於插入時出現衝突的解決方案。因此,DATrie樹的難點只有一個:衝突解決方案。
在學習的過程,反覆在紙上畫出trie的結構,本身推理double-array值對於理解trie是很是有幫助的。
最後提供一個測試樣例:「ba」 「bac」 」be」「bae」,由於沒有這個樣例,我在編碼的時候被困了好幾天。
在個人筆記本電腦上<i5+4G內存+32位win7+2.4GZ>,用DATrie 插入38萬數量的詞典,用時240084毫秒,查詢用時299毫秒。
最後仍是貼出代碼吧!
package com.vancl.dic; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Arrays; public class DATrie { final int DEF_LEN=1024; final char END_TAG='#';//#在CWC中的value=1 int[] base = new int[DEF_LEN]; int[] check = new int[DEF_LEN]; char[] tail =new char[DEF_LEN]; int tailPos; public DATrie(){ base[1]=1;tailPos=1; Hash.put(END_TAG,1); } /* * 查找一個詞是否在Trie樹結構中 * */ public boolean retrieval(String word){ //執行查詢操做 int pre=1,cur=0; for(int i=0;i<word.length();i++){ cur=base[pre]+GetCode(word.charAt(i)); if(check[cur]!=pre)return false; //到tail數組中去查詢 if(base[cur]<0 ){ int head=-base[cur]; return MatchInTail(word, i+1, head); } pre=cur; } //這一句是關鍵,對於一個串是另外一個字符串 子串的狀況 if(check[base[cur]+GetCode(END_TAG)]==cur)return true; return false; } public void insert(String word){ word+=END_TAG; for(int i=0,pre=1,cur;i<word.length();i++){ cur=base[pre]+GetCode(word.charAt(i)); //容量不夠的時,擴容 if(cur>=base.length)extend(); //空白位置,能夠添加,這裏須要注意的是若是check[cur]=0,則base[cur]=0成立 if( check[cur] == 0){ check[cur]=pre; base[cur]=-tailPos; toTail(word,i+1); //把剩下的字符串存儲到tail數組中 return;//當前詞已經插入到DATire中 } //公共前綴,直接走 if(check[cur]==pre && base[cur]>0 ){ pre=cur;continue; } //遇到壓縮到tail中的字符串,有多是公共前綴 if(check[cur] == pre && base[cur]<0 ){ //是公共前綴,把前綴解放出來 int new_base_value,head; head=-base[cur]; //插入相同的字符串 if(tail[head]==END_TAG && word.charAt(i+1)== END_TAG) return ; if(tail[head]==word.charAt(i+1)){ int ncode=GetCode(word.charAt(i+1)); new_base_value=x_check(new Integer[]{ncode}); //解放當前結點 base[cur]=new_base_value; //鏈接到新的結點 base[new_base_value+ncode]=-(head+1); check[new_base_value+ncode]=cur; //把邊推向前一步,繼續 pre=cur;continue; } /* * 兩個字符不相等,這裏須要注意"一個串是另外一個串的子串的狀況" * */ int tailH=GetCode(tail[head]),nextW=GetCode(word.charAt(i+1)); new_base_value=x_check(new Integer[]{tailH,nextW}); base[cur]=new_base_value; //肯定父子關係 check[new_base_value+tailH] = cur; check[new_base_value+nextW] = cur; //處理原來tail的首字符 base[new_base_value+tailH] = (tail[head] == END_TAG) ? 0 : -(head+1); //處理新加進來的單詞後綴 base[new_base_value+nextW] = (word.charAt(i+1) == END_TAG) ? 0 : -tailPos; toTail(word,i+2); return; } /* * 衝突:當前結點已經被佔用,須要調整pre的base * 這裏也就是整個DATrie最複雜的地方了 * */ if(check[cur] != pre){ int adjustBase=pre; Integer[] list=GetAllChild(pre);//父結點的全部孩子 Integer[] tmp=GetAllChild(check[cur]);//產衝突結點的全部孩子 int new_base_value; if(tmp!=null && tmp.length<=list.length+1){ list=tmp;tmp=null; adjustBase=check[cur]; new_base_value=x_check(list); }else{ //因爲當前字符也是結點的孩子,因此須要把當前字符加上 list=Arrays.copyOf(list, list.length+1); list[list.length-1]=GetCode(word.charAt(i)); new_base_value=x_check(list); //可是當前字符 如今並非他的孩子,因此暫時先須要去掉 list=Arrays.copyOf(list, list.length-1); } int old_base_value=base[adjustBase]; base[adjustBase]=new_base_value; int old_pos,new_pos; //處理全部節點的衝突 for(int j=0;j<list.length;j++){ old_pos=old_base_value+list[j]; new_pos=new_base_value+list[j]; /* * if(old_pos==pre)pre=new_pos; * 這句代碼差很少花了我3天的時間,纔想出來 * 其間,反覆看論文,理解DATrie樹的操做過程。 * 動手在紙上畫分析DATrie可能的結構。最後找到 * 樣例:"ba","bac","be","bae" 解決問題 * */ if(old_pos==pre)pre=new_pos; //把原來老結點的信息遷移到新節點上 base[new_pos]=base[old_pos]; check[new_pos]=check[old_pos]; //有後續,全部孩子都用新的父親替代原來的父親 if(base[old_pos]>0){ tmp=GetAllChild(old_pos); for (int k = 0; k < tmp.length; k++) { check[base[old_pos]+tmp[k]] = new_pos; } } //釋放廢棄的節點空間 base[old_pos]=0; check[old_pos]=0; } //衝突處理完畢,把新的單詞插入到DATrie中 cur=base[pre]+GetCode(word.charAt(i)); if(check[cur]!=0){ System.err.println("collision exists~!"); } base[cur]=(word.charAt(i)==END_TAG)?0:-tailPos; check[cur]=pre; toTail(word,i+1);return;//這裏不能忘記了 } } } //到Tail數組中進行比較 private boolean MatchInTail(String word,int start,int head){ word+=END_TAG; while(start<word.length()){ if(word.charAt(start++)!=tail[head++])return false; } return true; } /* * 尋找最小的q,q要知足的條件是:q>0 ,而且對於list中全部的元素都有check[q+c]=0 * */ private int x_check(Integer[] c){ int cur,q=1,i=0; do{ cur = q + c[i++]; if(cur >= check.length) extend(); if(check[cur] != 0 ){ i=0;++q; } }while(i<c.length); return q; } //尋找一個節點的全部子元素 private Integer[] GetAllChild(int pos){ if(base[pos]<0)return null; ArrayList<Integer> c=new ArrayList<Integer>(); for(int i=1;i<=Hash.size();i++){ if(base[pos] + i >= check.length)break; if(check[base[pos]+i] == pos)c.add(i); } return c.toArray(new Integer[c.size()]); } public Integer GetCode(char ch){ return Hash.GetCode(ch); } //將字符串的後綴存儲到tail數組中 private void toTail(String w,int pos){ //若是容量不足,就擴容 if(tail.length-tailPos < w.length()-pos) tail=Arrays.copyOf(tail, tail.length<<1); while(pos<w.length()){ tail[tailPos++]=w.charAt(pos++); } } private void extend(){ base=Arrays.copyOf(base, base.length<<1); check=Arrays.copyOf(check, check.length<<1); } }
Hash.java
package com.vancl.dic; import java.util.HashMap; public class Hash { private static final HashMap<Character,Integer> hash= new HashMap<Character,Integer>(); public static int GetCode(char ch){ if(!hash.containsKey(ch)){ hash.put(ch, hash.size()+1); } return hash.get(ch); } public static void put(char ch,int value){ hash.put(ch, value); } public static int size(){ return hash.size(); } }
Test程序:
package com.vancl.dic; import junit.framework.Assert; import org.junit.Test; public class DATrieTest { @Test public void testInsert(){ String[] s={"bachelor","jar","badge","baby"}; String[] s2={"ba","bac","be","bae"}; DATrie dat=new DATrie(); for (String string : s2) { dat.insert(string); } for (String string : s2) { Assert.assertEquals(true, dat.retrieval(string)); } } }
參考文檔:
《An effcient Implements of Trie Structures》
http://blog.csdn.net/dingyaguang117/article/details/7608568
http://www.iteye.com/topic/391892
因爲文章是從word中粘貼出來的,排版效果很難看,這裏我轉成了pdf,能夠在附件中下載。