1. 程式人生 > >雙陣列trie樹的基本構造及簡單優化(DAT沒那麼複雜)

雙陣列trie樹的基本構造及簡單優化(DAT沒那麼複雜)

一 基本構造

Trie樹是搜尋樹的一種,來自英文單詞"Retrieval"的簡寫,可以建立有效的資料檢索組織結構,是中文匹配分詞演算法中詞典的一種常見實現。它本質上是一個確定的有限狀態自動機(DFA),每個節點代表自動機的一個狀態。在詞典中這此狀態包括“詞字首”,“已成詞”等。

雙陣列Trie(double array Trie)是trie樹的一個簡單而有效的實現,由兩個整數陣列構成,一個是base[],另一個是check[]。設陣列下標為i ,如果base[i],check[i]均為0,表示該位置為空。如果base[i]為負值,表示該狀態為詞語。check[i]表示該狀態的前一狀態, t=base[i]+a, check[t]=i 。

下面舉例來說明用雙陣列Trie構造分詞演算法詞典的過程。假定詞表中只有“啊,阿根廷,阿膠,阿拉伯,阿拉伯人,埃及”這幾個詞,用Trie樹可以表示為: 

 

我們首先對詞表中所有出現的10個漢字進行編碼:
啊-1,阿-2,唉-3,根-4,膠-5,拉-6,及-7,廷-8,伯-9,人-10。
對於每一個漢字,需要確定一個base值,使得對於所有以該漢字開頭的詞,在雙陣列中都能放下。例如,現在要確定“阿”字的base值,假設以“阿”開頭的詞的第二個字序列碼依次為a1,a2,a3,…,an,我們必須找到一個值i,使得base[i+a1],check[i+a1],base[i+a2], check[i+a2],…,base[i+an],check[i+an]均為0。一旦找到了這個i,“阿”的base值就確定為i。用這種方法構建雙陣列Trie,經過四次遍歷,將所有的詞語放入雙陣列中,然後還要遍歷一遍詞表,修改base值。因為我們用負的base值表示該位置為詞語。如果狀態i對應某一個詞,而且Base[i]=0,那麼令Base[i]=(-1)*i,如果Base[i]的值不是0,那麼令Base[i]=(-1)*Base [i]。得到雙陣列如下:

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

Base

-1

4

4

0

0

0

0

4

-9

4

-11

-12

-4

-14

Check

0

0

0

0

0

0

0

2

2

2

3

8

10

13

詞綴

阿根

阿膠

阿拉

埃及

阿根廷

阿拉伯

阿拉伯人

用上述方法生成的雙陣列,將“啊”,“阿”,“埃”,“阿根”,“阿拉”,“阿膠”,“埃及”,“阿拉伯”,“阿拉伯人”,“阿根廷”均視為狀態。每個狀態均對應於陣列的一個下標。例如設“阿根”的下標為i=8,那麼check[i]的內容是“阿”的下標,而base[i]是“阿根廷”的下標的基值。“廷”的序列碼為x=8,那麼“阿根廷”的下標為base[i]+x=base[8]+8=12。

二 基本操作與存在問題

1 查詢
trie樹的查詢過程其實就是一個DFA的狀態轉移過程,在雙陣列中實現起來比較簡單:只需按照狀態標誌進行狀態轉移即可.例如查詢“阿根廷”,先根據 “阿”的序列碼b=2,找到狀態“阿”的下標2,再根據“根”的序列碼d=4找到“阿根”的下標base[b]+d=8,同時根據check[base [b]+d]=b,表明“阿根”是某個詞的一部分,可以繼續查詢。然後再找到狀態“阿根廷”。它的下標為y=12,此時base[y]<0。

查詢過程中我們可以看到,對於一個詞語的查詢時間是隻與它的長度相關的,也就是說它的時間複雜度為O(1).在漢語中,詞語以單字詞,雙字詞居多,超過三字的詞語少之又少.因此,用雙陣列構建的trie樹詞典查詢是理論上中文機械分詞中的最快實現。

2 插入與刪除
雙陣列的缺點在於:構造調整過程中,每個狀態都依賴於其他狀態,所以當在詞典中插入或刪除詞語的時候,往往需要對雙陣列結構進行全域性調整,靈活效能較差。

將一個詞語插入原有的雙陣列trie樹中,相當於對DFA增加一個狀態。首先我們應根據查詢方法找出該狀態本應所處的位置,如果該位置為空,那好辦,直接插入即可。如果該位置不為空。那麼我們只好按照構造時一樣的方法重新掃描得出該狀態已存在的最大字首狀態的BASE值,並由此依次得出該狀態後繼結點的BASE值。在這其中還要注意CHECK值的相應變化。

例如說,如果“阿拉根”某一天也成為了一個詞,我們要在trie樹中插入這一狀態。按計算它的位置應在8,但8是一個已成狀態.所以我們得重新確定 “阿拉”這一最大已成字首狀態的BASE值.重新掃描得出BASE[10]=11。這樣狀態15為“阿拉根”,且BASE[15]為負(成詞), CHECK[15]=10;狀態20為“阿拉佰”,且BASE[20]=-4,CHECK=10。

這樣的處理其實是非常耗時間的,因為得依次對每一個可能BASE值進行掃描來進行確定最大已成字首狀態的BASE值。這個確定過程在構造時還是基本可以忍受的,畢竟你就算用上一,兩天來構造也沒有問題(只要你構造完後可以在效執行即可)。但在插入比較頻繁時,如果每次都需要那麼長的執行時間,那確實是無法忍受的。

雙陣列刪除實現比較簡單,只需要將刪除詞語的對應狀態設為空即可――即BASE值,CHECK均為設0。但它存在存在一個空間效率的問題.例如,當我們在上面刪除“埃及”這一詞語時,狀態11被設為空。而狀態10則成了一個無用結點――它不成詞,而且在插入新詞時也不可重用。所以,隨著刪除的進行, 空狀態點和無用狀態點不斷增多,空間的利用率會不斷的降低。

三 簡單優化

優化的基本思路是將雙陣列trie樹構建為一種動態檢索方法,從而解決插入和刪除所存在的問題。

1 插入優化
在插入需要確定新的BASE值時,我們是隻需要遍歷空狀態的。非空狀態的出現意味著某個BASE值嘗試的打敗,我們可以完全不必理會。所以,我們可以對所有的空狀態構建一個序列,在確定BASE值時只需要掃描該序列即可。
對雙陣列中的空狀態的遞增結點r1,r2, …, rm,我們可以這樣構建這一空序列:
CHECK[ri]=−ri+1 (1 i m−1),
CHECK[rm]=−(DA_SIZE+1)
其中r1= E_HEAD,為第一個空值狀態對應的索引點。這樣我們在確定BASE值時只需掃描這一序列即可。這樣就省去了對非空狀態的訪問時間。

這種方法在空狀態並不太多的情況下可以很大程度的提高插入速度。

2 刪除優化
1) 無用結點
對於刪除葉結點時產生的無用結點,可以通過依次判斷將它們置為空,使得可在插入新詞時得以重用。例如,如果我們刪除了上例中的"阿根廷",可以看到"阿根"這一狀態沒有子狀態,因此也可將它置為空。而"阿"這一狀態不能置空,因為它還有兩個子狀態。

2) 陣列長度的壓縮
在刪除了一個狀態後,陣列末尾可能出現的連續空狀態我們是可以直接刪除的。另外我們還可以重新為最大非空索引點的狀態重新確定BASE值,因為它有可能已經由於刪除的進行而變小。這們我們可能又得以刪除一些空值狀態。