双数组trie树的构建
NLP中trie树常用于做快速查询,但普通的trie树由于要保存大量的节点信息,当储存的词量非常大时,不仅所占空间巨大,而且查询的速度也不够快。而双数组trie树就可以比较好的解决这两个问题。
之所以叫双数组trie树,是因为它只用base[] 和check[] 两个数组就将整个trie的信息储存了起来,这两个数组的构建规则是:
b
a
s
e
[
i
]
+
c
o
d
e
(
x
)
=
j
c
h
e
c
k
[
j
]
=
i
\begin{aligned} &base[i] + code(x) = j\\ &check[j] = i \end{aligned}
?base[i]+code(x)=jcheck[j]=i? 其中i,j都是base array的index,base array的长度是trie树中节点的个数,每个节点在base array中都有一个对应的index。
base[] 数组用于记录跳转结构, base array中index为i的那个节点,如果按照字符x转移,会转移到index为j的节点check[] 数组用于 标识出 base array 中每个状态的前一个状态,其主要作用是检验按base做转移的转移正确性code(x) 是字符x的编码,现实中为了方便通常直接用char code
构建base array
下面我们看一个例子来学习base array的构建方法:
现在有“清华”、“清华大学”、“清新”、“中华”、“华人”五个词,构成的trie树如下图所示: 假设例树的字符编码表为:
初始化root的base index为0,base值记为1。首先看root的所有子节点"清,中,华":
- base[0] + code(清) = 2,此时位置2空闲,因此“清”放入位置2
- base[0] + code(中) = 7,此时位置7空闲,因此“中”放入位置7
- base[0] + code(华) = 3,此时位置3空闲,因此“中”放入位置3
目前base array的情况如下:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | | | | 中 | | | | base | 1 | | | | | | | | | | |
- 每次遍历完一个节点的所有子节点,只可以确认当前节点的base值,以及它的子节点的index位置
- 子节点的base值此时会默认继承当前节点的base值,但在遍历子节点的子节点时,一旦有冲突,子节点的base值就会做相应修改
接下来遍历第二层的节点"华,新,华,人":
- base[2] + code(华) = 3,冲突!因此 base[2] 修改为2;base[2] + code(华) = 4,可用,因此 “清华” 放入位置4
- base[2] + code(新) = 7,冲突!因此 base[2] 修改为3;base[2] + code(新) = 8,可用,因此 “清新” 放入位置8
- 因为base[2] 再次北被修改,所以“清华”的位置要重新计算:base[2] + code(华) = 5,可用,因此 “清华” 放入位置5
- base[7] + code(华) = 3,冲突!因此 base[7] 修改为2;base[7] + code(华) = 4,可用,因此 “中华” 放入位置4
- base[3] + code(人) = 8,冲突!因此 base[3] 修改为2;base[3] + code(人) = 9,可用,因此 “华人” 放入位置9
目前base array的情况如下:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | 中华 | 清华 | | 中 | 清新 | 华人 | | base | 1 | | 3 | 2 | | | | 2 | | | |
接下来遍历第三层的节点"大":
- base[5] + code(大) = 6,可用;因此 “清华大” 放入位置6
目前base array的情况如下:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | 中华 | 清华 | 清华大 | 中 | 清新 | 华人 | | base | 1 | | 3 | 2 | | | | 2 | 3 | | |
接下来遍历第四层的节点"学":
- base[6] + code(学) = 7,冲突!base[6] 修改为4;base[6] + code(学) = 8,还是冲突!base[6] 修改为5;base[6] + code(学) = 9,还是冲突!base[6] 修改为6;base[6] + code(学) = 10,可用,因此 “清华大学” 放入位置10
目前base array的情况如下:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | 中华 | 清华 | 清华大 | 中 | 清新 | 华人 | 清华大学 | base | 1 | | 3 | 2 | | | 6 | 2 | 3 | | |
此时节点已经遍历完,剩余base值未确定的都是尾节点了,因为它们都没用子节点了,所以不存在位置冲突,因此可以直接继承父节点的base值:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | 中华 | 清华 | 清华大 | 中 | 清新 | 华人 | 清华大学 | base | 1 | | 3 | 2 | 2 | 3 | 6 | 2 | 3 | 2 | 6 |
构建check array
check array的构建比较简单,只需要将子节点index的check值设为父节点的base值即可,所以有:
position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|
char | root | | 清 | 华 | 中华 | 清华 | 清华大 | 中 | 清新 | 华人 | 清华大学 | base | 1 | | 3 | 2 | 2 | 3 | 6 | 2 | 3 | 2 | 6 | check | -2 | | 0 | 0 | 7 | 2 | 5 | 0 | 2 | 3 | 6 |
双数组trie树的查询
此时如果有一个词“清中”,我们要查询它是否在双数组trie里面,那么首先从root出发,由于“清”的code是1,因此会走到 base[0]+code(清) = 2,这里要用check数组检查位置3上的节点其父节点是否是位置0上的节点:check[2] = 0,等式成立!
然后继续看“中”:base[2]+code(中) = 9,再检查位置9上的节点的父节点是否是位置2上的节点:check[9] = 3 !=2 ,检查发现不满足,因此"清中"不在该trie里面。
Reference: 小白详解 Trie 树 double-array-trie双数组trie树原理解析和数据构建过程
|