double-array trie (转载)

http://blog.csdn.net/zzran/article/details/8462002

http://sbp810050504.blog.51cto.com/2799422/1310596

http://www.360doc.com/content/11/0607/13/28217_122214053.shtml

 

benwen.pdf

Implement.pdf

自己对树是情有独钟,故在元旦放假的时候,翻译了an efficient implementation of trie structures。作者及来源就不叙述了。英文水平有限,但是尽量还原此文章的灵魂。如果有什么不对的地方,敬请各位给予指点。(http://blog.csdn.net/zzran/article/details/8461985-源码).学习的时候要有trie的基础。

 

概论

    下面将呈现一种新的内部数组结构,它便是double-array。double-array继承了数组访问快速的特性和链表结构紧密的特点。对于 double-array的插入,查找和删除将会通过实例来给出解析。虽然插入的过程很慢,但是还是很实用的,对于查找和删除,由于double- array继承了链表的特性,所以很速度。在操作大量关键的时候,我们把double-array和list形式(也就是原始trie的链表的形式)进行 比较,会得到如下结果:double-array占用的空间比trie以链表的形式存储节省了百分之十七的空间,同时double-array的遍历,也 就是查找的速度会比链表的形式快3.1到5.1倍。

简介

     在很多检索的应用中,有必要采用trie树的形式来检索单词。例如:编译器的单词分析器,拼写检查器,参考书目的检索, 在自然语言处理中的形态字典,等等。看到这里,是不是觉得trie是一个很强大的数据结果。对于trie树,例如它的节点是下面这样的几个struct形 式: struct node {char data, struct node next[26]},这是最常见的trie节点形式。它是array-structured。对于next数组的索引index是由一个单词中data所 存储字母的下一个字母来决定的。next[index]所指示或者是一个新的trie节点,或者是一个NULL值。图一给出了一个这样形式的trie树, 它是基于关键字数组K = {baby, bachelor, badge, jar}建立的。trie树的检索,插入,删除都很快,但是它占用了很大的内存空间,而且空间的复杂度是基于节点的个数和字符的个数。如果是纯单词,而且 兼顾大小写的话,每个节点就要分配52*4的内存空间,耗费很大。一种很多人都知道压缩空间的策略就是列出从每个节点引申出来的边,并以空值结尾。图二基 于list-structured形式的trie。这种链表的形式通过对于数组形式中NULL值的压缩来节省空间,也就说指向子节点的指针是以链表的形式 来存放而不是数组的形式。但是如果从每个节点引申出很多边的话,检索的速度会很慢。

    接下来这篇文章会讲解它trie树的结构压缩到两个一位数组BASE和CHECK中去,这种结构叫做double-array。在double- array中,通过数组BASE,非空子节点的位置被映射到CHECK中去,同时,原来array-trie中,每个节点的非空子节点的位置不会被映射到 CHECK的相同位置中。trie树中的每条边都可以在double-array中以O(1)的时间检索到,也就是说,在最坏的情况下,检索一个长度为k 的单词只要O(k)的时间。对于拥有大量关键字的结合,trie树种将会有很多节点,所以要用double-array的形式来达到减少空间分配的目的。 为了能够让trie存储大量的关键字,double-array尽可能的根据需要存储关键字的前缀来区分不同的单词,除去前缀剩下的部分会被存储到 TAIL的一维数组中,以便更进一步的区分不同的单词。

图一



图2


trie树的构建

     关于trie的叙述如下,从根节点到叶子节点形成的每条路径提取出来的字母组成的单词代表了在关键字集合中的一个关键字,或者说,这个提取出来的关键字 可以在关键字集合中找到。所有的这些路径代表的单词加起来,就是关键字的结合。为了不混淆像“the”和“then”这样的单词,在每个单词的 后面多加一个结束符号:“#”。接下来的这些定义将会被用到插入,查找,删除的步骤中去,所以不求能够先理解这些,只要能够记得怎么用,在插入,查找,删 除的过程中,就会逐渐明白这些定义的用意。K代表关键字集合。trie树是由弧和节点主城的。如果一条弧上标记着a,(注意,在这里a是代表子母集合中的 某一个字母,而不是真正的a),那么这个弧可以这样定义 g(n,a)=m,其中n,m是trie树种节点的标号。解释一下n,m。我们用的是两个一位数组来存储的trie,所以n,m就是这两个一位数组中的索 引。代表了trie的两个节点对于K中的一个关键字key,他的节点m满足关系g(n,a) = m,如果字母a是一个能够有效的将自己所属的关键字和其他关键字区分的字母的话,那么m就是一个独立的节点。怎么理解这个呢,看下图:



    还记得之前说过的吗,double-array尽量存储关键字的前缀来压缩空间,但是还要能够把这些拥有公共前缀的关键字区分开,就要求这些关键字要有自 己的特色,特色就是独立于其他关键字的节点。看上图中的字母c,d,b,先说c吧,g(3,c) = 5,这条边中m=5是一个节点,这个节点能够把 bachelo这个单词和其他单词区分开,那么m=5就是一个独立的节点。在这里,话题要进行一个转移,我们想一下原始的trie,就是上面的那个 struct形式的那个,它会把每个单词的每个字母都以一个节点罗列出来,刚才我们找到了独立节点(不包括独立节点中的字母),那么从原始trie中对应 的这个节点开始到单词最后一个字母所占的节点结束,这些字母所组成的字符串叫做独立节点m的字母串。也就是后面的内容都是由m衔接出来的。一棵树,它是由 K集合中所有关键字的独立节点,和独立节点之前的节点,以及这些节点之间的边所组成的,那么我们就叫这棵树为reduced trie。

对于图三,就是一个棵reduced trie。TAIL是用来存储独立节点之后的string的。对TAIL中的符号?现在不要在意,等到我们分析插入和删除的时间就会用到,标记为?,其实相应的内容不是?,而是一些由插入和删除造成的无效字符。

reduced trie 和double-array,还有TAIL之间的关系如下

     1,如果在reduced trie中有一条这样的边:g(n, a) = m, then BASE[n] + a = m, and CHECK[m] = n.(在这个等式BASE[n] + a = m中,a代表的是一个字母,但是在相加的过程中会把它替换成一个整数,因为这个字母代表的是一条边,定义如下:“#”= 1, “a”=2,..."z"= 27)。在实际的编码中没有这么做,因为前面的定义只涉及到27个字符,实际应用中会涉及到更多的字符。


    2,如果m是一个独立的节点,那么string str[m] = b1b2...bn。then (a), BASE[m] < 0, (b), let p = - BASE[m], TAIL[p]= b1, TAIL[p + 1]= b2, TAIL[p + h + 1]= bn.

在整个论文中,这两个关系式是很重要的,所以请先用机械的方式把这两个关系记录下来,不要试图去理解,在检索,插入,和删除的过程中就会明白这样定义的目的和巧妙之处了。

trie树的检索

    先从检索说起吧,建设我们已经将K={bachelor, jar, badge, baby}中的关键字都处理过,放到reduced trie和TAIL中去了。以bachelor为例子作为检索吧。还是以下图为例。



    step1:把trie树的根节点放在BASE数组中第一个位置,然后从第一个位置开始,字母‘b’代表的弧的值为3,在上面定义过,从上面的关系1中可 以得到:BASE[n] + 'b' = BASE[1]  + 'b' = BASE[1] + 3 = 4 + 3 = 7; 然后看CHECK[7]的值是1.


    step2,:因为在第一步中BASE[1]  + 'b' 得到的值是正数,如果不是正数,那它的值就代表独立节点后字符串在TAIL中存储的起始位置。是正数,继续进行。把得到的值7作为BASE数组的新索引, 字母‘a’代表的弧的值为2,所以:BASE[7]+'a'=BASE[7] + 2 = 3 and CHECK[3]=7.先解释CHECK[3]=7吧,它表示的是指向节点3的弧是顺从节点7出发的。


    step3,4: 'c'代表的弧值是4, step2中求得的3作为BASE的新索引,BASE[3]+4 = 5, CHECK[5]=3,


    step5,再看上面的数组,得到BASE[5]=-1,这个负数表明,bachelor#中剩下的字母存储在TAIL 中,TAIL[-BASE[5]]=1,从索引1开始的。K中的其他关键字可以用同样的方式检索。不过开始位置都是BASE数组的第一个位置 -position 1.


     从上面的检索步骤我们可以看到,检索的过程中我们只是做了简单的读取数组中的值然后和其他值进行相加,没有进行整整的查找。所以这种reduced trie的实现,对于检索来说效率相当高。

关键字的插入

    在插入之前首先要做的事情就是初始化,BASE[1] = 1, 除此之外,BASE和CHECK中的其他数值都设置为0;0表示这些节点还没有被使用。


    1,当double-array都是空的时候,即BASE和CHECK中存储的元素都是0的时候。

    2,插入新的关键字的时候没有发生碰撞。

    3,插入新关键字的时候发生了碰撞,发生这种碰撞的有两种原因,第一种原因就是因为两个关键字有相同的前缀,解决的方法是为这些前缀包含的单词都创建一个 节点,并把对应的节点与边之间的关系写入到BASE,CHECK当中去。同时还对TAIL进行了操作,因为要提取TAIL中的字母。对于BASE和 CHECK在发生碰撞之前原有的内容不做改变。

    4,插入新关键字的时候发生碰撞,发生这种碰撞的原因不是因为单词之间有公共前缀,而是因为插入过程中某个关键字字母通过计算即将存放它所代表的弧的弧头节点已经被其他关键字的某个字母代表的边的弧头节点所占用。


    在插入之前着重的给大家讲解BASE和CHECK的概念:BASE中如果存储的是正数,表示的是一个基数,什么基数呢,假设有两个节点n,m同属于一条边 a,知道边a的弧尾节点,也就是非箭头指向的节点,知道这个边代表的值,比如是2,那么怎么求另一个节点是谁呢,那就需要BASE[n]+2=m,m便是 这个节点。如果BASE中存储的是负数呢,那就代表了一个关键字除了被边表示的字母之外,其他的字母都被存储在TAIL数组中,BASE[n]的绝对值就 代表存储位置的开始。CHECK呢,CHECK表示的是当前索引指示的节点有没有被其他边作为弧头节点或者弧尾节点来使用,如果为0表示没有,如果为正 数,表示有,同时这个正数也表示了是从哪个节点出发的弧指向了当前节点。


第一种情况:double-array都是空的情况,插入bachelor#步骤如下:

step1, 在BASE数组的第一个节点开始,‘b’所代表的边的值是3,有:BASE[1]+‘b’= BASE[1] + 3 = 4, and CHECK[4]= 0 != 1, 这说明什么呢,说明还没有弧指向第四个节点,那么我们可以把'b'代表的这条边指向第四个节点。


step2,CHECK[4]=0同时也表示着achelor#将被放在TAIL数组中去,然后定义'b'代表的边,g(1,'b')=4.


step3, 置BASE[4]= -POS = -1,这表示着bachelor#除了我们已经定义的边b之外,其他的字母被放到了TAIL数组中去了,起始位置是POS。同时CHECK[4]=1,表示指向节点4的边是从节点1发射出来的。


step4,POS <---9,表示下次可以出入的位置,算一下achelor#长度是8,则下次有效插入位置将是9.

下图显示了插入bachelor之后double-array和TAIL。



第二种情况:插入jar#

step1,在BASE的第一个位置开始,‘j’代表边的值是11,所以:BASE[1]+'j'=BASE[1]+11 = 12, and CHECK[12]=0 != 1,


step2,CHECK[12]=0表示了jar#中的其他部分要被插入到TAIL中去,同时也表示了插入的过程中是没有发生碰撞的,即存在公共前缀或者计算出来的节点已经被占用。从POS=9的位置开始,将ar#存入到TAIL中去。


step3,置BASE[12]= -9,CHECK[12]=1,表示从节点1出发的弧‘j’的尾部节点是第12个点。


step4, POS= 12,下一个有效插入位置。


从上面的两种插入情况来看,插入的过程并没有明显的区别,他们只是再理论上有所不同,即是不是在double-array为空的时候插入的。还有造成他们插入操作相同的原因是没有发生碰撞。

在 讲述第三种情况之前,先要说一个概念,考虑有这样一个函数   X_CHECK(LIST), 它返回最小的q,q满足以下两个条件:q>0 and 对于在LIST中的所有字母c都满足:CHECK[q + c] = 0。q的值总是从1开始,并且每次只增值1。记住这个重要的条件,就是q要满足LIST中的所有字母。


第三种情况,插入badge#:

step1,从BASE数组的第一位开始,‘b’代表的边的取值为3, 有:BASE[1]+'b'=4,and CHECK[4]=1,CHECK[4]中的非零值告诉我们,存在一条这样的边:它的起始位置是CHECK[4]=1,结束位置是节点4.。也就是‘b’边代表的弧尾节点和弧头节点。


step2, 由于在第一步中找到的值是整数4,则要继续进行下一个字母的寻找,4被用来当做BASE数组的新索引,BASE[4]=-1,说明搜索暂时停止,要进行字 符串的比对,比对那些字符串呢,一个是badge#剩余的没有进行查找的部分,一个是存储在TAIL数组中的部分,为什么要进行比对呢,比对的原因很简 单,看这个关键字之前是否已经插入了,如果已经插入了,那么badge#的再次插入是重复的,所以应该停止。在-BASE[4]=1的TAIL的起始位置 找到字符串achelor#,然后和剩余未插入的字符串adge#进行比较,比较的结果是不相同。但是细心的看一下,他们有相同的前缀,ba,b就不用说 了,因为已经有一条边用b代表了。那a呢,如果我们贸然的将剩下的字符插入到TAIL数组中,会有什么后果呢,后果就是BASE[4]中不知道该存储哪个 值好,存储-1吧badge#下次找的时候找不到,存储-9吧,那下次找bachehlor#的时候就找不到了。解决的办法是找到能够独立代表两个关键字 的方法,那就是要除去他们的公共部分之后为两个关键字都建立一个独立的节点,注意,这个独立的节点我们之前就提到过。


step3,把BASE[4]=-1存放到一个临时变量中去,TEMP<---BASE[4]


step4,对 adge#和achelor#的公共字符a使用X_CHECK[{'a'}]函数,CHECK[q+a]= CHECK[1+'a'] = CHECK[1+2]=CHECK[3]=0,这表示什么呢,节点3还没有被那条边当做弧头或者弧尾使用,我们可以把它当做从节点4发射出来的边‘a’的 弧头。q=1是BASE[4]的一个候选值,为什么说是候选值呢,等到后面就会理解了,暂时不用在意。然后有:BASE[4]=1, CHECK[BASE[4]+'a']=CHECK[1+2]=CHECK[3]<-4。它表示了我们又定义了一条新的边'a',从节点4起,到节 点3终止。注意到一点,因为这两个字符串的公共前缀只有a,如果换做其他字符串,不只是一个公共前缀字母,step3和step4就要循环操作。


step6, 接下来这个比较复杂,用英语把,要不然会打乱逻辑: to store remaining string 'chelor#' and 'dge#', calculate the value to be store into BASE[3] for two arc labels 'c',  and 'd', according to hte closest neighbour available by X_CHECK[{c,d}], 也就是说找到一个合适的q值,即BASE值,使得从节点3出发,加上这个值,得到的另外两个节点都没有被使用过,都可以用来分别作为弧c,d的弧头。

FOR c : CHECK[q+'c']= CHECK[1+4]=CHECK[5]=0=>available

FOR d : CHECK[q+'d']= CHECK[1+5]=CHECK[6]=0=>available

得到的q=1作为BASE[3]的候选值,BASE[3]=1,要理解候选的意思,就是这不是最终的值。


step7, 接下来就是计算BASE和CHECK的值,以找到合适的节点和合适的TAIL中位置来存储剩余的chelor#,在上步骤中,已经找到了BASE[3]的值,有:

BASE[3]+c = 5, BASE[5]=-TEMP, CHECK[5]=3,想一想,为什么这么直接的就做了呢,因为节点3到节点5的弧c已经能够把当前的两个有前缀的关键字区分开了,那么剩余的就没有必要 在细分了,直接放到TAIL数组中去了。同理,CHECK[5]=3代表从3出发,到5截止有一条弧c。


step8,剩余的字符'helor#'要放到TAIL中去了其实位置是1,计算一下,是6个字符,所有TAIL[7],TAIL[8]中的位置存储的字符就没有意义了。为什么呢,因为之前我们已经计算好了下一个有效的TAIL中的位置POS=9.


stpe9,对于'dge#'有同样的处理:BASE[3]='d'= 1+ 5 = 6, BASE[6]= -POS=12 and CHECK[6]=3, store 'ge#' int TAIL starting at POS.


step10,计算下一个TAIL中的有效存储位置:POS = 12 + length['ge#']= 15


插入badge#后的结果如图所示:



插入的第四种情况:插入'baby#':

step1: 还是从BASE中的第一个节点开始,计算步骤如下: 

         BASE[1]+'b'= BASE[1]+3 = 4 and CHECK[4]=1,

 BASE[4]+'a'= BASE[4]+2 = 3, and CHECK[3]=4,

BASE[3]+'b'= 4 and CHECK[4]=1 != 3,这是怎么回事呢,让我来清晰的解释一下:因为baby#的前两个字母是ba,按照规定,前缀都必须用单独的边表示,因为他们不足以区分有着相同前缀的 不同单词。所以接下来我们还得给'by#'中的b建立一条边,那么b这条边的起始节点有了,怎么找它的终止节点呢,按照我们当时的要求机械记忆的第一条 BASE[3]+'b'=1+ 3=4,那么我们就要用节点4来当做这个'b'代表的边,但是看一下前面的4已经被其他节点征用了。就产生了矛盾。那么到底是用4节点作为当前边'b'的 截止节点呢,还是它为原来的边贡献。这要做一下pk吧。但是还是寻找问题的根源吧,因为BASE[1]=1,BASE[3]等于,这次遇到了'b' 差生了矛盾,那么下次遇到其他单词中含有'b'也还有可能产生矛盾,那么为了根除这个矛盾,就得改变BASE[1]或者BASE[3]中的值,使得通过它 中的值计算出来的可用节点不在发生冲突。刚才说道pk,那么怎么pk呢,代价最小原则,看使用BASE[1]计算出来的在使用的节点个数多还是使用 BASE[3]计算出来的在使用的节点的个数多,计算的时候要包括即将插入的边的弧尾节点。


step2:设定一个临时变量TEMP_NODE1 <-BASE[3]+'b'= 4,


step3:把由节点3引申出来的边所代表的字母存放到LIST[3]中,很显然有'c','d', 把由节点1引申出来的边代表的字母存在LIST[1],即:b,j。


stpe4: 接下来就pk了,因为节点3刚才要新引申边'b'来着,所以要加上,compare(length[LIST[3]]+1 , length[LIST[1]]) = compare(3,2) .从节点1,引申出来的边少,就改变BASE[1]的值,如果情况是相反的,那么就改变BASE[3]中的值。


step5: 设定临时变量:TEMP_BASE <-BASE[1]=1, and calculate a new BASE using LIST[1] according to the closest neighbour available as follows:

            X_CHECK['b']=: CHECK[q+'b' ]= CHECK[1+3]= CHECK[4]!= 0

CHECK[q+'b' ]= CHECK[2+3]= CHECK[5]!= 0

CHECK[q+'b' ]= CHECK[3+3]= CHECK[6]!= 0

CHECK[q+'b' ]= CHECK[4+3]= CHECK[7]= 0

对于j X_CHECK['j']: CHECK[q+'j']= CHECK[4+11]= CHECK[15]=0=>available.

所以q=4是BASE[1]的候选值。BASE[1]=4


step6:store the value for the states to be modified in temporal variables: TEMP_NODE1 = TEMP_BASE+'b'=1+3 = 4, TEMP_NODE2= BASE[1]+'b'=7

把原来放在BASE[TEMP_NODE1]中的值放到BASE[TEMP_NODE2]中去,因为BASE[1]改变了,所以由BASE[1]计算出来的节点也要相应的做改变 BASE[TEMP_NODE2]=BASE[TEMP_NODE1]即:BASE[7]=BASE[4]=1,CHECK[TEMP_NODE2]=CHECK[4]=1


step7:BASE[TEMP_NODE1]=BASE[4]=1>0,说明什么呢,说明原来由节点4引申出去的边不能在从节点4出发了,应该从新的节点,即节点7出发,所以要做改动:

           CHECK[BASE[TEMP_NODE1]+E]=TEMP_NODE1, CHECK[BASE[4]+E]=CHECK[1+E]=4=>E = 2

           and modify CHECK to point to new status:CHECK[BASAE[4]+2]=CHECK[3]<-TEMP_NODE2=7


step8:因为更换BASE[1]的值,我们弃用了节点4,所以它将重新变为一个下次插入时候可用的节点。CHECK[TEMP_NODE1]=0,BASE[TEMP_NODE1]=0


step9:for 'j',TEMP_NODE1<-TEMP_BASE+'j'= 1+11 = 12, TEMP_NODE2<-BASE[1]+'j'= 4+11= 15,

         BASE[TEMP_NODE2]<-BASE[TEMP_NODE1] 即:BASE[15]=BASE[12]=-9 and SET the CHECK value for new node:              CHECK[TEMP_NODE2]=CHECK[15]=CHECK[12]=1.


step10: BASE[TEMP_NODE1]= BASE[12]= -9,说明BASE中存储的值是在TAIL中的有效存储剩余字符串的位置,所以可以重置其值。BASE[TEMP_NODE1]=BASE[12]=0,

CHECK[12]=0;


step11:继续考虑引起冲突的节点3,我们继续进行插入,BASE[3]+'b'=4,and CHECK[4]=0,这回节点4可以用了,有CHECK[4]=3,表示从节点3出发到节点4截止的边'b',那么可以很直观的看出,到目前位置,这条边足够能够把baby#和其他单词区分开,则右BASE[4]=-15, TAIL[POS]= TAIL[15]= 'y#',


step12, 重新计算POS的有效值:POS+length['y#']= 17.

最终插入结果如下图:



删除关键字

     关 键字的删除首先要找到double-array中是否有存储此关键字。就像插入过程的case2那样,只是操作有所不同,需要把对应关键字的独立节点的 BASE中存储的指向TAIL数组中的有效位置清空,即变成0.同时CHECK也需要置为0.表示指向独立节点的边被删除。

下面以删除‘badge#’为例:

stpe1:从BASE数组的第一个位置开始,对‘badge’的前三个字节:

BASE[1]+'b'= BASE[1]+3= 4+3 = 7, and CHECK[7]=1

BASE[7]+'a'= BASE[7]+2 = 1+2 = 3, and CHECK[3]=7

BASE[3]+'d'= BASE[3]+ 5= 1+5 = 6, and CHECK[6]=3

BASE[6]=-12<0 ==> separate node, 独立节点BASE中的值指示了剩余字符串在TAIL中存储的起始位置.

step2:将给定的字符串剩余部分和TAIL中存储的剩余部分进行比较,compare('ge#', 'ge#').

step3: 两个字符串的比较结果相等,所以重置指向TAIL的BASE[6],和去掉指向独立节点的边:BASE[6]<-0 , CHECK[6]<-0

 由于指向TAIL中'ge#'的独立节点BASE的值置成了0, 那么说明'ge#'再也没有办法被读取了,便成了没有用的内容:garbage,这些空间可以供以后的插入字符使用。


评估

其余的内容都是对这个算法的时间和空间效率的评估,对理解算法帮助不大。等有空的时候再做翻译。同时,将这篇文章的pdf上传,希望对大家有帮助,特别的要说明一下,pdf中源码有错误之处,我已经改正,在文章开头的连接中。http://download.csdn.net/detail/zzran/4966777

 

 

引言

 

在许多的信息检索应用中,很多地方都需要以前缀匹配的方式来检索输入的字符串。比如:编译器的词法分析、目录检索、拼写检查、中文分词的词库等自然语言处理相关的应用。为了提高检索的效率,我们通常把字符串构建成Trie树的形式。Trie树的每个结点是一个数组,数组中存储着下一个结点的索引信息。如对K=set{baby,bachelor,badge,jar}建立Trie树,结构如下图:

 

101213309.jpg

 

从上图中可以看出,基于Trie树对字符串进行检索、删除、插入是非常快的,但是空间开销却随着字符种类数和结点个数成比例增长,而且这种增加是成指数的增长。为了解决空间闲置问题,我们最容易想到的压缩方法就是把Trie树结点中的数组换成链表,这样就避免了数组中出现大量的NULL值情况,通俗地说,就是用左孩子右兄弟的方式来维持Trie树,如下图:

 

spacer.gif101234620.jpg

 

但是这样却带来了检索效率的降低,特别是一个结点有多个子结点的时候,例如,图中结点b的孩子结点a{c,b,d}三个孩子,这样在检索badge的时候,就需要遍历结点b-a-c-b-d-g-e(如上图红色的线条。)才能检索到。

 

本文提出了一种新的压缩方法,把Trie树压缩到两个一维数组BASECHECK中,数组BASECHECK合称Double-Array。在Double-Array中,非空的结点n通过BASE数组映射到CHECK数组中。也就是说,没有任何两个非空的结点会通过BASE数组映射到CHECK数组的同一个位置。Trie树中的每条边都能通过Double-Arrayo(1)的复杂度检索到。换句话说:如果一个字符串的长度为k,那么最多只需要o(k)的复杂度就可以完成检索。当字符串的数量众多时,使Double-Array中的空间尽可能充分利用就变得十分重要了。为了使Trie树能够满足存储海量字符串的需求,Double-Array中只储存字符串中的前缀,然后把字符串的剩余部分存储到字符数组中,我们把这个字符数组称为tail数组。通过Double-Arraytail数组的组合,就达到了尽可能节约内存,同时又能区别出任意两个字符串的目的。

 

 

 

 

 

详细解说DATrie(Double-Array Trie)

 

Trie是一种树形的数据结构。Trie中从根结点到叶子结点的每条路径代表着存储在Trie中的一个字符串(key)。这也就是说,路径中连接两个结点的边代表着一个字符。这跟我们通常了解到的把字符信息储存到结点中的树是不一样的。为了避免类似于the then这样的字符串在Trie中造成混淆,我们引入一个特别的字符# 用来表示每个字符串的结束。这样插入thethenTrie中时,实际上是插入the# then# 

 

为了更清晰地解说Trie,我们作如下的定义:

 

K 代表形成Trie的字符串集合。Trie结点和连接结点的边(arc组成。结点由Double-Array的下标来标记,边则是由字符来标记,如果一条边从结点n到结点m被标记成a,那么我们可以定义如下的函数g(n,a)=m 

 

对于集合K中的一个字符串STrie中形成的一条路径P,如果路径P中有结点m满足g(n,a)=m ,使得在Trie中检索S时,检索到字符a就已经能够将字符串STrie中的其它字符串区别开来,那么结点m称为separate node 。例如Figure 3中结点15、结点5、结点6、结点4都是separatenode

 

101332986.jpg

 

separatenode到叶子结点之间的字符串称之为结点m single string.STR[m]表示。例如图Figure3str[5]str[6]都是single string.S中删除singlestring后剩余的部分称为tail . (这里我没没理解清楚,只是觉得tailsingle string是一个意思,就是STR[m])树中只由从根结点到separatenode 之间的边组成的部分称为reduced trieFigure3就是reducedtrie的一个例子,用Double-Array和字符数组来存储tail信息。TAIL数组中的问号标志?用来表示废弃的空间(原来存储过信息,后来该位置不用了)。关于Double-Aarray TAIL的用法将会在插入操作和删除操作中详细解释。

 

Figure3 中,Double-Arrayreducedtrie 的关系如下:(到这里就很容易理解了,reduced trie表示的是一种结构,而Double-Array则表示reduced-trie这种结构的存储方式

 

第一、如果reduced trie中的一条边满足g(n,a)=m,那么对应到Double-Array中的实现则有:BASE[n]+a=m, CHECK[m]=n (对于边的标识字符,有如下约定 #=1 a=2 b=3 c=4…. )

 

第二、如果结点mseprate node,由此得到的tail字符串STR[m]=b1b2....bh那么有 BASE[m]<0 p=-BASE[m],则TAIL[p]=b1 ,TAIL[p+1]=b2 …… TAIL[p+h-1]=bh

 

以上两条关系将贯穿本文。

 

例如:

 

对于关系一:

 

Figure 3中根结点1到结点7 及字符b g(1,b)=7 -> BASE[1]+b = 4+3=7 (b=3) CHECK[7]=1 
对于关系二:

 

Figure 3中结点5BASE[5]=-1 ,对应到TAIL数组中p=-BASE[5]=1 TAIL[1]=h TAIL[2]=e TAIL[3]=l

 

那么从根结点到结点5,到STR[m],我们就可以检索到bachelor#这个字符串了。

 

实际上,有以下几点是需要提出来的:

 

一、结点1永远是Trie的根结点,所以Trie的插入和查找操作都是从BASE[1]开始。

 

二、CHECK[m]=n 所表达的意思是:结点m的父结点是n . 所以如果表述为father[m]=n可能更清楚一些。

 

三、Double-Array中,除CHECK[1]之外,如果CHECK[m]=0,则表示结点m是孤岛,是可用的。Double-Array实际就就是通过g(n,a)=m把这些孤岛连接成reduced trie这种树形结构。这一点在理解trie的insertion操作时会有帮助。

 

字符串的查找

 

通过Double-Array对字符串进行查找是非常快捷的。例如:用Double-Array查找Figure3中的字符串bachelor# 。执行步骤如下:

 

步骤1:从根结点1开始,由于我们已经定义b=3,所以:

 

BASE[n]+a=BASE[1]+b=4+3=7

 

我们也观察到CHECK[7]=1 所以这是一条通路,能往下走。

 

步骤2:由于BASE[1]=7>0,我们继续。用7作为BASE数组新的下标,由于bachelor#的第二个字符是a,所以

 

BASE[n]+a=BASE[7]+2=1+2=3 . 而且CHECK[3]=7

 

步骤3,4:按如上的方式继续,由于已经定义了c=4,我们有:

 

BASE[3]+c=BASE[3]+4=1+4=5而且CHECK[5]=3

 

步骤5BASE[5]=-1 ,表示剩下的字符串被存储到了TAIL数组中。从TAIL[ -BASE[5]]=TAIL[1]开始,检索剩下的字符串就可以用最常用的字符串比较方法了。

 

反复体会这个过程,我们能够发现:在trie中检索字符串只是直接地在数组中定位下一个节点。没有类似于广度或者深度优先搜索这样的查找操作。这种方式使得字符串的查找变得十分直截了当。

 

实际上在编写代码的时候,查找操作还是有一些细节需要注意的。这些细节就只能在代码里面体会了。

 

插入操作

 

DATrie的插入操作也是相当的直截了当。在插入过程中,无外乎以下四种情况:

 

Case 1 : 插入字符串时树为空。

 

Case 2: 插入字符串时无冲突。

 

Case 3: 插入字符串时出现不用修改现有BASE值的冲突,但是需要把TAIL数组中的字符信息展开到BASE数组中。(解决有公共前缀字符串的问题)

 

Case 4: 插入字符时出现需要修改现有BASE值的冲突。(位置占用冲突)

 

冲突的出现意味着在double-array中两个不同的字符通过g(n,a)得到了同样的m值,换话话说,两个不同的父结点拥有了同一个孩子(表现在double-array中就是两个字符争夺数组中的同一个空间位置)。上述的四种情况将通过在一棵空的Trie (Figure 4)中插入bachelor# (Case1) ;jar#(Case 2); badge#(Case 3) baby#(Case 4) 一一演示出来。我们定义DA_SIZE表示double-arrayCHECK数组的最大长度(BASE数组与CHECK数组大小相等),并且BASE数组和CHECK数组的长度可以动态地增加,数组默认以0来填充。

 

 

 

 

 

 

 

 

 

 

 

Case 1 : 插入字符串时Trie树为空。

 

101424177.jpg

 

(树为空时,BASE[1]=1,CHECK[1]=0, TAIL数组的起始位置POS=1; 这其实也就是编码时Trie的初始化。)

 

插入单词bachelor#将按如下步骤进行;

 

步骤1:从double-arrayBASE数组的下标1开始(也就是树的根结点)b的值为3,所以

 

BASE[1]+b=1+3=4, CHECK[4]=0≠1

 

步骤2CHECK[4]=0表示结点4separate node,由于b已经通过g(1,’b’)=4这种方式存储到double-array中了,所以单词剩下的部分achelor#直接插入到TAIL数组中即可。

 

步骤3:赋值BASE[4]=-POS=-1 表明achelor#将从POS位置开始插入到TAIL数组中。

 

步骤4:赋值POS=1+length(‘achelor#’) =9,表示下一个字符串存储到TAIL数组的起始位置。

 

Figure 5 显示了插入bachelor#reduced trie double-array的状态。

 

101447834.jpg

 

 

 

 

 

Case 2: 插入字符串时无冲突。

 

按如下的步骤插入单词jar#

 

步骤1:从double-arrayBASE数组下标1开始,由于已经定义j=11

 

BASE[1]+j=1+11=12 CHECK[12]=0≠1

 

步骤2:CHECK[12]=0表示结点12是空结点,可以将剩余的部分ar#直接插入到TAIL数组中去。插入的起始位置POS=9

 

步骤3:赋值BASE[12]=-POS=-9,同时赋值CHECK[12]=1 表明结点12是结点1的子结点。

 

步骤4:设置POS=12 , 表示下一个字符串存储到TAIL数组的起始位置。

 

实际上通过插入bachelor#jar#很难看出Case 1 Case 2之间的不同,所以其实他们的不同也是只概念上不同,而非操作上的不同。(由于Case 1Case 2的实现非常简单,也无需纠结于此。我们可认为这是作者玩的文字游戏)插入jar#reduced triedouble-array的状态如图Figure 6所示:

 

101509449.jpg

 

为了研究Case 3Case 4两种情况,我们需要定义一个函数X_CHECK(LIST)其功能是返回一个最小的整数qq满足如下条件:q>0 并且对于LIST中的每一个元素c,有CHECK[q+c]=0(对于X_CHECK函数,我们可以分两步理解,第一步:CHECK [m]=n表示结点m的父结点是n;第二步:设LIST={c1,c2,…cn},我们可认为q+c1,q+c2 … q+cn都是将要被领养的孩子,而这些孩子被领养必须有一个条件:没有父亲,而CHECK[q+c]=0即表示结点q+c没有父结点)

 

Case 3:公共前缀冲突

 

(公共前缀冲突是我自己起的名字,上文已经交代过,这种冲突的特点是无需修改以有的结点位置,即BASE数组中的非零值,只是把TAIL数组中的字符“解压缩”到double-array中)

 

通过插入单词badge#,我们可以认识到这种冲突。

 

步骤1:从BASE[1]开始,由于b=3,所以:

 

BASE[1]+b=1+3=4, CHECK[4]=1

 

CHECK[4]的非零值表示有一条边从结点CHECK[4](也就是结点1)到到结点4,就是Figure 6中的字符b所标识的边

 

步骤2:如果BASE[1]>0,我们直接到下一个结点就可以了,但是这里:

 

BASE[1]=-1

 

BASE[1]的值为负表明在trie中的查询已经结束,我们需要到TAIL数组中进行字符串比较。

 

步骤3:从pos=-BASE[1]=1作为TAIL数组的起始位置,比较achelor#和待插入字符串的剩余部分,也就是adge#。当两个字符串的比较失败,就用步骤456的方式把他们的公共前缀插入到double-array中。

 

步骤4:申明一个临时变量TEMP,并把-BASE[1]保存到这个临时变量中

 

TEMP à -BASE[1]

 

步骤5:计算字符串achelor#adge#的公共前缀字符aX_CHECK[{‘a’}]值:

 

CHECK[q+a]= CHECK[1+2]=CHECK[3]=0所以q=1

 

(q是从1开始递增试出来的)

 

这样1就作为了BASE[4]新的候选值,CHECK[3]=0也表明结点3是可以作为结点4的子结点。这样结点4和结点3就可以通过g(4,a)=g(4,2)=3关系式,把字符a存储到double-array中了。

 

步骤6:给BASE[4]赋新的值:

 

BASE[4]=1 , 同时赋值CHECK[BASE[4]+a]=CHECK[1+2]=CHECK[3]=4

 

这样trie中就有一条新的边诞生了,边从结点4开始到结点3结束,边的标识符号为a

 

注意:由于本例中只有一个公共前缀,所以步骤5和步骤6没有重复。但是如果有多个公共前缀,步骤56会重复执行多次,直到公共前缀都处理完。

 

步骤7:为了存储剩下的字符串chelor#dge#,我们需要为BASE[3]寻找新的候选值,使得字符c和字符d能够存储到double-array中,其计算方法为X_CHECK[{‘c’,’d’}]

 

对于’c’ : CHECK[q+’c’]=CHECK[1+4]=CHECK[5]=0 满足条件

 

对于’d’: CHECK[q+’d’]=CHECK[1+5]=CHECK[6]=0 满足条件

 

所以q=1, 赋值BASE[3]=1

 

步骤8:以字符串chelor#的首字符作为参数,计算字符串在BASECHECK数组中的结点编号。通过该结点可以在TAIL中定位到helor# 

 

BASE[3]+’c’=1+4=5

 

BASE[5]=-TEMP=-1 ,CHECK[5]=3

 

通过BASE数组建立到TAIL数组的引用,通过CHECK数组确定状态3到状态5的边。

 

(这里“状态”与“结点”是同一个意思)

 

步骤9:把字符串的剩余部分”helor#”存储到TAIL数组中,其起始位置为-BASE[5]=1,只是TAIL[7]TAIL[8]两个位置就变成空位了。(实际编程会有所不同

 

步骤10:对于新插入字符串剩余部分”dge#”:

 

BASE[3]+’d’=1+5=6 ;

 

BASE[6]=-POS=-12

 

CHECK[6]=3

 

然后把”ge#”存储到TAIL数组中。

 

步骤11:最后更新POS为下一次插入的起始位置,也就是TAIL中已用空间的的末尾。

 

POS=12+length[‘ge#’]=12+3=15

 

总的来说,当冲突发生了,字符串中产生冲突的公共前缀需要从TAIL数组中提取出来,然后存储到double-array中。冲突字符串(包括新插入的字符串)在double-array中关联的值(满足条件BASE[n]<0)都要转移到最近的空结点位置。(参考Figure 7)

 

101549544.jpg

 

Case 4:抢占位置引发的冲突

 

(现在进入到整篇文章最核心的地方了,也就是DATrie最难的地方)

 

就像Case 3 一样,BASE数组中的值必须进行修改才能解决冲突。插入”baby#”的步骤演示如下:

 

步骤1:根结点在BASE数组的下标1位置,所以从BASE[1]开始。

 

对于baby#中前三个字符而言,BASECHECK中的值如下;

 

BASE[1]+’b’=1+3=4, CHECK[4]=1

 

BASE[4]+’a’=1+2=3, CHECK[3]=4

 

BASE[3]+’b’=1+3=4, CHECK[4]=1≠3

 

CHECK[4]的计算结果出现前后不一致的现象表明有一个状态没有被考虑到。这也意味着结点1或者结点3BASE值需要进行修改。(这里可以这样理解:结点4作为子结点被父结点1和父结点3争夺,我们有两种方法解决冲突:结点1放弃孩子或者结点3放弃孩子。如果是结点1让步,那么就需要修改BASE[1],如果结点3让步,那么就需要修改BASE[3]

 

步骤2:申明变量TEMP_NODE1,并赋值TEMP_NODE1 = BASE[3]+’ b’=1+3=4

 

如果CHECK[4]=0,那么直接把剩余部分存储到TAIL中就可以了,但是事与愿违。

 

步骤3:分别把从结点3和结点1引出的边存储到list中,通过Figure 7有:

 

list[3]={‘c’,’d’}

 

list[1]={‘b’,’j’}

 

步骤4:由于我们的目的是让新插入的字符串与结点3进行关联(实际上修改BASE[1]或者修改BASE[3]哪种方案最优,是通过工作量来进行衡量的。因为修改BASE[n]同时需要修改BASE[n]的子结点位置,所以子结点数越少,工作量就越少),即字符’b’将给结点3带来一个新子结点,所以从结点3引出的边的个数需要加1。所以我们:

 

Compare(length(list[3])+1,list[1]) =compare(3,2)

 

如果length(list[3]+1)<length(list[1]),那么我们就修改结点3BASE值,但是由于length(list[3]+1)length(list[1]) ,我们修改结点1

 

步骤5:申明变量TEMP_BASE,赋值

 

TEMP_BASE=BASE[1]=1

 

并且用X_CHECK计算BASE[1]新的候选值:

 

X_CHECK(‘b’): CHECK[q+’b’]=

 

CHECK[1+3]=CHECK[4]≠0

 

CHECK[2+3]=CHECK[5]≠0

 

CHECK[3+3]=CHECK[6]≠0

 

CHECK[4+3]=CHECK[7]=0满足条件

 

而且对于

 

X_CHECK(‘j’): CHECK[q+’j’]=

 

CHECK[4+11]=CHECK[15]=0满足条件

 

所以q=4合法,赋值 BASE[1]=4

 

步骤 6:对于’b’ ,把将被修改的状态值存储到临时变量中:

 

TEMP_NODE1 TEMP_BASE+ ‘b’=1+3=4

 

TEMP_NODE2 BASE[1]+ ‘b’=4+3=7

 

BASE值从旧的状态更新到新的状态:

 

BASE[TEMP_NODE2]---- BASE[TEMP_NODE1] 也就是

 

BASE[7]=BASE[4]=1

 

同时把CHECK值也更新

 

CHECK [TEMP_NODE2 ] =CHECK [7] ---- CHECK [4]=1

 

(这里其实也好理解,对于结点1,原来的子结点为412BASE值更新后,子结点随之变成715,把原来结点4和结占12BASECHECK值转移到结点7和结点15就完成了子结点的更新了,如果子结点还有孩子,比如结点4的子结点为3,那么更新后,结点3的父结点将不再是结点4,而变成结点7)

 

步骤7:由于

 

BASE[TEMP_NODE1]=BASE[4]=1>0 <结点4有子结点>

 

把结点4的所有子结点的父结点更新为结点7

 

CHECK [ BASE[4]+2] = CHECK [l+2]= CHECK [3] TEMP_NODE2 =7

 

步骤8:结点1的子结点从结点4变成结点7后,结点4已经空置了。所以需要进行标记:

 

BASE[4]=0

 

CHECK[4]=0

 

 

 

 

 

 

 

同样地,对于’j’<导向子结点12的边>

 

步骤9:把将被修改的状态值存储到临时变量中:

 

TEMP_NODE1 TEMP_BASE+ ‘j’=1+11=12

 

TEMP_NODE2 BASE[1]+ ‘j’=4+11=15

 

BASE值从旧的状态更新到新的状态:

 

BASE[TEMP_NODE2] ----BASE[TEMP_NODE1] 也就是

 

BASE[15]=BASE[12]=-9

 

同时把CHECK值也更新

 

CHECK [ TEMP_NODE2 ]=CHECK [15] ---- CHECK [12]=1

 

步骤10:由于

 

BASE[TEMP_NODE1]=BASE[12]=-9<0

 

BASE[12]没有子结点,所以只需要把结点12置空就可以了。

 

BASE[12]=0

 

CHECK[12]=0

 

这样的话由baby中的字符’b’产生的冲突就被解决了。最后,把新插入字符串的剩余部分存储到TAIL数组中就OK了。

 

步骤11:从产生冲突的那个结点(即结点3)开始,重新计算由字符’b’得到的新结点,并把它存储到临时变量TEMP_NODE

 

TEMP_NODE=BASE[3]+’b’=4

 

步骤12:把字符串在TAIL数组存储的起始位置存储到BASE数组中

 

BASE[TEMP_NODE]=BASE[4]=-POS=-15

 

步骤13:把字符串的剩余部分存储到TAIL数组中

 

TAIL[POS]=TAIL[15]+’y#’

 

步骤14:更新POS为下一次插入的起始位置,也就是TAIL中已用空间的的末尾。

 

POS=15+length[‘y#’]=15+2=17

 

小结一下,当double-array中发生了位置占用冲突,我们需要修改产生冲突结点的父结点BASE值,对于这两个父结点(对应到程序中就是preCHECK[cur],具体修改哪一个取决于其子结点的个数,子结点个数少的父结点将被修改。这样冲突就可以得到解决,字符串也能顺利地插入到trie中了。插入后的结果如Figure 3所示:

 

 

 

101620290.jpg

 

Trie的删除操作

 

Trie的删除操作也是非常简单。把前面的插入熟悉后,自己看论文《An Efficient Implementation of TrieStructures》就能懂了,这里也就不继续解说了。

 

论文剩下的部分就是性能的评估,感兴趣的可以自行了解。最后有实现的伪代码,还是有一定的参考价值的。

 

 

 

其实,字符串处理的相关数据结构,无非是利用了字符串的前缀和后缀信息。比如SuffixArray(后缀数组)利用的是后缀信息,Trie树,利用的是前缀信息。

 

理解DATrie树,我们应该认识到,DATrie是一种数据结构,理解数据结构,只需要理解数据结构对数据的”增删改查”四种操作就可以了。对于DATrie,其核心在于理解插入操作;而插入操作的难点在于理解BASE数组和CHECK数组,BASE数组和CHECK数组的难点在于插入时出现冲突的解决方案。所以,DATrie树的难点只有一个:冲突解决方案。

 

在学习的过程,反复在纸上画出trie的结构,自己推理double-array值对于理解trie是非常有帮助的。

 

最后提供一个测试样例:“ba” “bac” be”“bae,因为没有这个样例,我在编码的时候被困了好几天。

 

在我的笔记本电脑上<i5+4G内存+32位win7+2.4GZ>,用DATrie 插入38万数量的词典,用时240084毫秒,查询用时299毫秒。

 

最后还是贴出代码吧!

 

 

 

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
package com.vancl.dic;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
public class DATrie {
                                                                                                                                                                                                                                                                                                                
     final  int DEF_LEN=1024;
     final  char END_TAG='#';//#在CWC中的value=1
     int[] base = new int[DEF_LEN];
     int[] check = new int[DEF_LEN];
     char[] tail =new char[DEF_LEN];
                                                                                                                                                                                                                                                                                                                
     int tailPos;
                                                                                                                                                                                                                                                                                                                 
    public DATrie(){
        base[1]=1;tailPos=1;
        Hash.put(END_TAG,1);
    }
                                                                                                                                                                                                                                                                                                                
    /*
     * 查找一个词是否在Trie树结构中
     * */
    public boolean retrieval(String word){
        //执行查询操作
        int pre=1,cur=0;
        for(int i=0;i<word.length();i++){
            cur=base[pre]+GetCode(word.charAt(i));
                                                                                                                                                                                                                                                                                                                        
            if(check[cur]!=pre)return false;
                                                                                                                                                                                                                                                                                                                        
            //到tail数组中去查询
            if(base[cur]<0 ){
                int head=-base[cur];
                return MatchInTail(word, i+1, head);
            }
            pre=cur;
        }
        //这一句是关键,对于一个串是另一个字符串 子串的情况
        if(check[base[cur]+GetCode(END_TAG)]==cur)return true;
        return false;
    }
                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                
    public void insert(String word){
        word+=END_TAG;
                                                                                                                                                                                                                                                                                                                    
        for(int i=0,pre=1,cur;i<word.length();i++){
            cur=base[pre]+GetCode(word.charAt(i));
                                                                                                                                                                                                                                                                                                                        
            //容量不够的时,扩容
            if(cur>=base.length)extend();
                                                                                                                                                                                                                                                                                                                        
            //空白位置,可以添加,这里需要注意的是如果check[cur]=0,则base[cur]=0成立
            if( check[cur] == 0){
                check[cur]=pre;
                base[cur]=-tailPos;
                toTail(word,i+1); //把剩下的字符串存储到tail数组中
                return;//当前词已经插入到DATire中
            }
            //公共前缀,直接走
            if(check[cur]==pre && base[cur]>0 ){
                pre=cur;continue;
            }
            //遇到压缩到tail中的字符串,有可能是公共前缀
            if(check[cur] == pre && base[cur]<0 ){
                //是公共前缀,把前缀解放出来
                int new_base_value,head;
                head=-base[cur];
                                                                                                                                                                                                                                                                                                                            
                //插入相同的字符串
                if(tail[head]==END_TAG && word.charAt(i+1)== END_TAG)
                    return ;
                                                                                                                                                                                                                                                                                                                            
                if(tail[head]==word.charAt(i+1)){
                    int ncode=GetCode(word.charAt(i+1));
                    new_base_value=x_check(new Integer[]{ncode});
                    //解放当前结点
                    base[cur]=new_base_value;
                    //连接到新的结点
                    base[new_base_value+ncode]=-(head+1);
                    check[new_base_value+ncode]=cur;
                    //把边推向前一步,继续
                    pre=cur;continue;
                }
                /*
                 * 两个字符不相等,这里需要注意"一个串是另一个串的子串的情况"
                 * */
                int tailH=GetCode(tail[head]),nextW=GetCode(word.charAt(i+1));
                new_base_value=x_check(new Integer[]{tailH,nextW});
                base[cur]=new_base_value;
                //确定父子关系
                check[new_base_value+tailH] = cur;
                check[new_base_value+nextW] = cur;
                                                                                                                                                                                                                                                                                                                            
                //处理原来tail的首字符
                base[new_base_value+tailH] = (tail[head] == END_TAG) ? 0 : -(head+1);
                //处理新加进来的单词后缀
                base[new_base_value+nextW] = (word.charAt(i+1) == END_TAG) ? 0 : -tailPos;
                                                                                                                                                                                                                                                                                                                            
                toTail(word,i+2); return;
            }
            /*
             * 冲突:当前结点已经被占用,需要调整pre的base
             * 这里也就是整个DATrie最复杂的地方了
             * */
            if(check[cur] != pre){
                int adjustBase=pre;
                Integer[] list=GetAllChild(pre);//父结点的所有孩子
                Integer[] tmp=GetAllChild(check[cur]);//产冲突结点的所有孩子
                                                                                                                                                                                                                                                                                                                            
                int new_base_value;
                if(tmp!=null && tmp.length<=list.length+1){
                    list=tmp;tmp=null;
                    adjustBase=check[cur];
                    new_base_value=x_check(list);
                }else{
                    //由于当前字符也是结点的孩子,所以需要把当前字符加上
                    list=Arrays.copyOf(list, list.length+1);
                    list[list.length-1]=GetCode(word.charAt(i));
                    new_base_value=x_check(list);
                    //但是当前字符 现在并不是他的孩子,所以暂时先需要去掉
                    list=Arrays.copyOf(list, list.length-1);
                }
                                                                                                                                                                                                                                                                                                                            
                int old_base_value=base[adjustBase];
                base[adjustBase]=new_base_value;
                                                                                                                                                                                                                                                                                                                            
                int old_pos,new_pos;
                //处理所有节点的冲突
                for(int j=0;j<list.length;j++){
                    old_pos=old_base_value+list[j];
                    new_pos=new_base_value+list[j];
                    /*
                     * if(old_pos==pre)pre=new_pos;
                     * 这句代码差不多花了我3天的时间,才想出来
                     * 其间,反复看论文,理解DATrie树的操作过程。
                     * 动手在纸上画分析DATrie可能的结构。最后找到
                     * 样例:"ba","bac","be","bae" 解决问题
                     * */
                    if(old_pos==pre)pre=new_pos;
                                                                                                                                                                                                                                                                                                                                
                    //把原来老结点的信息迁移到新节点上
                    base[new_pos]=base[old_pos];
                    check[new_pos]=check[old_pos];
                    //有后续,所有孩子都用新的父亲替代原来的父亲
                    if(base[old_pos]>0){
                       tmp=GetAllChild(old_pos);
                        for (int k = 0; k < tmp.length; k++) {
                            check[base[old_pos]+tmp[k]] = new_pos;
                        }
                    }
                    //释放废弃的节点空间
                    base[old_pos]=0;
                    check[old_pos]=0;
                }
                //冲突处理完毕,把新的单词插入到DATrie中
                cur=base[pre]+GetCode(word.charAt(i));
                if(check[cur]!=0){
                    System.err.println("collision exists~!");
                }
                base[cur]=(word.charAt(i)==END_TAG)?0:-tailPos;
                check[cur]=pre;
                                                                                                                                                                                                                                                                                                                            
                toTail(word,i+1);return;//这里不能忘记了
            }
        }
    }
                                                                                                                                                                                                                                                                                                                
    //到Tail数组中进行比较
    private boolean MatchInTail(String word,int start,int head){
        word+=END_TAG;
        while(start<word.length()){
            if(word.charAt(start++)!=tail[head++])return false;
        }
        return true;
    }
    /*
     * 寻找最小的q,q要满足的条件是:q>0 ,并且对于list中所有的元素都有check[q+c]=0
     * */
    private int x_check(Integer[] c){
        int cur,q=1,i=0;
         do{
            cur = q + c[i++];
            if(cur >= check.length) extend();
            if(check[cur] != 0 ){
                i=0;++q;
            }
        }while(i<c.length);
        return q;
    }
    //寻找一个节点的所有子元素
    private Integer[] GetAllChild(int pos){
        if(base[pos]<0)return null;
        ArrayList<Integer> c=new ArrayList<Integer>();
        for(int i=1;i<=Hash.size();i++){
            if(base[pos] + i >= check.length)break;
            if(check[base[pos]+i] == pos)c.add(i);
        }
        return c.toArray(new Integer[c.size()]);
    }
                                                                                                                                                                                                                                                                                                                
    public Integer GetCode(char ch){
        return Hash.GetCode(ch);
    }
                                                                                                                                                                                                                                                                                                                
    //将字符串的后缀存储到tail数组中
    private void toTail(String w,int pos){
        //如果容量不足,就扩容
        if(tail.length-tailPos < w.length()-pos)
            tail=Arrays.copyOf(tail, tail.length<<1);
                                                                                                                                                                                                                                                                                                                    
        while(pos<w.length()){
            tail[tailPos++]=w.charAt(pos++);
        }
    }
                                                                                                                                                                                                                                                                                                                
    private void extend(){
        base=Arrays.copyOf(base, base.length<<1);
        check=Arrays.copyOf(check, check.length<<1);
    }
}

 

Hash.java

 

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
package com.vancl.dic;
import java.util.HashMap;
public class Hash {
    private static final HashMap<Character,Integer> hash=
            new HashMap<Character,Integer>();
                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                  
    public static int GetCode(char ch){
                                                                                                                                                                                                                                                                                                      
        if(!hash.containsKey(ch)){
            hash.put(ch, hash.size()+1);
        }
                                                                                                                                                                                                                                                                                                      
        return hash.get(ch);
    }
                                                                                                                                                                                                                                                                                                  
    public static void put(char ch,int value){
        hash.put(ch, value);
    }
                                                                                                                                                                                                                                                                                                  
    public static int size(){
        return hash.size();
    }
                                                                                                                                                                                                                                                                                                  
}

 

Test程序:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.vancl.dic;
import junit.framework.Assert;
import org.junit.Test;
public class DATrieTest {
    @Test
    public void testInsert(){
        String[] s={"bachelor","jar","badge","baby"};
        String[] s2={"ba","bac","be","bae"};
        DATrie dat=new DATrie();
        for (String string : s2) {
            dat.insert(string);
        }
        for (String string : s2) {
            Assert.assertEquals(true, dat.retrieval(string));
        }
    }
}

 

参考文档:

 

《An effcient Implements of Trie Structures》

 

http://blog.csdn.net/dingyaguang117/article/details/7608568

 

http://www.iteye.com/topic/391892

 

由于文章是从word中粘贴出来的,排版效果很难看,这里我转成了pdf,可以在附件中下载。

 

posted on 2014-04-01 17:13  agilezing  阅读(365)  评论(0编辑  收藏  举报

导航