1. 程式人生 > >Double Array Trie(一)

Double Array Trie(一)

Trie是一種常見的資料結夠,可以實現字首匹配(hash是不行的),而且對於詞典搜尋來說也是O(1)的時間複雜度,雖然比不上Hash,但是空間會省不少。

Trie樹主要應用在資訊檢索領域,非常高效。今天我們講Double Array Trie,請先把Trie樹忘掉,把資訊檢索忘掉,我們來講一個確定有限自動機(deterministic finite automaton ,DFA)的故事。所謂“確定有限自動機”是指給定一個狀態和一個變數時,它能跳轉到的下一個狀態也就確定下來了,同時狀態是有限的。請注意這裡出現兩個名詞,一個是“狀態”,一個是“變數”,下文會舉例說明這兩個名詞的含義。

舉個例子,假設我們一共有10個漢字,每個漢字就是一個“變數”。我們為每個漢字編個序號。

 

1

2

3

4

5

6

7

8

9

10

             表1. “變數”的編號

這10個漢字一共可以構成6個詞語:啊,埃及,阿膠,阿根廷,阿拉伯,阿拉伯人。         

這裡的每個詞以及它的任意字首都是一個“狀態”,“狀態”一共有10個:啊,阿,埃,阿根,阿根廷,阿膠,阿拉,阿拉伯,阿拉伯人,埃及

我們把DFA圖畫出來:

        圖1. DFA,同時也是Trie樹

在圖中每個節點代表一個“狀態”,每條邊代表一個“變數”,並且我們把變數的編號也標在了圖中。

下面我們構造兩個int陣列:base和check,它們的長度始終是一樣的。陣列的長度定多少並沒有嚴格的規定,反正隨著詞語的插入,陣列肯定是要擴容的。說到陣列擴容,大家可以看一下java中HashMap的擴容策略,每次擴容陣列的長度都會變為2的整次冪。HashMap中有這麼一個精妙的函式:

1

2

3

4

5

6

7

8

9

10

//給定一個整數,返回大於等於這個數的2的整次冪

static int tableSizeFor(int cap) {

        int n = cap - 1;

        n |= n >>> 1;

        n |= n >>> 2;

        n |= n >>> 4;

        n |= n >>> 8;

        n |= n >>> 16;

        return (n < 0) ? 1 :  n + 1;

}

回到今天的正題,我們不妨把double array的初始長度就定得大一些。兩陣列元素初始值均為0。

double array的初始狀態:

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

check

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

state

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

把詞新增到詞典的過程就給base和check陣列中各元素賦值的過程。下面我們層次遍歷圖1所示的Trie樹。

step1.

第一層上取到3個“狀態”:啊,阿,埃。把這3個狀態按照其對應的變數的編號(查表1)放到state陣列中。

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

check

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

state

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

step2.

當存在狀態轉移時,有

1

2

check[t]=s

base[s]+c=t

其中s和t代表某個狀態在陣列中的下標,c代表變數的編號。

此時層次遍歷來到了圖1所示DFA的第二層,我們看到“阿”的子節點有“阿根”、“阿膠”、“阿拉”,已知狀態“阿”的下標是2,變數“根”、“膠”、“拉”的編號依次是4、5、6,下面我們要給base[2]賦值:從小到大遍歷所有的正整數,直到發現某個數正整k滿足base[k+4]=base[k+5]=base[k+6]=check[k+4]=check[k+5]=check[k+6]=0。(查詢到base和check等於0的, 是因為0代表該位沒有被使用)得到k=1,那麼就把1賦給base[2],同時也確定了狀態“阿根”、“阿膠”、“阿拉”的下標依次是k+4、k+5、k+6,即5、6、7,而且check[5]=check[6]=check[7]=2。

同理,“埃”的子節點是“埃及”,狀態“埃”的下標是3,變數“及”的編號是7,此時有check[1+7]=base[1+7]=0,所以base[3]=1,狀態“埃及”的下標是8,check[8]=3。

遍歷完DFA的第二層後得到下表:

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

0

1

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

check

0

0

0

0

2

2

2

3

0

0

0

0

0

0

0

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

 

 

 

 

 

 

 

 

 

 

 

step3.

重複step2,層次遍歷完整查詢樹之後,得到:

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

0

1

1

0

1

0

1

0

0

1

0

0

0

0

0

0

0

0

0

check

0

0

0

0

2

2

2

3

5

7

10

0

0

0

0

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

阿根廷

阿拉伯

阿拉伯人

 

 

 

 

 

 

 

 

step4.

最後遍歷一次DFA,當某個節點已經是一個詞的結尾時,按下列方法修改其base值。

1

2

3

4

if(base[i]==0)

    base[i]=-i

else

    base[i]=-base[i]

得到:

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

-1

1

1

0

1

-6

1

-8

-9

-1

-11

0

0

0

0

0

0

0

0

check

0

0

0

0

2

2

2

3

5

7

10

0

0

0

0

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

阿根廷

阿拉伯

阿拉伯人

 

 

 

 

 

 

 

 

double array建好之後,如果詞典中又動態地添加了一個新詞,比如“阿拉根”,那麼“阿拉”的所有子孫節點在double array中的位置要重新分配。

 

圖2. 動態新增一個詞

首先,把“阿拉伯”和“阿拉伯人”對應的base、check值清0,把“阿拉伯”和“阿拉伯人”從state陣列中刪除掉,把“阿拉”的base值清0。

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

-1

1

1

0

1

-6

0

-8

-9

0

0

0

0

0

0

0

0

0

0

check

0

0

0

0

2

2

2

3

5

0

0

0

0

0

0

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

阿根廷

 

 

 

 

 

 

 

 

 

 

然後,按照上面step2所述的方法把“阿拉伯”、“阿拉根”插入到double array中。變數“根”、“伯”的編號是4和9,滿足base[k+4]=base[k+9]=check[k+4]=check[k+9]=0的最小的k是6,所以base[7]=6,“阿拉伯”和“阿拉根”對應的下標是10和15。同理把“阿拉伯人”插入到double array中。

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

-1

1

1

0

1

-6

6

-8

-9

0

0

0

0

0

1

0

0

0

0

check

0

0

0

0

2

2

2

3

5

7

15

0

0

0

7

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

阿根廷

阿拉根

阿拉伯人

 

 

 

阿拉伯

 

 

 

 

最後,遍歷圖2所示的DFA,當某個節點已經是一個詞的結尾時按照step4中的方法修改其base值。

下標

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

base

-1

1

1

0

1

-6

6

-8

-9

-10

-11

0

0

0

-1

0

0

0

0

check

0

0

0

0

2

2

2

3

5

7

15

0

0

0

7

0

0

0

0

state

 

阿根

阿膠

阿拉

埃及

阿根廷

阿拉根

阿拉伯人

 

 

 

阿拉伯

 

 

 

 

 

double array建好之後,如何查詢一個詞是否在詞典中呢?

比如要查“阿膠及”,每個字的編號是已知的,我們畫出狀態轉移圖。

變數“阿”的編號是2,base[2]=1,變數“膠”的編號是5,base[2]+5=6,我們檢查一下check[6]是否等於2。check[6]確實等於2,則繼續看下一個狀態轉移。同時我們發現base[6]是負數,這說明“阿膠”已經是一個完整的詞了。

繼續看下一個狀態轉移,base[6]=-6,負數取其相反數,base[6]=6,變數“及”的編號是7,base[6]+7=13,我們檢查一下check[13]是否等於6,發現不滿足,則“阿膠及”不是一個詞,甚至都是不是任意一個詞的字首。