1. 程式人生 > >trie樹(字首樹)

trie樹(字首樹)

Trie 樹, 又稱字典樹,單詞查詢樹。它來源於retrieval(檢索)中取中間四個字元構成(讀音同try)。用於儲存大量的字串以便支援快速模式匹配。主要應用在資訊檢索領域。

Trie 有三種結構: 標準trie (standard trie)、壓縮trie、字尾trie(suffix trie) 。這裡只將前兩種。

1. 標準Trie (standard trie)

標準 Trie樹的結構 

 所有含有公共字首的字串將掛在樹中同一個結點下。實際上trie簡明的儲存了存在於串集合中的所有公共字首。 假如有這樣一個字串集合X{bear,bell,bid,bull,buy,sell,stock,stop}。它的標準Trie樹如下圖:

上圖我們可以很清楚的看到字串集合X構造的Trie樹結構。其中從根結點到紅色方框葉子節點所經歷的所有字元組成的串就是字串集合X中的一個串。

注意這裡有一個問題: 如果X集合中有一個串是另一個串的字首呢? 比如,X集合中加入串bi。那麼上圖的Trie樹在綠色箭頭所指的內部結點i 就應該也標記成紅色方形結點。這樣話,一棵樹的枝幹上將出現兩個連續的葉子結點(這是不合常理的)。

也就是說字串集合X中不存在一個串是另外一個串的字首 。如何滿足這個要求呢?我們可以在X中的每個串後面加入一個特殊字元$(這個字元將不會出現在字母表中)。這樣,集合X{bear$、bell$、.... bi$、bid$}一定會滿足這個要求。

      總結:一個儲存長度為n,來自大小為d的字母表中s個串的集合X的標準trie具有性質如下:

      (1) 樹中每個內部結點至多有d個子結點。

      (2) 樹有s個外部結點。

      (3) 樹的高度等於X中最長串的長度。

      (4) 樹中的結點數為O(n)。

標準 Trie樹的查詢

       對於英文單詞的查詢,我們完全可以在內部結點中建立26個元素組成的指標陣列。如果要查詢a,只需要在內部節點的指標陣列中找第0個指標即可(b=第1個指標,隨機定位)。時間複雜度為O(1)。

      查詢過程:假如我們要在上面那棵Trie中查詢字串bull (b-u-l-l)。

      (1) 在root結點中查詢第('b'-'a'=1)號孩子指標,發現該指標不為空,則定位到第1號孩子結點處——b結點。

      (2) 在b結點中查詢第('u'-'a'=20)號孩子指標,發現該指標不為空,則定位到第20號孩子結點處——u結點。

      (3) ... 一直查詢到葉子結點出現特殊字元'$'位置,表示找到了bull字串

如果在查詢過程中終止於內部結點,則表示沒有找到待查詢字串。

      效率:對於有n個英文字母的串來說,在內部結點中定位指標所需要花費O(d)時間,d為字母表的大小,英文為26。由於在上面的演算法中內部結點指標定位使用了陣列隨機儲存方式,因此時間複雜度降為了O(1)。但是如果是中文字,下面在實際應用中會提到。因此我們在這裡還是用O(d)。 查詢成功的時候恰好走了一條從根結點到葉子結點的路徑。因此時間複雜度為O(d*n)。

      但是,當查詢集合X中所有字串兩兩都不共享字首時,trie中出現最壞情況。除根之外,所有內部結點都自由一個子結點。此時的查詢時間複雜度蛻化為O(d*(n^2))

中文詞語的標準Trie樹

      由於中文的字遠比英文的26個字母多的多。因此對於trie樹的內部結點,不可能用一個26的陣列來儲存指標。如果每個結點都開闢幾萬個中國字的指標空間。估計記憶體要爆了,就連磁碟也消耗很大。

      一般我們採取這樣種措施:

     (1) 以詞語中相同的第一個字為根組成一棵樹。這樣的話,一箇中文詞彙的集合就可以構成一片Trie森林。這篇森林都儲存在磁碟上。森林的root中的字和root所在磁碟的位置都記錄在一張以Unicode碼值排序的有序字表中。字表可以存放在記憶體裡。

    (2) 內部結點的指標用可變長陣列儲存。

     特點:由於中文詞語很少操作4個字的,因此Trie樹的高度不長。查詢的時間主要耗費在內部結點指標的查詢。因此將這項指向字的指標按照字的Unicode碼值排序,然後載入進記憶體以後通過二分查詢能夠提高效率。

標準Trie樹的應用和優缺點

     (1) 全字匹配:確定待查字串是否與集合的一個單詞完全匹配。如上程式碼fullMatch()。

     (2) 字首匹配:查詢集合中與以s為字首的所有串。

     注意:Trie樹的結構並不適合用來查詢子串。這一點和前面提到的PAT Tree以及後面專門要提到的Suffix Tree的作用有很大不同。

      優點: 查詢效率比與集合中的每一個字串做匹配的效率要高很多。在o(m)時間內搜尋一個長度為m的字串s是否在字典裡。

      缺點:標準Trie的空間利用率不高,可能存在大量結點中只有一個子結點,這樣的結點絕對是一種浪費。正是這個原因,才迅速推動了下面所講的壓縮trie的開發。

2.壓縮trie樹 (compressed trie)

壓縮Trie類似於標準Trie,但它能保證trie中的每個內部結點至少有兩個子節點(根結點除外)。通過把單子結點鏈壓縮排葉子節點來執行這個規則。

壓縮Trie的定義

      冗餘結點(redundant node):如果T的一個非根內部結點v只有一個子結點,那麼我們稱v是冗餘的。

      冗餘鏈(redundant link):如上標準Trie圖中,內部結點e只有一個內部子結點l,而l也只有一個葉子結點。那麼e-l-l就構成了一條冗餘鏈。

      壓縮(compressed):對於冗餘鏈 v1- v2- v3- ... -vn,我們可以用單邊v1-vn來替代。

對上面標準Trie的圖壓縮之後,形成了Compressed Trie的字元表示圖如下:

壓縮Trie的性質和優勢

與標準Trie比較,壓縮Trie的結點數與串的個數成正比了,而不是與串的總長度成正比。一棵儲存來自大小為d的字母表中的s個串的結合T的壓縮trie具有如下性質:

     (1) T中的每個內部結點至少有兩個子結點,至多有d個子結點。

     (2) T有s個外部結點。

     (3) T中的結點數為O(s)

儲存空間從標準Trie的O(n)降低到壓縮後的O(s),其中n為集合T中總字串長度,s為T中的字串個數。

壓縮Trie的壓縮表示

     上面的圖是壓縮Trie的字串表示。相比標準Trie而言,確實少了不少結點。但是細心的讀者會發現,葉子結點中的字元數量增加了,比如結點ell,那麼這種壓縮空間的效率當然會打折扣了。那麼有什麼好辦法呢,這裡我們介紹一種壓縮表示方法。即把所有結點中的字串用三元組的形式表示如下圖:

      其中三元組(i,j,k)表示S[i]的從第j個位置到第k個位置間的子串。比如(5,1,3,)表示S[5][1...3]="ell"。即第5個字串中第1位起到底3位止的子串。

      這種壓縮表示的一個巨大的優點就是:無論結點需要儲存多長的字串,全部都可以用一個三元組表示,而且三元組所佔的空間是固定有限的。但是為了做到這一點,必須有一張輔助索引結構。(如上圖右側s0—s7所示)。