datrie树修改
2016-05-11 19:22
459 查看
论文中的datrie树的创建:
内存分析:严谨的遵循论文中的生成过程,词组是单个插入的, 程序中预先开辟的空间,是为最后生成的.bin 文件预留, 一共有三个数组:base、check、tail。单词在插入的过程中,只是读取字符串,然后将单个的字符插入树中,所以程序运行期间占用的内存,与最后生成的.bin文件大小一致。单词只作为只读的字符串,用于给base、check、tail三个数组相关的元素赋值。
运算复杂度分析:
词组是单个的插入树中,会遇到两种冲突, 1. Tail数组冲突。 2. Base 数组冲突。Check数组作为前缀指向辅助元素,与base数组相关,base数组修改,check将紧随着base修改。
1. Tail 数组冲突, 含义是,在插入过程,有相同的前缀,但在尾部的数组中,也有相同的字符。解决方法是:将tail尾数组中,相同的字符,提取到base数组中,形成共同的前缀节点。
时间度分析:该过程只是单纯的将共同的元素,提取后,赋值给base数组,可以简化为 从一个数组,向另一个数组赋值的过程,空间复杂度不变,时间复杂度为O(n). 之所以和要提取的字符长度相同,是因为base数组和check数组,都需要单个的赋值,无法做到整块数据的赋值,总耗费 O(n) 时间,n 为相同的tail数组中的元素。
2. Base数组冲突:
含义: base数组中存放都是公共节点,该数组冲突, 在现实中的含义是, 插入的字符,与前缀中的某个字符相同 。这描述不清楚,举个例子:
“abcdb”
先建立数学描述: 设base[0] = 1, D(a)=2, D(b) = 3, D(c) = 4, D(d) = 5.. 依次类推。
没有占用的base数组, base[x] = 0.
为没有占用的base数组赋值后, base[x] = 1.
现在分析字符 “abcdeb” , 该字符, 出现了2个’b’ , 看下计算过程:
1 .Base[0] + D(a) = 1 + 2 = 3, 因check[3] = 0, 故:base[3] = 1, check[3] = 0
2.Base[3] + D(b) = 1 + 3 = 4, check[4] = 0, base[4] = 1, check[4] = 3
3.Base[4] + D(c) = 1 + 4 = 5, check[5] = 0, base[5] = 1, Check[5] = 4
4.Base[5] + D(d) = 1 + 5 = 6, check[6] = 0, base[6] = 1, check[6] = 5
到此为止,都没有产生冲突, 所以base数组依次向后运算, check指出base的前缀关系。
下一步:
5.Bae[6] + D(b) = 1 + 3 = 4, check[4] = 3 !!!
注意这一步运算,很关键,在这一步产生冲突, 冲突的原因:
在第2步,和第 5 步,都要插入 ‘b’ 字符,弧向量 D(b) = 3 相同。
插入’b’之前的base节点, base[3] 和 base[6] , 都认为是第一次占用
所以 base[3] == bae[6] == 1, 导致计算出的下一个base节点的下标 4 是相同的.
但 check[4] = 3, 表明已经有了前缀,所以第 5 步计算出的 下标为 4 的base节点,产生了冲突,直接按照计算赋值,将会导致树的前缀产生一条环, 每当计算到第5个节点,就将跳转到 第 2 个节点, 从而产生死循环。
上面分析至关重要,从中将得出一条关于节点冲突的结论:
冲突节点产生的原因,正是在于,插入的字符串中,存在重复的字符。
上面的计算过程是在第一次插入字符的时候, 现在假设在树的建立的中间过程:
在中间过程,已经建好的树的节点,应当遵循论文中的数学规则,不应当有错误,也就是节点冲突的死循环的产生。所以问题就将存在于新插入的字符串。
当新插入的部分,每个字符, 都与现有的树的路径不同,那么直接计算即可。
当插入的某个字符,与树中,该字符的路径中的字符,存在重复,那么将导致计算出的base节点的 base[x]的数值,在计算 check [ base[x] ] != 0, 此节点产生冲突。
产生冲突之后,就需要解决掉冲突的节点, 解决办法分析:
之所以计算出的base [x] 相同, 是因为 base [ x-1 ] 和 D(x) 都相同,但是D(x) 作为弧向量,是不能被修改的, 那么如果要 base[x] 不同, 必然要修改 base [x-1] 的值。
也就是说,要修改插入的字符 ‘x’ 的前一个字符的 base值。
但现在面临一个选择, 树中最后会出现两个 ‘x’, 虽然现在,他们的base值是有待修改的, 但是他们的上一个节点的base,却是确定的,修改的原则,从修改上一步的base的值后, 计算出的下一个节点,在base数组中依然有空闲的节点。
也就是说, 修改 ‘x’ 的上一个节点的base, 随后波及的修改范围, 只能是
base { base[x-1] +1 }, 也就是只能让下一层相关的节点的base值被影响。
所以,为解决冲突,就需要修改 base[x-1]的值,并且需要修改, base[x-1] +1 层中,所有 base[x-1] 节点的后继的节点的base和check, 修改的波动只涉及随后的一层。
那么就计算,比较 树中,原有的 ‘x’ 的前缀 base[x-1] 的代价,就是 base[x-1]的后缀边的数量。
与修改 新插入的 ’x’ 的前缀 base[X-1] 的代价,就是base[X-1]后缀边的数量。
所以结论是:
如果要插入一个字符串, 有两种情况:
1. 插入的字符串中,没有重复的字符,那么直接在datrie树中运算相应数组的值,进行赋值操作。
2. 插入的字符串里含有相同的字符。
那么, 计算相同的字符 的 {上一个字符}的位置,计算到该位置,他们的后缀边有多少,选择后缀边最少的, 修改该{上一个字符}的base值, 并修改该{上一个字符}的节点 的 所有的后缀的base值。
修改完成之后,再将该字符,插入已经修改过base值的上一个节点的的后边。
然后按照dat树的数学特性,进行计算和赋值。
论文中的datrie的性能评估:
可以根据以上分析看出, 论文中的dat树,之所以内存占用只与最终的.bin文件相同,在于整个过程,是不断进行动态插入的,要插入的字符串,除了用于参与树的数组计算外,不具有其他的意义,所以并不对其进行存储和重新调整,只是读取而已。至于运算复杂度, 因为该做法,并不利用其他的辅助特性,所以只能依靠 base数组和check数组中记录的值,并结合tail数组中的空余位置,进行反复的计算,找出要修改的{上一个节点的}base的值,并将base数组中,相应的随后所有的后缀的base和check的值,都做相应的修改。
这个过程,按照最坏的情况分析,那么修改一个base值,接着修改随后的所有的后继节点的base值和check值,那么一个base节点的复杂度是,
0(n)
最坏的,莫过于,每插入一个节点,都修改随后整个的后缀节点,那么整个树的计算复杂度是
O(n^n)
n的n次方,n为base数组的长度。
现有项目的dat树的建立过程:
解决问题的症结,就在于base节点的冲突。已有项目的做法,是建立 { 路径=>后缀边 } 的映射map关系,然后每条路径最后一个base节点,查找tail数组中,哪一个能容纳所有的后缀边,然后给base赋值。分析下这个过程, 其实并没有考虑base节点冲突的情况,确切的说,是没有考虑任何可能的冲突情况,之所以能建立,在于利用了该map映射特性,预先建立了整个树中,所有的路径,在每一层,对随后的边的映射,也就是对随后边的关系,利用对下一层节点的存储关系,寻找出,适合当前层的base的节点的值。
其实是一种,预先利用后缀存储的节点的数目,来决定当前的节点的base值。
该过程利用了dat树,每层路径与后缀边的关系。
但是,在建立树的时候,必然需要知道整个树的结构,然后才能利用该结构的特性,对base数组和tail数组进行赋值, check数组其实并没有起到应有的作用。
在建立树之前,必然要预先建立该map映射结构,这样,就不能再接受字符串的动态插入。
因为一旦要动态的插入字符串,就必然要整合进该树的map特性结构中,为了插入一个字符串,就要把以前所有的字符串都加载进来,建立完整个新的map,然后利用该map结构建立树。
这么分析,那么动态插入任何一条字符串,都相当于按照新的字典文件整个的进行加载和运算,和统一的字典文件加载,没有区别。
项目中的dat树的性能分析:
像论文中的做法,运算复杂度无法接受, 暂时不考虑字符串的动态插入,先优化运算过程。之所以运算量巨大,其实就是为了解决base节点的冲突。
现有项目的做法,不解决冲突,根据base节点和后缀节点的关系,从tail数组中寻找符合条件的值。该过程利用了map结构,即使不考虑字符插入的动态性需求,运算量依然庞大,
该运算的复杂度,每一个节点,都需要根据后缀的节点决定,并且都需要从tail数组的头开始查找,所以单节点的复杂度
O(n) , n是tail数组的长度,最坏情况,就是查找所有tail数组
按层建立,假设该层有k个节点,那么一层的复杂度
O(n * K)
假设有m层,那总的复杂度是
O(n * k*m)
注意,该分析,并没有将建立map所占用的空间消耗进行分析。
即使利用语言和os的特性,只第一次将所有的字典文件加载入内存,随后只是用指针,指向该空间,但每个map的节点,都需要保存从该节点的从跟到该节点的路径。每个节点保存一条路径,空间占用,将是
S(n^n * m) , n为节点数, m为树的层次, n^n*m 为所需要的额外空间,利用指针可以削减该损耗,但是不能完全去除。
新的dat建立的办法:
计算量的原因: 要解决base节点的冲突。Base节点冲突的原因: 插入的字符中,含有相同的字符。
现在添加一种辅助的结构,用矩阵来辅助运算。
a | b | c | d |
a | b | d | a |
a | b | d | b |
b | a | c | d |
b | a | d | a |
c | a | d | b |
现在先将所有要插入的字符串存入文本,每行一个字符串,然后读取,建立矩阵表示。
假设矩阵有 m 行 * n 列。
M 行的含义,有m个字符串。
N列的含义,是字符串的长度, 现在为了计算方便,将字符串的长度都简化为n, 该细节的简化,不影响整体的结构分析,随后会对该细节简化进行分析。
将所有的要建立dat树的字符串,读取到内存中,然后建立一个 m * n 的矩阵,
然后, 对矩阵进行排序。
以列为基准,按照基数排序。
从第1列, 对所有的m行的第1列,按照他们的编码数值,从小到到大排列。
在第2列, 注意,规则是, 在第1列中,相同的字符,已经聚集在相邻行,那么在第1行相同的字符为块, 对该块,在进行排序。
第3列,以第2列的字符排列块的基准,以块为单位进行排序。
抽象下,就是:
对第n 列的排序,必须要以第 n-1 列中的字符是相同的,才可进行。
上面的矩阵作为一个示例, 排序后,字符串被排序为:
abcd
abda
abdb
bacd
bada
这么排序的原因是:
经过排序后, 前 n 列中,相同的m行,含义是, 树的前n层,他们的具有相同的前缀 [0....n].
该前缀n, 如果 要知道他们的后缀,就扫描该m行中,第n+1个字符,所有在m行中,含有的所有的不同的 第n+1列的字符,就是该前缀的后缀弧。
建立树的时候,按层建立,也就是,按照列为基准,从第一列,一直扫描到最后一列。
现在回到冲突的解决上,base节点冲突,是因为插入的字符串,含有相同的字符,
(真实的情况是,字符,如果重复的话,将不只两个,因为是按层建立,所以出现重复的字符,那么只与最接近的相同的字符有关, 而不考虑更远的相同的字符。)
出现相同的字符之后, 为重新给相同的字符的前一个base赋值,需要知道,相同的字符的上一层的两个base,他们的后缀边都分别有多少。
论文中为了知道他们的后缀边的多少,展开base和check数组相关的计算。
现在利用数组的特性, 举个例子:
现在扫描到第 4 列, 在第5行, 出现冲突, 第4列,第5行,是字符’a’,
但是第5行的第2列,已经有了字符 ‘a’
现在,需要知道 {5, 2} ‘a’ 的前一个字符 {5, 1} 有多少后缀边。
也需要知道 {5,4} ‘a’ 的前一个字符,{5, 3} 有多少后缀边。
{5,1} 的后缀边的个数, 在矩阵中,运算规则是
以第1列为基准,寻找到该列中,所有与{5. 1 } 字符相同的列,符合需要的有 k 行
在从该 k 行中,寻找所有 1 + 1 列, 有多少个字符不同,该1+1列,是 {5, 1} 字符的后继边。
抽象成数学规则,就是
如果要知道矩阵中, 字符 {m, n} 的后缀边,
扫描 第n列,寻找与前n列的字符串,相同的行,假设有 k行,
那么扫描该k行的,第 n+1 列,查出n+1列中,共有多少个不同的字符,不同的个数为 s,
那么, 字符 {m, n} 的后缀边的个数为 s。
上边描述的过程,是针对随机的矩阵进行,如果先对矩阵进行排序,那么相同前缀,就必然意味着,相同的行,前n-1列的相同的。
排序之后,只用顺序的比较相邻的两行,就能进行快速的查找出相邻的行。
这样,寻找后缀的边的个数的过程,被转换为在矩阵中,寻找k个行中,有多少行的第n+1的字符不相同的个数。
这样,就找到了需要修改的base节点,修改完该base节点之后,修改其后缀的节点,也就是 该列的下一列的base的数值。
现在考虑重新给选定的节点i的base赋值的过程, 即给base[i] 重新赋值:
现在已经知道了base[i] 的所有后缀边, 假设边为 ‘x’, ‘y’, ‘z’,
新的base[i] , 需要满足以下的特定,
Check [ base[i] + D(x) ] == 0 , 且
Check [ base[i] + D(y) ] == 0, 且
Check [ base[i] + D(z) ] == 0.
之后,对剩下的字符串,按照dat树的规则进行插入。
可以看出,利用矩阵的结构,其实要改变的,是寻找出冲突节点的复杂度,最大的计算量,在于找出所冲突的节点,以及修改冲突节点各个的计算损耗,从而决定修改哪一个节点。
相关文章推荐
- 搞懂树状数组(转)
- unity 4 Please check your configuration file and verify this type name.
- 表视图实现好友分组(可收起放下)
- Linux进程通信共享内存函数
- C++ 无锁队列 ABA <2>
- bzoj3524 [Poi2014]Couriers
- Codeforces Round #350 (Div. 2) F. Restore a Number 模拟构造题
- iOS TextField监听、判断按钮可用及BUG修复
- spring @Transactional 方法内事务不起作用的解决办法
- 47905375
- Leetcode 20. Valid Parentheses
- 这或许是华为荣耀六root的另一种方法。
- reflow(回流)和repaint(重绘)及其优化
- iOS开发之基础视图— UISwitch
- Volley StringRequest和JSONObjectRequest使用几个细节
- hdu 2084 数塔
- saltstack之(九)配置管理源码部署Nginx
- HDU 2851.Lode Runner【DP动态规划】【5月11】
- POJ2234(二进制和平衡状态概念)
- python super