Tire 樹結構存在較大的資料稀疏,造成了空間浪費。Double-array結合了array查詢效率高、list節省空間的優點,可以有效降低空間浪費,具體是通過兩個數組base、check來實作。Trie樹可以等同于一個自動機,狀态為樹節點的編号,邊為字元。
- base數組中每個元素對應trie中的一個節點即狀态;
- check數組表示的事某個狀态的前驅狀态,用來驗證轉移的有效性;
- 兩個數組滿足如下轉移方程:
[1]base[s] + c = t
check[t] = s
[2]
s 是目前狀态下标 c 是輸入字元的數值(或編碼)。
double array trie 在構造時 base 數組中的值如何确定:
- 首先預處理需要構造的檔案,将首字相同的詞條排列在一起,首字不同的詞條按照首字詞條數兩由大到小排列,首字相同的按照次字 UTF 編碼有小到大排序,依次類推,形成有序的詞條集合。 比如:“lie”、“人民”、“浙江”、“民生”,“like” ,排序後為:
。lie、like、人民、民生、浙江
'浙'.encode('utf-8') > '民'.encode('utf-8') > '人'.encode('utf-8')
True
在構造英文或中英文混合Double-array Trie 時英文可以使用 ACSII編碼,如果以中文 UTF 編碼作為輸入數值的話,會造成樹嚴重的稀疏;中文
‘人’.encode('utf-8')
輸出
b'\xe4\xba\xba'
數值比較大,是以要對其進行重新編碼:比如說所有字元從數字
1
開始遞增編碼;
就拿上面排序好的詞條:
lie、like、人民、民生、浙江
----->>
l -- 1, 人 -- 2, 民 -- 2, 浙 -- 4, i -- 5, 生 -- 6, 江 -- 7, e -- 8, k -- 9
。
構造 Double-array Trie字典時,初始化數組為0;根節點對應于0位置并且base和check都為0:
- 首先确定所有詞條深度為1(首字,‘中國’中深度1,國深度為2)的字元對應位置的base值;
- 确定base值時要確定其後深度為2的字元都能存入數組;
- 然後根據深度2的字元的編碼來确定其對應的位置以及check值;
- 以此類推确定2層base值,直至字元結束。
對上面已将編碼的詞條來确定其base和check值:
-
确定四個首字元的對應的位置:
根據上面狀态轉移公式 [1] 可以得出:
、base[0] + code[l] = 0 + 1
、base[0] + code[人] = 0 + 2 = 2
、base[0] + code[民] = 0 + 3 = 3
, 根據狀态轉移公式 [2] 可以得出:base[0] + code[浙] = 0 + 4 = 4
check[1] = check[2] = check[3] = check[4] = 0
s&t | 1 | 2 | 3 | 4 |
---|---|---|---|---|
base | ||||
check | ||||
l | 人 | 民 | 浙 |
- 有了開頭我們繼續确定接下來的 base 和 check:
-
,base[l] + code[i] = x + 5
,,取check[x + 5] = x
,那麼x=1
,base[l] = 1
;check[1 + 5] = 1
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
base | 1 | |||||
check | 1 | |||||
l | 人 | 民 | 浙 | li |
-
-
,base[人] + code[民] = x + 3
,check 位置0、1、2、3、4、6已經被用,那麼x可以取check[x + 3] = x
,那麼x = 2
,base[人] = 2 (base[2] = 2)
,此時check[2 + 3] = 2
已經組成了一個詞條并且不是其他詞條的字首(在目前上下文中), 是以人民
;base[5] = -5
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
base | 1 | 2 | -5 | |||
check | 2 | 1 | ||||
l | 人 | 民 | 浙 | 人民 | li |
-
-
,base[民] + code[生] = x + 6
, 可以取check[x + 6] = x
, 那麼x = 1
,base[民] = 1
,注意check[1 + 6] = 3
已經是一個詞條了,并且不是其他詞條的字首,是以民生
;base[7] = -7
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
base | 1 | 2 | 1 | -5 | -7 | ||
check | 2 | 1 | 3 | ||||
l | 人 | 民 | 浙 | 人民 | li | 民生 |
-
-
,base[浙] + code[江] = x + 7
, 可以取check[x + 7] = x
, 那麼x = 1
,base[浙] = 1
,注意check[1 + 7] = 4
已經是一個詞條了,并且不是其他詞條的字首,是以浙江
;base[8] = -8
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
base | 1 | 2 | 1 | 1 | -5 | -7 | -8 | |
check | 2 | 1 | 3 | 4 | ||||
l | 人 | 民 | 浙 | 人民 | li | 民生 | 浙江 |
-
-
,base[i] + code[e] = x + 8
, 可以取check[x + 8] = x
, 那麼x = 1
,base[i] = 1
,注意check[1 + 9] = 6
已經是一個詞條了,并且不是其他詞條的字首,是以lie
;base[9] = -9
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
base | 1 | 2 | 1 | 1 | -5 | 1 | -7 | -8 | -9 |
check | 2 | 1 | 3 | 4 | 6 | ||||
l | 人 | 民 | 浙 | 人民 | li | 民生 | 浙江 | lie |
-
-
,base[i] + code[k] = 1 + 9
,這裡base[i] 已知了;check[1 + 9] = 6
-
s&t | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
base | 1 | 2 | 1 | 1 | -5 | 1 | -7 | -8 | -9 | |
check | 2 | 1 | 3 | 4 | 6 | 6 | ||||
l | 人 | 民 | 浙 | 人民 | li | 民生 | 浙江 | lie | lik |
-
-
,base[k] + code[e] = x + 8
,取check[x + 8] = x
,那麼x = 3
, 注意check[3 + 8] = 10
已經是一個詞條了,并且不是其他詞條的字首,是以like
;base[11] = -11
-
最終:
s&t | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
base | 1 | 2 | 1 | 1 | -5 | 1 | -7 | -8 | -9 | 3 | -11 |
check | 2 | 1 | 3 | 4 | 6 | 6 | 10 | ||||
l | 人 | 民 | 浙 | 人民 | li | 民生 | 浙江 | lie | lik | like |
查找“人民”這個詞為例:
狀态轉移公式:
base[s] + c = t
[1]
check[t] = s
[2]
t1 = base[0] + code[人] = 0 + 2 = 2
check[2] = 0
并且
base[2] !< 0
,繼續往下找;
base[2] + code[民] = 2 + 3 = 5
,
check[5] = 2
并且
base[5] < 0
,是以
人民
這個詞在Double-array Trie樹中;
并且
base[5] = -5
說明樹中沒有以
人民
為字首的詞,如果來查找
人民大會堂
就會查找失敗;
參考文獻
[1] 戴耿毅、佘靜濤,基于雙數組Trie樹算法的字典改進和實作 2012