您的位置:首页 > 其它

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树的规则进行插入。

可以看出,利用矩阵的结构,其实要改变的,是寻找出冲突节点的复杂度,最大的计算量,在于找出所冲突的节点,以及修改冲突节点各个的计算损耗,从而决定修改哪一个节点。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: