原文:http://blog.csdn.net/zzran/article/details/8462002node
概論數組
下面將呈現一種新的內部數組結構,它即是double-array。double-array繼承了數組訪問快速的特性和鏈表結構緊密的特色。對於double-array的插入,查找和刪除將會經過實例來給出解析。雖然插入的過程很慢,可是仍是很實用的,對於查找和刪除,因爲double-array繼承了鏈表的特性,因此很速度。在操做大量關鍵的時候,咱們把double-array和list形式(也就是原始trie的鏈表的形式)進行比較,會獲得以下結果:double-array佔用的空間比trie以鏈表的形式存儲節省了百分之十七的空間,同時double-array的遍歷,也就是查找的速度會比鏈表的形式快3.1到5.1倍。函數
簡介編碼
在不少檢索的應用中,有必要採用trie樹的形式來檢索單詞。例如:編譯器的單詞分析器,拼寫檢查器,參考書目的檢索, 在天然語言處理中的形態字典,等等。看到這裏,是否是以爲trie是一個很強大的數據結果。對於trie樹,例如它的節點是下面這樣的幾個struct形式: struct node {char data, struct node next[26]},這是最多見的trie節點形式。它是array-structured。對於next數組的索引index是由一個單詞中data所存儲字母的下一個字母來決定的。next[index]所指示或者是一個新的trie節點,或者是一個NULL值。圖一給出了一個這樣形式的trie樹,它是基於關鍵字數組K = {baby, bachelor, badge, jar}創建的。trie樹的檢索,插入,刪除都很快,可是它佔用了很大的內存空間,並且空間的複雜度是基於節點的個數和字符的個數。若是是純單詞,並且兼顧大小寫的話,每一個節點就要分配52*4的內存空間,耗費很大。一種不少人都知道壓縮空間的策略就是列出從每一個節點引伸出來的邊,並以空值結尾。圖二基於list-structured形式的trie。這種鏈表的形式經過對於數組形式中NULL值的壓縮來節省空間,也就說指向子節點的指針是以鏈表的形式來存放而不是數組的形式。可是若是從每一個節點引伸出不少邊的話,檢索的速度會很慢。spa
接下來這篇文章會講解它trie樹的結構壓縮到兩個一位數組BASE和CHECK中去,這種結構叫作double-array。在double-array中,經過數組BASE,非空子節點的位置被映射到CHECK中去,同時,原來array-trie中,每一個節點的非空子節點的位置不會被映射到CHECK的相同位置中。trie樹中的每條邊均可以在double-array中以O(1)的時間檢索到,也就是說,在最壞的狀況下,檢索一個長度爲k的單詞只要O(k)的時間。對於擁有大量關鍵字的結合,trie樹種將會有不少節點,因此要用double-array的形式來達到減小空間分配的目的。爲了可以讓trie存儲大量的關鍵字,double-array儘量的根據須要存儲關鍵字的前綴來區分不一樣的單詞,除去前綴剩下的部分會被存儲到TAIL的一維數組中,以便更進一步的區分不一樣的單詞。.net
圖一指針
圖2blog
trie樹的構建繼承
關於trie的敘述以下,從根節點到葉子節點造成的每條路徑提取出來的字母組成的單詞表明瞭在關鍵字集合中的一個關鍵字,或者說,這個提取出來的關鍵字能夠在關鍵字集合中找到。全部的這些路徑表明的單詞加起來,就是關鍵字的結合。爲了避免混淆像「the」和「then」這樣的單詞,在每一個單詞的 後面多加一個結束符號:「#」。接下來的這些定義將會被用到插入,查找,刪除的步驟中去,因此不求可以先理解這些,只要可以記得怎麼用,在插入,查找,刪除的過程當中,就會逐漸明白這些定義的用意。K表明關鍵字集合。trie樹是由弧和節點主城的。若是一條弧上標記着a,(注意,在這裏a是表明子母集合中的某一個字母,而不是真正的a),那麼這個弧能夠這樣定義 g(n,a)=m,其中n,m是trie樹種節點的標號。解釋一下n,m。咱們用的是兩個一位數組來存儲的trie,因此n,m就是這兩個一位數組中的索引。表明了trie的兩個節點對於K中的一個關鍵字key,他的節點m知足關係g(n,a) = m,若是字母a是一個可以有效的將本身所屬的關鍵字和其餘關鍵字區分的字母的話,那麼m就是一個獨立的節點。怎麼理解這個呢,看下圖:索引
還記得以前說過的嗎,double-array儘可能存儲關鍵字的前綴來壓縮空間,可是還要可以把這些擁有公共前綴的關鍵字區分開,就要求這些關鍵字要有本身的特點,特點就是獨立於其餘關鍵字的節點。看上圖中的字母c,d,b,先說c吧,g(3,c) = 5,這條邊中m=5是一個節點,這個節點可以把bachelo這個單詞和其餘單詞區分開,那麼m=5就是一個獨立的節點。在這裏,話題要進行一個轉移,咱們想一下原始的trie,就是上面的那個struct形式的那個,它會把每一個單詞的每一個字母都以一個節點羅列出來,剛纔咱們找到了獨立節點(不包括獨立節點中的字母),那麼從原始trie中對應的這個節點開始到單詞最後一個字母所佔的節點結束,這些字母所組成的字符串叫作獨立節點m的字母串。也就是後面的內容都是由m銜接出來的。一棵樹,它是由K集合中全部關鍵字的獨立節點,和獨立節點以前的節點,以及這些節點之間的邊所組成的,那麼咱們就叫這棵樹爲reduced trie。
對於圖三,就是一個棵reduced trie。TAIL是用來存儲獨立節點以後的string的。對TAIL中的符號?如今不要在乎,等到咱們分析插入和刪除的時間就會用到,標記爲?,其實相應的內容不是?,而是一些由插入和刪除形成的無效字符。
reduced trie 和double-array,還有TAIL之間的關係以下:
1,若是在reduced trie中有一條這樣的邊:g(n, a) = m, then BASE[n] + a = m, and CHECK[m] = n.(在這個等式BASE[n] + a = m中,a表明的是一個字母,可是在相加的過程當中會把它替換成一個整數,由於這個字母表明的是一條邊,定義以下:「#」= 1, 「a」=2,..."z"= 27)。在實際的編碼中沒有這麼作,由於前面的定義只涉及到27個字符,實際應用中會涉及到更多的字符。
2,若是m是一個獨立的節點,那麼string str[m] = b1b2...bn。then (a), BASE[m] < 0, (b), let p = - BASE[m], TAIL[p]= b1, TAIL[p + 1]= b2, TAIL[p + h + 1]= bn.
在整個論文中,這兩個關係式是很重要的,因此請先用機械的方式把這兩個關係記錄下來,不要試圖去理解,在檢索,插入,和刪除的過程當中就會明白這樣定義的目的和巧妙之處了。
trie樹的檢索
先從檢索提及吧,建設咱們已經將K={bachelor, jar, badge, baby}中的關鍵字都處理過,放到reduced trie和TAIL中去了。以bachelor爲例子做爲檢索吧。仍是如下圖爲例。
step1:把trie樹的根節點放在BASE數組中第一個位置,而後從第一個位置開始,字母‘b’表明的弧的值爲3,在上面定義過,從上面的關係1中能夠獲得:BASE[n] + 'b' = BASE[1] + 'b' = BASE[1] + 3 = 4 + 3 = 7; 而後看CHECK[7]的值是1.
step2,:由於在第一步中BASE[1] + 'b' 獲得的值是正數,若是不是正數,那它的值就表明獨立節點後字符串在TAIL中存儲的起始位置。是正數,繼續進行。把獲得的值7做爲BASE數組的新索引,字母‘a’表明的弧的值爲2,因此:BASE[7]+'a'=BASE[7] + 2 = 3 and CHECK[3]=7.先解釋CHECK[3]=7吧,它表示的是指向節點3的弧是順從節點7出發的。
step3,4: 'c'表明的弧值是4, step2中求得的3做爲BASE的新索引,BASE[3]+4 = 5, CHECK[5]=3,
step5,再看上面的數組,獲得BASE[5]=-1,這個負數代表,bachelor#中剩下的字母存儲在TAIL中,TAIL[-BASE[5]]=1,從索引1開始的。K中的其餘關鍵字能夠用一樣的方式檢索。不過開始位置都是BASE數組的第一個位置-position 1.
從上面的檢索步驟咱們能夠看到,檢索的過程當中咱們只是作了簡單的讀取數組中的值而後和其餘值進行相加,沒有進行整整的查找。因此這種reduced trie的實現,對於檢索來講效率至關高。
關鍵字的插入
在插入以前首先要作的事情就是初始化,BASE[1] = 1, 除此以外,BASE和CHECK中的其餘數值都設置爲0;0表示這些節點尚未被使用。
1,當double-array都是空的時候,即BASE和CHECK中存儲的元素都是0的時候。
2,插入新的關鍵字的時候沒有發生碰撞。
3,插入新關鍵字的時候發生了碰撞,發生這種碰撞的有兩種緣由,第一種緣由就是由於兩個關鍵字有相同的前綴,解決的方法是爲這些前綴包含的單詞都建立一個節點,並把對應的節點與邊之間的關係寫入到BASE,CHECK當中去。同時還對TAIL進行了操做,由於要提取TAIL中的字母。對於BASE和CHECK在發生碰撞以前原有的內容不作改變。
4,插入新關鍵字的時候發生碰撞,發生這種碰撞的緣由不是由於單詞之間有公共前綴,而是由於插入過程當中某個關鍵字字母經過計算即將存放它所表明的弧的弧頭節點已經被其餘關鍵字的某個字母表明的邊的弧頭節點所佔用。
在插入以前着重的給你們講解BASE和CHECK的概念:BASE中若是存儲的是正數,表示的是一個基數,什麼基數呢,假設有兩個節點n,m同屬於一條邊a,知道邊a的弧尾節點,也就是非箭頭指向的節點,知道這個邊表明的值,好比是2,那麼怎麼求另外一個節點是誰呢,那就須要BASE[n]+2=m,m即是這個節點。若是BASE中存儲的是負數呢,那就表明了一個關鍵字除了被邊表示的字母以外,其餘的字母都被存儲在TAIL數組中,BASE[n]的絕對值就表明存儲位置的開始。CHECK呢,CHECK表示的是當前索引指示的節點有沒有被其餘邊做爲弧頭節點或者弧尾節點來使用,若是爲0表示沒有,若是爲正數,表示有,同時這個正數也表示了是從哪一個節點出發的弧指向了當前節點。
第一種狀況:double-array都是空的狀況,插入bachelor#步驟以下:
step1,在BASE數組的第一個節點開始,‘b’所表明的邊的值是3,有:BASE[1]+‘b’= BASE[1] + 3 = 4, and CHECK[4]= 0 != 1, 這說明什麼呢,說明尚未弧指向第四個節點,那麼咱們能夠把'b'表明的這條邊指向第四個節點。
step2,CHECK[4]=0同時也表示着achelor#將被放在TAIL數組中去,而後定義'b'表明的邊,g(1,'b')=4.
step3, 置BASE[4]= -POS = -1,這表示着bachelor#除了咱們已經定義的邊b以外,其餘的字母被放到了TAIL數組中去了,起始位置是POS。同時CHECK[4]=1,表示指向節點4的邊是從節點1發射出來的。
step4,POS <---9,表示下次能夠出入的位置,算一下achelor#長度是8,則下次有效插入位置將是9.
下圖顯示了插入bachelor以後double-array和TAIL。
第二種狀況:插入jar#
step1,在BASE的第一個位置開始,‘j’表明邊的值是11,因此:BASE[1]+'j'=BASE[1]+11 = 12, and CHECK[12]=0 != 1,
step2,CHECK[12]=0表示了jar#中的其餘部分要被插入到TAIL中去,同時也表示了插入的過程當中是沒有發生碰撞的,即存在公共前綴或者計算出來的節點已經被佔用。從POS=9的位置開始,將ar#存入到TAIL中去。
step3,置BASE[12]= -9,CHECK[12]=1,表示從節點1出發的弧‘j’的尾部節點是第12個點。
step4, POS= 12,下一個有效插入位置。
從上面的兩種插入狀況來看,插入的過程並無明顯的區別,他們只是再理論上有所不一樣,便是不是在double-array爲空的時候插入的。還有形成他們插入操做相同的緣由是沒有發生碰撞。
在講述第三種狀況以前,先要說一個概念,考慮有這樣一個函數 X_CHECK(LIST), 它返回最小的q,q知足如下兩個條件:q>0 and 對於在LIST中的全部字母c都知足:CHECK[q + c] = 0。q的值老是從1開始,而且每次只增值1。記住這個重要的條件,就是q要知足LIST中的全部字母。
第三種狀況,插入badge#:
step1,從BASE數組的第一位開始,‘b’表明的邊的取值爲3, 有:BASE[1]+'b'=4,and CHECK[4]=1,CHECK[4]中的非零值告訴咱們,存在一條這樣的邊:它的起始位置是CHECK[4]=1,結束位置是節點4.。也就是‘b’邊表明的弧尾節點和弧頭節點。
step2,因爲在第一步中找到的值是整數4,則要繼續進行下一個字母的尋找,4被用來當作BASE數組的新索引,BASE[4]=-1,說明搜索暫時中止,要進行字符串的比對,比對那些字符串呢,一個是badge#剩餘的沒有進行查找的部分,一個是存儲在TAIL數組中的部分,爲何要進行比對呢,比對的緣由很簡單,看這個關鍵字以前是否已經插入了,若是已經插入了,那麼badge#的再次插入是重複的,因此應該中止。在-BASE[4]=1的TAIL的起始位置找到字符串achelor#,而後和剩餘未插入的字符串adge#進行比較,比較的結果是不相同。可是細心的看一下,他們有相同的前綴,ba,b就不用說了,由於已經有一條邊用b表明了。那a呢,若是咱們貿然的將剩下的字符插入到TAIL數組中,會有什麼後果呢,後果就是BASE[4]中不知道該存儲哪一個值好,存儲-1吧badge#下次找的時候找不到,存儲-9吧,那下次找bachehlor#的時候就找不到了。解決的辦法是找到可以獨立表明兩個關鍵字的方法,那就是要除去他們的公共部分以後爲兩個關鍵字都創建一個獨立的節點,注意,這個獨立的節點咱們以前就提到過。
step3,把BASE[4]=-1存放到一個臨時變量中去,TEMP<---BASE[4]
step4,對adge#和achelor#的公共字符a使用X_CHECK[{'a'}]函數,CHECK[q+a]= CHECK[1+'a'] = CHECK[1+2]=CHECK[3]=0,這表示什麼呢,節點3尚未被那條邊當作弧頭或者弧尾使用,咱們能夠把它當作從節點4發射出來的邊‘a’的弧頭。q=1是BASE[4]的一個候選值,爲何說是候選值呢,等到後面就會理解了,暫時不用在乎。而後有:BASE[4]=1, CHECK[BASE[4]+'a']=CHECK[1+2]=CHECK[3]<-4。它表示了咱們又定義了一條新的邊'a',從節點4起,到節點3終止。注意到一點,由於這兩個字符串的公共前綴只有a,若是換作其餘字符串,不僅是一個公共前綴字母,step3和step4就要循環操做。
step6,接下來這個比較複雜,用英語把,要否則會打亂邏輯: to store remaining string 'chelor#' and 'dge#', calculate the value to be store into BASE[3] for two arc labels 'c', and 'd', according to hte closest neighbour available by X_CHECK[{c,d}], 也就是說找到一個合適的q值,即BASE值,使得從節點3出發,加上這個值,獲得的另外兩個節點都沒有被使用過,均可以用來分別做爲弧c,d的弧頭。
FOR c : CHECK[q+'c']= CHECK[1+4]=CHECK[5]=0=>available
FOR d : CHECK[q+'d']= CHECK[1+5]=CHECK[6]=0=>available
獲得的q=1做爲BASE[3]的候選值,BASE[3]=1,要理解候選的意思,就是這不是最終的值。
step7, 接下來就是計算BASE和CHECK的值,以找到合適的節點和合適的TAIL中位置來存儲剩餘的chelor#,在上步驟中,已經找到了BASE[3]的值,有:
BASE[3]+c = 5, BASE[5]=-TEMP, CHECK[5]=3,想想,爲何這麼直接的就作了呢,由於節點3到節點5的弧c已經可以把當前的兩個有前綴的關鍵字區分開了,那麼剩餘的就沒有必要在細分了,直接放到TAIL數組中去了。同理,CHECK[5]=3表明從3出發,到5截止有一條弧c。
step8,剩餘的字符'helor#'要放到TAIL中去了其實位置是1,計算一下,是6個字符,全部TAIL[7],TAIL[8]中的位置存儲的字符就沒有意義了。爲何呢,由於以前咱們已經計算好了下一個有效的TAIL中的位置POS=9.
stpe9,對於'dge#'有一樣的處理:BASE[3]='d'= 1+ 5 = 6, BASE[6]= -POS=12 and CHECK[6]=3, store 'ge#' int TAIL starting at POS.
step10,計算下一個TAIL中的有效存儲位置:POS = 12 + length['ge#']= 15
插入badge#後的結果如圖所示:
插入的第四種狀況:插入'baby#':
step1: 仍是從BASE中的第一個節點開始,計算步驟以下:
BASE[1]+'b'= BASE[1]+3 = 4 and CHECK[4]=1,
BASE[4]+'a'= BASE[4]+2 = 3, and CHECK[3]=4,
BASE[3]+'b'= 4 and CHECK[4]=1 != 3,這是怎麼回事呢,讓我來清晰的解釋一下:由於baby#的前兩個字母是ba,按照規定,前綴都必須用單獨的邊表示,由於他們不足以區分有着相同前綴的不一樣單詞。因此接下來咱們還得給'by#'中的b創建一條邊,那麼b這條邊的起始節點有了,怎麼找它的終止節點呢,按照咱們當時的要求機械記憶的第一條BASE[3]+'b'=1+ 3=4,那麼咱們就要用節點4來當作這個'b'表明的邊,可是看一下前面的4已經被其餘節點徵用了。就產生了矛盾。那麼究竟是用4節點做爲當前邊'b'的截止節點呢,仍是它爲原來的邊貢獻。這要作一下pk吧。可是仍是尋找問題的根源吧,由於BASE[1]=1,BASE[3]等於,此次遇到了'b'差生了矛盾,那麼下次遇到其餘單詞中含有'b'也還有可能產生矛盾,那麼爲了根除這個矛盾,就得改變BASE[1]或者BASE[3]中的值,使得經過它中的值計算出來的可用節點不在發生衝突。剛纔說道pk,那麼怎麼pk呢,代價最小原則,看使用BASE[1]計算出來的在使用的節點個數多仍是使用BASE[3]計算出來的在使用的節點的個數多,計算的時候要包括即將插入的邊的弧尾節點。
step2:設定一個臨時變量TEMP_NODE1 <-BASE[3]+'b'= 4,
step3:把由節點3引伸出來的邊所表明的字母存放到LIST[3]中,很顯然有'c','d', 把由節點1引伸出來的邊表明的字母存在LIST[1],即:b,j。
stpe4:接下來就pk了,由於節點3剛纔要新引伸邊'b'來着,因此要加上,compare(length[LIST[3]]+1 , length[LIST[1]]) = compare(3,2) .從節點1,引伸出來的邊少,就改變BASE[1]的值,若是狀況是相反的,那麼就改變BASE[3]中的值。
step5:設定臨時變量:TEMP_BASE <-BASE[1]=1, and calculate a new BASE using LIST[1] according to the closest neighbour available as follows:
X_CHECK['b']=: CHECK[q+'b' ]= CHECK[1+3]= CHECK[4]!= 0
CHECK[q+'b' ]= CHECK[2+3]= CHECK[5]!= 0
CHECK[q+'b' ]= CHECK[3+3]= CHECK[6]!= 0
CHECK[q+'b' ]= CHECK[4+3]= CHECK[7]= 0
對於j X_CHECK['j']: CHECK[q+'j']= CHECK[4+11]= CHECK[15]=0=>available.
因此q=4是BASE[1]的候選值。BASE[1]=4
step6:store the value for the states to be modified in temporal variables: TEMP_NODE1 = TEMP_BASE+'b'=1+3 = 4, TEMP_NODE2= BASE[1]+'b'=7
把原來放在BASE[TEMP_NODE1]中的值放到BASE[TEMP_NODE2]中去,由於BASE[1]改變了,因此由BASE[1]計算出來的節點也要相應的作改變 BASE[TEMP_NODE2]=BASE[TEMP_NODE1]即:BASE[7]=BASE[4]=1,CHECK[TEMP_NODE2]=CHECK[4]=1
step7:BASE[TEMP_NODE1]=BASE[4]=1>0,說明什麼呢,說明原來由節點4引伸出去的邊不能在從節點4出發了,應該重新的節點,即節點7出發,因此要作改動:
CHECK[BASE[TEMP_NODE1]+E]=TEMP_NODE1, CHECK[BASE[4]+E]=CHECK[1+E]=4=>E = 2
and modify CHECK to point to new status:CHECK[BASAE[4]+2]=CHECK[3]<-TEMP_NODE2=7
step8:由於更換BASE[1]的值,咱們棄用了節點4,因此它將從新變爲一個下次插入時候可用的節點。CHECK[TEMP_NODE1]=0,BASE[TEMP_NODE1]=0
step9:for 'j',TEMP_NODE1<-TEMP_BASE+'j'= 1+11 = 12, TEMP_NODE2<-BASE[1]+'j'= 4+11= 15,
BASE[TEMP_NODE2]<-BASE[TEMP_NODE1] 即:BASE[15]=BASE[12]=-9 and SET the CHECK value for new node : CHECK[TEMP_NODE2]=CHECK[15]=CHECK[12]=1.
step10: BASE[TEMP_NODE1]= BASE[12]= -9,說明BASE中存儲的值是在TAIL中的有效存儲剩餘字符串的位置,因此能夠重置其值。 BASE[TEMP_NODE1]=BASE[12]=0,
CHECK[12]=0;
step11:繼續考慮引發衝突的節點3,咱們繼續進行插入,BASE[3]+'b'=4,and CHECK[4]=0,這回節點4能夠用了,有CHECK[4]=3,表示從節點3出發到節點4截止的 邊'b',那麼能夠很直觀的看出,到目前位置,這條邊足夠可以把baby#和其餘單詞區分開,則右BASE[4]=-15, TAIL[POS]= TAIL[15]= 'y#',
step12, 從新計算POS的有效值:POS+length['y#']= 17.
最終插入結果以下圖:
刪除關鍵字
關鍵字的刪除首先要找到double-array中是否有存儲此關鍵字。就像插入過程的case2那樣,只是操做有所不一樣,須要把對應關鍵字的獨立節點的BASE中存儲的指向TAIL數組中的有效位置清空,即變成0.同時CHECK也須要置爲0.表示指向獨立節點的邊被刪除。
下面以刪除‘badge#’爲例:
stpe1:從BASE數組的第一個位置開始,對‘badge’的前三個字節:
BASE[1]+'b'= BASE[1]+3= 4+3 = 7, and CHECK[7]=1
BASE[7]+'a'= BASE[7]+2 = 1+2 = 3, and CHECK[3]=7
BASE[3]+'d'= BASE[3]+ 5= 1+5 = 6, and CHECK[6]=3
BASE[6]=-12<0 ==> separate node, 獨立節點BASE中的值指示了剩餘字符串在TAIL中存儲的起始位置.
step2:將給定的字符串剩餘部分和TAIL中存儲的剩餘部分進行比較,compare('ge#', 'ge#').
step3: 兩個字符串的比較結果相等,因此重置指向TAIL的BASE[6],和去掉指向獨立節點的邊:BASE[6]<-0 , CHECK[6]<-0
因爲指向TAIL中'ge#'的獨立節點BASE的值置成了0, 那麼說明'ge#'再也沒有辦法被讀取了,便成了沒有用的內容:garbage,這些空間能夠供之後的插入字符使用。