《An Efficient Implementation of Trie Structures 》論文解讀

<因爲本文從word中粘貼過來,故排版比較亂,可下載附件中的pdf版>java

引言node

在許多的信息檢索應用中,不少地方都須要之前綴匹配的方式來檢索輸入的字符串。好比:編譯器的詞法分析、目錄檢索、拼寫檢查、中文分詞的詞庫等天然語言處理相關的應用。爲了提升檢索的效率,咱們一般把字符串構建成Trie樹的形式。Trie樹的每一個結點是一個數組,數組中存儲着下一個結點的索引信息。如對K=set{baby,bachelor,badge,jar}創建Trie樹,結構以下圖:編程

101213309.jpg

從上圖中能夠看出,基於Trie樹對字符串進行檢索、刪除、插入是很是快的,可是空間開銷卻隨着字符種類數和結點個數成比例增加,並且這種增長是成指數的增加。爲了解決空間閒置問題,咱們最容易想到的壓縮方法就是把Trie樹結點中的數組換成鏈表,這樣就避免了數組中出現大量的NULL值狀況,通俗地說,就是用左孩子右兄弟的方式來維持Trie樹,以下圖:數組

spacer.gif101234620.jpg

可是這樣卻帶來了檢索效率的下降,特別是一個結點有多個子結點的時候,例如,圖中結點b的孩子結點a{c,b,d}三個孩子,這樣在檢索badge的時候,就須要遍歷結點b-a-c-b-d-g-e(如上圖紅色的線條。)才能檢索到。數據結構

本文提出了一種新的壓縮方法,把Trie樹壓縮到兩個一維數組BASECHECK中,數組BASECHECK合稱Double-Array。在Double-Array中,非空的結點n經過BASE數組映射到CHECK數組中。也就是說,沒有任何兩個非空的結點會經過BASE數組映射到CHECK數組的同一個位置。Trie樹中的每條邊都能經過Double-Arrayo(1)的複雜度檢索到。換句話說:若是一個字符串的長度爲k,那麼最多隻須要o(k)的複雜度就能夠完成檢索。當字符串的數量衆多時,使Double-Array中的空間儘量充分利用就變得十分重要了。爲了使Trie樹可以知足存儲海量字符串的需求,Double-Array中只儲存字符串中的前綴,而後把字符串的剩餘部分存儲到字符數組中,咱們把這個字符數組稱爲tail數組。經過Double-Arraytail數組的組合,就達到了儘量節約內存,同時又能區別出任意兩個字符串的目的。dom



詳細解說DATrie(Double-Array Trie)ide

Trie是一種樹形的數據結構。Trie中從根結點到葉子結點的每條路徑表明着存儲在Trie中的一個字符串(key)。這也就是說,路徑中鏈接兩個結點的邊表明着一個字符。這跟咱們一般瞭解到的把字符信息儲存到結點中的樹是不同的。爲了不相似於the then這樣的字符串在Trie中形成混淆,咱們引入一個特別的字符# 用來表示每一個字符串的結束。這樣插入thethenTrie中時,其實是插入the# then# 函數

爲了更清晰地解說Trie,咱們做以下的定義:性能

K 表明造成Trie的字符串集合。Trie結點和鏈接結點的邊(arc組成。結點由Double-Array的下標來標記,邊則是由字符來標記,若是一條邊從結點n到結點m被標記成a,那麼咱們能夠定義以下的函數g(n,a)=m 學習

對於集合K中的一個字符串STrie中造成的一條路徑P,若是路徑P中有結點m知足g(n,a)=m ,使得在Trie中檢索S時,檢索到字符a就已經可以將字符串STrie中的其它字符串區別開來,那麼結點m稱爲separate node 。例如Figure 3中結點15、結點5、結點6、結點4都是separatenode

101332986.jpg

separatenode到葉子結點之間的字符串稱之爲結點m single string.STR[m]表示。例如圖Figure3str[5]str[6]都是single string.S中刪除singlestring後剩餘的部分稱爲tail . (這裏我沒沒理解清楚,只是以爲tailsingle string是一個意思,就是STR[m])樹中只由從根結點到separatenode 之間的邊組成的部分稱爲reduced trieFigure3就是reducedtrie的一個例子,用Double-Array和字符數組來存儲tail信息。TAIL數組中的問號標誌?用來表示廢棄的空間(原來存儲過信息,後來該位置不用了)。關於Double-Aarray TAIL的用法將會在插入操做和刪除操做中詳細解釋。

Figure3 中,Double-Arrayreducedtrie 的關係以下:(到這裏就很容易理解了,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、若是結點mseprate 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中結點5BASE[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

步驟5BASE[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-arrayCHECK數組的最大長度(BASE數組與CHECK數組大小相等),而且BASE數組和CHECK數組的長度能夠動態地增長,數組默認以0來填充。






Case 1 : 插入字符串時Trie樹爲空。

101424177.jpg

(樹爲空時,BASE[1]=1,CHECK[1]=0, TAIL數組的起始位置POS=1; 這其實也就是編碼時Trie的初始化。)

插入單詞bachelor#將按以下步驟進行;

步驟1:從double-arrayBASE數組的下標1開始(也就是樹的根結點)b的值爲3,因此

BASE[1]+b=1+3=4, CHECK[4]=0≠1

步驟2CHECK[4]=0表示結點4separate 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的狀態。

101447834.jpg



Case 2: 插入字符串時無衝突。

按以下的步驟插入單詞jar#

步驟1:從double-arrayBASE數組下標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 1Case 2的實現很是簡單,也無需糾結於此。咱們可認爲這是做者玩的文字遊戲)插入jar#reduced triedouble-array的狀態如圖Figure 6所示:

101509449.jpg

爲了研究Case 3Case 4兩種狀況,咱們須要定義一個函數X_CHECK(LIST)其功能是返回一個最小的整數qq知足以下條件: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#。當兩個字符串的比較失敗,就用步驟456的方式把他們的公共前綴插入到double-array中。

步驟4:申明一個臨時變量TEMP,並把-BASE[1]保存到這個臨時變量中

TEMP à -BASE[1]

步驟5:計算字符串achelor#adge#的公共前綴字符aX_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沒有重複。可是若是有多個公共前綴,步驟56會重複執行屢次,直到公共前綴都處理完。

步驟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#的首字符做爲參數,計算字符串在BASECHECK數組中的結點編號。經過該結點能夠在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)

101549544.jpg

Case 4:搶佔位置引起的衝突

(如今進入到整篇文章最核心的地方了,也就是DATrie最難的地方)

就像Case 3 同樣,BASE數組中的值必須進行修改才能解決衝突。插入」baby#」的步驟演示以下:

步驟1:根結點在BASE數組的下標1位置,因此從BASE[1]開始。

對於baby#中前三個字符而言,BASECHECK中的值以下;

BASE[1]+’b’=1+3=4, CHECK[4]=1

BASE[4]+’a’=1+2=3, CHECK[3]=4

BASE[3]+’b’=1+3=4, CHECK[4]=1≠3

CHECK[4]的計算結果出現先後不一致的現象代表有一個狀態沒有被考慮到。這也意味着結點1或者結點3BASE值須要進行修改。(這裏能夠這樣理解:結點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]),那麼咱們就修改結點3BASE值,可是因爲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

CHECK[4+3]=CHECK[7]=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,原來的子結點爲412BASE值更新後,子結點隨之變成715,把原來結點4和結佔12BASECHECK值轉移到結點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值,對於這兩個父結點(對應到程序中就是preCHECK[cur],具體修改哪個取決於其子結點的個數,子結點個數少的父結點將被修改。這樣衝突就能夠獲得解決,字符串也能順利地插入到trie中了。插入後的結果如Figure 3所示:


101620290.jpg

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,能夠在附件中下載。

相關文章
相關標籤/搜索