0. 前置知识
1. 双数组字典树介绍
双数组字典树英文名为DoubleArrayTrie,他的特点就是使用两个数组来表示一颗字典树,这里有比较有趣了,两个数组是怎么表达出字典树的呢?
2. 双数组介绍
顾名思义,有两个数组,一个是base,另一个是check。
首先介绍数组的下标,数组的下标代表字典树上节点的编号,一个下标对应一个节点。
其实base数组的作用是用来记录一个基础值,这个值可以是随机值,只要不产生冲突就可以了,所以这个值可以用随机数算法获取,当然这样效率不高,高效的做法应该是使用指针枚举技术,ok,现在你已经明白了,base数组是一个不产生冲突的随机数组。
最后,check数组,check数组与base数组相互照应,如果base[i]=check[j]
则说明j是i的儿子,而且i到j的边权恰好为j-base[i]
,也可以写作j-check[j]
好好理解这句话
从另一个方面而言,双数组字典树的base数组,应该是一个指针数组,他指向了一个长度为字符集大小的数组的首地址,而check数组是一种hash碰撞思路,由于base数组疯狂指向自己,导致产生了很多碰撞,但是由于字典树是一个稀疏图,导致儿子节点指针利用率低,所以base数组疯狂复用这段空间,最后必须要依赖check来解决冲突,
双数组字典树相比于传统字典树,仅仅只在内存方面于增删改查占有优势,但是唯一不好的地方就是删和改会导致base数组内存分裂,难以回收,删和改如果占大头,那么传统字典树的内存效率更高
由于搜索领域几乎不涉及到删和改,所以这个数据结构很nice,字符集多大,就节省了多少倍的空间
数据结构很棒,但是在现在这个内存不值钱的时代,这些指针的储存用hashmap直接无脑顶掉,空间占用也高不了多少,hashmap顶多浪费两倍空间
两倍的空间算不上啥,除非这是唯一的优化点,否则不会优化到这个数据结构上来
3. 图解双数组字典树
首先我们直接给出一颗双数组字典树,下面是三列,第一列是下标,第二列是base,第三列是check, 我们来根据这个双数组还原那颗字典树
1 | index base check |
从第一行看起,base[0]=1, check[0]=0 ,这说明根节点是0号节点。
1 | graph LR |
然后我们来找0节点的子节点,只需要哪些check值为base[0]即可,我们发现check[66],check[68],check[91]满足,所以他们都是0号节点的子节点
对于check[66], 边权为66-check[66]=65 ,是字符A
对于check[68], 边权67, 是字符C
对于check[91], 边权90,是字符Z
所以现在图成了
1 | graph LR |
接着我们看66号节点,base[66]=2,那么他的子节点为69和70,边权分别为69-check[69]=67,70-check[70]=68,分别是C和D
1 | graph LR |
69的字节点为74和75, 权重为74-5=69和75-5=70, 分别是E和F
1 | graph LR |
当然,后面的笔者就不进行模拟了,到此为止。
4. 双数组字典树的构建算法
首先算法的第一步是构建一颗字典树,你需要将这颗字典树构建出来。字典树的代码如下,这个不用多说了
1 | void addTire(const char* str, int len) { |
第二步,就是在字典树上进行BFS,在BFS过程中增量构建双数组字典树,当我们BFS到某个节点U的时候,便开始寻找,当前还有哪个base值没有被使用,可以使得这个U的子节点的check值均为0, no BB , show code
先找到U的所有子节点
1 | vector<int> sonTransList; |
然后寻找一个合适的base值,这个值储存在变量begin中
1 | // find begin |
最后就是赋值就好了.
5. 代码
最后附上一个很丑的代码,第三节中介绍的双数组就来源于这个代码的输出。
1 | // ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 |
- 本文作者: fightinggg
- 本文链接: http://fightinggg.github.io/yilia/yilia/R16K60.html
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!