雙陣列字典樹(Double Array Trie)

hgz_dm發表於2020-11-18

參考文獻

1.雙陣列字典樹(DATrie)詳解及實現

2.小白詳解Trie樹

3.論文《基於雙陣列Trie樹演算法的字典改進和實現》

 

        DAT的基本內容介紹這裡就不展開說了,從Trie過來的同學應該比較熟悉,Trie對記憶體的消耗比較大,DAT正是為了優化該問題而提出。此文重點說一下如何去理解DAT的base陣列和check陣列,希望能給諸位些幫助,DAT中定義base陣列、check陣列滿足的條件為:

        base[s] + c = t 

        check[t] = s    

這裡s指轉移前的狀態,c指字元的編碼,t指轉移後的狀態,下面逐個理解這兩個公式表示的邏輯。

1. base[s] + c = t

       理解這個式子時,不能單純的從陣列取值上去理解,比如直接將s當成base陣列的下標,因為 s 和 t 都是狀態,而 c 是字元編碼,此時就會疑惑,為啥等式兩邊輸出型別都不一致呢?這到底是怎麼個計算關係?如果有類似的疑惑,那麼可以先摒棄這種的想法。  

       base陣列維護的是Trie樹上節點的資訊,這個公式想表達的意思是:狀態 s (也就是一個節點,Trie樹上每個節點都表示一個狀態,不理解的可以先了解一下狀態機的概念)接收一個字元 c 後,就得到狀態 t ,而並不是base陣列中下標為 s 處的取值加上 c值 等於 t 值,所以說不能從僅僅從數值計算上理解這個公式。那麼這個公式是如何用在DAT的建立中的呢?我們從如下幾個問題入手,來了解這個公式的作用。

       1.1  s、t 具體是什麼意思?

       s、t 表示DAT上的一種狀態、base陣列中的元素,實際上就是Trie上的一個個節點,這個節點上包含很多資訊,建立過Trie樹的同學知道,Trie樹節點上一般包含:屬性值value(可以用來記錄詞性)、葉子節點標記flag(是否為詞語的未字元)、子節點陣列(用來儲存子節點的資訊)等等

      1.2  base陣列中儲存的是什麼?

       base陣列維護的是各個狀態的資訊,即陣列中儲存的是各節點的資訊,但是具體內容與1.1節中所說的Trie節點資訊不一致,以參考文獻1中實現為例,每個狀態下包含的資訊包括

      

 

      transferRatio:計運算元節點在base陣列中下標時的轉移基數,為了解決節點儲存位置衝突而引入的,初始值為0

      isLeaf:節點是否為詞語的葉子節點。該內容可選,也可以用其它方式表示葉子節點

      label:節點儲存的字元,可以理解為該節點是通過插入哪個字元得到的。該內容可選,也可以不要

      value:如果該節點為葉子節點,那麼其對應的詞語在詞典中序號。該內容為可選,也可以不要

      1.3  base陣列的下標表示什麼意思 

        base陣列的下標是基於字元的編碼做加減運算得到的數值。參照1.2節中的例子,建立DAT儲存詞語“中華”,先建立一個base陣列,

       TrieNode base[10]     //假設儲存10個節點

 令根節點root = new TrieNode ,很自然的將根節點root存入base[0]。令 root. transferRatio=1 (這裡設為1,也可以設個其他值,初始值隨便,保證base陣列不溢位即可)

接著插入字元“中”,假如採用unicode編碼,那麼“中”的碼值為20013,那麼儲存 字元“中”的節點在base陣列中的下標就為

0 + base[0].transferRatio + unicode("中")=0+1+20013=20014

令 base[20014].transferRatio=1    (這裡設為1主要是為了和初始時的0區分開,表示base[20014]這個位置已經有節點佔據了)

然後插入字元“華”,unicode('華')=21326,因為是節點“中”接收字元“華”,儲存 字元“華”的節點在base陣列中的下標就為

20014+ base[20014].transferRatio + unicode('華')=20014+0+21326=41340,

令base[41340].transferRatio=1  ,原因同上。

      1.4  c值怎麼計算

      c值得計算除了1.3中說的計算字元得unicode值,你還可以計算字元hash值得到,只要保證每個字元(中文、英文、日文等等文字的字元)有一個唯一且確定得編碼值即可。

 

       通過以上幾個問題的說明可知,DAT中每個狀態的下標是唯一的(解決位置衝突之後),可以用base陣列的下標表示狀態,通過索引base陣列的下標即可得到各個狀態的資訊。此時,我們再來從數值計算的角度來理解這個公式,即狀態 s 的下標(還要加上偏移transferRatio)加上字元c的編碼值,等於狀態 t (由狀態s 接收字元c 得到)的下標。

 

2. check[t] = s    

      理解這個等式,先要從DAT的查詢邏輯說起。在Trie中,我們是怎麼判斷詞語存在的?從根節點開始,依次查詢詞語中的每個字元,若各個字元均存在於當前節點的子節點中,則表明該詞語存在。如果在DAT上也按照這個邏輯來判斷詞語是否存在,則查詢過程是這樣的:還是以1.3節為例,查詢詞語 “中華” 是否存在。從根節點開始,首先查詢 “中”字,

0 + base[0].transferRatio + unicode("中")=0+1+20013=20014

若base[20014].transferRatio ≠ 0 ,則表明字元“中”存在。(判斷base陣列中一個位置上是否有資料可以採用很多方式,這裡採用參考文獻1中的實現方式,即判斷TrieNode.transferRatio是否非0)

接著查詢字元“華”,

20014+ base[20014].transferRatio + unicode('華')=20014+0+21326=41340

若base[20014].transferRatio ≠ 0,則表明“華”存在。那麼這個邏輯在DAT上是否可靠呢?答案是不可靠,因為在DAT上節點轉移是通過在base陣列上索引字元unicode碼值進行的,這就會存在以下的情況

unicode(A)  ≠  unicode(B)

unicode(A)+unicode(C) = unicode(B)+unicode(D)

其中 A、B、C、D指unicode編碼規範中收錄的某個字元,此種情況下的查詢邏輯就會出錯,所以在DAT中引入了check陣列,在check陣列中儲存的是當前狀態的父狀態(即當前節點的父節點),還是以1.3節中詞語“中華”為例,以base陣列的下標表示狀態,字元“中”所在節點的父節點為根節點,所以check[20014]=0;字元“華”所在節點的父節點下標為20014,即check[41340]=20014,這樣可以確定當前節點的父節點是哪一個,從而解決   unicode(A)+unicode(C) = unicode(B)+unicode(D) 帶來的問題。

 

    

 

 

相關文章