双数组字典树Double Array Trie(上)

本文转载自:http://www.cnblogs.com/zhangchaoyang 作者:Orisun 如有侵权,请联系本人,一定修改至您满意为止。


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图画出来:

双数组字典树Double Array Trie(上)_第1张图片

        图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.

当存在状态转移双数组字典树Double Array Trie(上)_第2张图片时,有

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。得到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中的位置要重新分配。

 双数组字典树Double Array Trie(上)_第3张图片

图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建好之后,如何查询一个词是否在词典中呢?

比如要查“阿胶及”,每个字的编号是已知的,我们画出状态转移图。

双数组字典树Double Array Trie(上)_第4张图片

变量“阿”的编号是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,发现不满足,则“阿胶及”不是一个词,甚至都是不是任意一个词的前缀。

github上一个日本人贡献了他的java版的Darts(Darts本来是一种Double Array Trie的C++实现),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import  java.io.BufferedInputStream;
import  java.io.BufferedOutputStream;
import  java.io.DataInputStream;
import  java.io.DataOutputStream;
import  java.io.File;
import  java.io.FileInputStream;
import  java.io.FileOutputStream;
import  java.io.IOException;
import  java.util.ArrayList;
import  java.util.Collections;
import  java.util.List;
 
/**
  * DoubleArrayTrie在构建双数组的过程中也借助于一棵传统的Trie树,但这棵Trie树并没有被保存下来,
  * 如果要查找以prefix为前缀的所有词不适合用DoubleArrayTrie,应该用传统的Trie树。
  *
  * @author zhangchaoyang
  *
  */
public  class  DoubleArrayTrie {
     private  final  static  int  BUF_SIZE =  16384 ; // 2^14,java采用unicode编码表示所有字符,每个字符固定用两个字节表示。考虑到每个字节的符号位都是0,所以又可以节省两个bit
     private  final  static  int  UNIT_SIZE =  8 // size of int + int
 
     private  static  class  Node {
         int  code; // 字符的unicode编码
         int  depth; // 在Trie树中的深度
         int  left; //
         int  right; //
     };
 
     private  int  check[];
     private  int  base[];
 
     private  boolean  used[];
     private  int  size;
     private  int  allocSize; // base数组当前的长度
     private  List key; // 所有的词
     private  int  keySize;
     private  int  length[];
     private  int  value[];
     private  int  progress;
     private  int  nextCheckPos;
     int  error_;
 
     // 扩充base和check数组
     private  int  resize( int  newSize) {
         int [] base2 =  new  int [newSize];
         int [] check2 =  new  int [newSize];
         boolean  used2[] =  new  boolean [newSize];
         if  (allocSize >  0 ) {
             System.arraycopy(base,  0 , base2,  0 , allocSize); // 如果allocSize超过了base2的长度,会抛出异常
             System.arraycopy(check,  0 , check2,  0 , allocSize);
             System.arraycopy(used,  0 , used2,  0 , allocSize);
         }
 
         base = base2;
         check = check2;
         used = used2;
 
         return  allocSize = newSize;
     }
 
     private  int  fetch(Node parent, List siblings) {
         if  (error_ <  0 )
             return  0 ;
 
         int  prev =  0 ;
 
         for  ( int  i = parent.left; i < parent.right; i++) {
             if  ((length !=  null  ? length[i] : key.get(i).length()) < parent.depth)
                 continue ;
 
             String tmp = key.get(i);
 
             int  cur =  0 ;
             if  ((length !=  null  ? length[i] : tmp.length()) != parent.depth)
                 cur = ( int ) tmp.charAt(parent.depth) +  1 ;
 
             if  (prev > cur) {
                 error_ = - 3 ;
                 return  0 ;
             }
 
             if  (cur != prev || siblings.size() ==  0 ) {
                 Node tmp_node =  new  Node();
                 tmp_node.depth = parent.depth +  1 ;
                 tmp_node.code = cur;
                 tmp_node.left = i;
                 if  (siblings.size() !=  0 )
                     siblings.get(siblings.size() -  1 ).right = i;
 
                 siblings.add(tmp_node);
             }
 
             prev = cur;
         }
 
         if  (siblings.size() !=  0 )
             siblings.get(siblings.size() -  1 ).right = parent.right;
 
         return  siblings.size();
     }
 
     private  int  insert(List siblings) {
         if  (error_ <  0 )
             return  0 ;
 
         int  begin =  0 ;
         int  pos = ((siblings.get( 0 ).code +  1  > nextCheckPos) ? siblings.get( 0 ).code +  1
                 : nextCheckPos) -  1 ;
         int  nonzero_num =  0 ;
         int  first =  0 ;
 
         if  (allocSize <= pos)
             resize(pos +  1 );
 
         outer:  while  ( true ) {
             pos++;
 
             if  (allocSize <= pos)
                 resize(pos +  1 );
 
             if  (check[pos] !=  0 ) {
                 nonzero_num++;
                 continue ;
             else  if  (first ==  0 ) {
                 nextCheckPos = pos;
                 first =  1 ;
             }
 
             begin = pos - siblings.get( 0 ).code;
             if  (allocSize <= (begin + siblings.get(siblings.size() -  1 ).code)) {
                 // progress can be zero
                 double  l = ( 1.05  1.0  * keySize / (progress +  1 )) ?  1.05  1.0
                         * keySize / (progress +  1 );
                 resize(( int ) (allocSize * l));
             }
 
             if  (used[begin])
                 continue ;
 
             for  ( int  i =  1 ; i < siblings.size(); i++)
                 if  (check[begin + siblings.get(i).code] != 

你可能感兴趣的:(数据结构,Java,双数组字典树,DAT)