本站有个翻译的文章,名字叫Ukkonen 的后缀树算法的清晰解释。这篇文章写得不错,可是仍是犯了错误的。 算法
我按照这篇文章的说明实现了所谓的Ukkonen算法,可是在测试时出现了错误,过后,进行了大量的排查(由于我一开始认为确定不是文章的问题是个人问题,花了2天没解决,最后抛开“文章正确”的观点才解决) 测试
下面,是我从新整理后的Ukkonen算法,采用的例子仍是"abcabxabcd"。 spa
生成后缀数和绘图的C++源码在http://www.oschina.net/code/snippet_593413_38384 .net
一开始依次插入'a'、'b'、'c'三个字符时,后缀树以下: 翻译
、 指针
图中,绿色的节点表示活动节点;节点内填充黄色,表示为根节点;没有活动边,说明活动长度为零。 code
图中的黑色箭头表示的边,x:(n,#)格式的,表示一个指向叶节点的边,x为该边第一个字符,n为该边起始的位置。正常的边会列出其包含的全部字符。 索引
叶节点的值,表示该叶节点表明的后缀的起始位置。 ip
从上面的例子说明,当向一个活动长度为零的活动点插入已有的边(的首字符)不存在的字符时,会插入新的边和叶节点。剩余后缀数会在扫描前+1,插入后-1,所以保持为0。 字符串
下一步,插入'a'(当前扫描位置3):
由于字符'a'已经存在于某个边中,因而咱们就将活动三元组置为(root,'a',1),剩余后缀数,并完成本次字符的扫描,事实上,活动边不是表示为'a',而是3——这表示活动边是以文本的索引为3的字符起始的。即,活动三元组为(root,3,1)。剩余后缀数在扫描前+1,而本次没有插入后缀,所以剩余后缀数为1。
图中,绿色箭头表示一个活动边,而该边最后冒号后面的数字表示活动长度。
下一步,插入'b'(当前扫描位置4):
由于字符'b'也已经存在于活动边的下一个位置,因而咱们就将活动三元组置为(root,3,2),剩余后缀数为2,并完成本次字符的扫描。
下一步,插入'x'(当前扫描位置5):
要插入'x',当前边为"abcabx",而活动长度为2,该边下一个字符是'c'不是'x',所以咱们将该边分割开,也就是图中的ab边和c:(2,#)边及二者间的节点,而且向该节点添加一个新的边x:(5,#)和叶节点3。
为啥叶节点是3?由于当先后缀数为3,表示咱们插入的后缀是"abx",在该后缀前面的部分是"abc",由于"ab"是隐含在已有后缀树里的,实际插入的边是从'x'开始的,可是叶节点倒是从第二个'a'开始的,其索引为3。简化计算的话,就是:当前扫描位置+1-剩余后缀数。
在插入新的边以后,剩余后缀树-1;由于剩余后缀数>0,咱们要重复插入后缀的操做,直到剩余后缀数==0或者遇到该后缀被隐含的状况。
在插入新的边和节点后,须要更新活动三元组。由于活动节点是根节点,操做为:活动边(索引)+1,活动长度-1,所以新的三元组为(root,4,1)。
文本索引4的位置,字符为'b',据此,咱们肯定新的边为b:(1,#)。
由于循环,咱们将该边分割、插入了新的边x:(5,#)和叶节点4。
同时,根据规则,咱们要添加后缀指针,后缀指针会添加到扫描一次字符的过程当中,由于分割边出现的新的内部节点之间(从旧到新)。假设一次扫描中,因分割出现的新节点依次为a,b,c;则应添加后缀指针,a -> b和b -> c。
图中,后缀指针用红色箭头表示。
此时,更新三元组为(root,5,0),而剩余后缀数为1。
下一次插入操做,和插入索引一、二、3位置的字符时同样的规则,在根节点(活动节点)添加了新的边x:(5,#)和叶节点5。
剩余后缀数为0,本次扫描结束。
下一步,插入'a'(当前扫描位置6):
更新三元组为(root,6,1),剩余后缀数1
下一步,插入'b'(当前扫描位置7):
更新三元组为(root,6,2),剩余后缀数2;由于抵达了新的点,三元组重置为(green,6,0)
下一步,插入'c'(当前扫描位置8):
更新三元组为(green,8,1),剩余后缀数3;
下一步,插入'd'(当前扫描位置9):
进行了一系列的后缀插入:
剩余后缀数+1(如今是4);
分割c:(2,#)=>0边,而后由于活动点有后缀指针,所以活动点重置为该点(root经边b到达该点),而活动边和活动长度保持为8和1
分割c:(2,#)=>1边,生成新的后缀指针。此时,活动点没有后缀指针了。活动点也不是根节点。为了找到下一个活动点,有两种方法。
1)总而言之,咱们知道剩余后缀数和当前扫描位置,换句话说,咱们知道当前要插入的后缀,所以从根节点沿着该后缀查找就是了。
当前扫描位置9,剩余后缀数2,所以当前要插入的后缀从8起始,插入的是[8,9]即"cd",重置活动三元组为(root,8,1):即根节点、当前扫描位置-剩余后缀数+1,剩余后缀数-1
而后对这个三元组进行修正,也就是沿着后缀树走,直到活动长度为0或者小于活动边的长度。
所以,咱们找到了root经边c到达的节点。
2)将活动三元组更新为(当前活动点,当前扫描位置-当前活动长度,当前活动长度)
从该活动点向父节点走,每次移动到父节点,都要让活动边(索引)减去通过的边的长度,而当前活动长度加上通过的边的长度
直到根节点,此时的操做和活动节点原本就在根节点是同样的:活动边(索引)+1,活动长度-1
或者移动到的节点有后缀指针,那么,咱们沿着后缀指针移动一次活动节点,活动边和活动长度不变
到达根节点或者沿后缀指针移动后,也要进行修正,沿着后缀树走,直到活动长度为0或者小于活动边的长度。
这两种方法无论采用哪种,都会到达同一个点。通常说,树较小时,第一种更简单,而树比较复杂的时候,第二种更好。
分割c:(2,#)=>2边,生成新的后缀指针。活动三元组更新为(root,9,0),剩余后缀数1
插入新的边d:(9,#)和新的点9
本次扫描结束。
…………
这个过程能够一直持续下去,知道接受了一个终止标识符,或者进行结束操做。
由于扫描'd'后,剩余后缀数已经减小到0了,咱们的结束操做只是将根节点设置为一个后缀标识节点,表明空后缀。
事实上,由于'd'在字符串中仅在最后出现了一次,它的行为和扫描终止标识符是相同的。若是咱们并不真的插入终止标识符(甚至咱们都不用去比较终止标识符和活动位置下一个字符),将每一次添加仅含终止标识符的边和叶节点的操做替换为修改节点的属性,那么就是一个标准的结束操做了。
总的来讲,咱们只作了一个修改,那就是活动三元组的更新规则。
确切地说,是活动点不是根节点且没有后缀指针的时候的更新规则。
本文中的图都是用graphviz生成的。