您的位置:首页 > 编程语言

后缀树系列二:线性时间内构建后缀树(包含代码实现)

2015-09-09 11:50 330 查看
 上一篇文章已经介绍了后缀树的前前后后的知识,并且采用各种技巧逼近线性时间了,至于具体怎么操作大家看完之后应该多多少少有点想法了。而之所以将本文跟上一篇文章分开,主要考虑有三:

第一,和在一起文章就会太长了,看的头疼。
第二,理论跟实现本来就有差异,本文中一些具体实现并没有严格遵守上文中的条条框框。当然了,主题思想是一样的。
第三,本文会从具体实现的角度,在实现过程中进一步阐述上文中的原理。换个角度看问题,会加深理解。

  声明一下,原本我打算自己操刀来写,但是我在网络上搜索到一篇很好的资源,本文也基本上沿着它的思路来写,建议英文不太差的小伙伴们自己先看看原文。当然喽,我也会把本文跟前一篇文章结合起来一起阐述,让大家体味到理论跟实现是如何水乳交融的。

上文中提到我们最终只是需要沿着去除头尾之后的“尾部链表”以及根节点更新增加节点就好了。但是我们怎么来做呢?带着这个问题,正式进入我们的实现环节!

首先引入两个概念:

当前活跃点active point:这是一个三元组包含了三个信息(活跃点,活跃边,活跃半径)。初始值为(root,'\0x',0),代表活跃点为根节点,没有活跃边,活跃长度为0;
计数器remainder: 记录我们在当前阶段需要插入的叶子节点数目。注意到我们每次插入都会增加新的叶子节点,具体原因见上一篇文章。初始值为1,每次进入新阶段增加1,每次从活跃点新增一个叶子节点减少1。

  我们每一阶段的目标都是把当前阶段的所有后缀插入到后缀树中。至于叶子节点,我们跟上文所说的一样,采用 [ , #]格式,每到一个新的阶段自动更新。

  好了,下面给出构建后缀树的三个定理:

  规则一(活跃点为根节点时候的插入):

插入叶子之后,活跃节点依旧为根节点;
活跃边更新为我们接下来要更新的后缀的首字母;
活跃半径减1;

  规则二(后缀链表):

每个阶段,当我们建立新的内部节点并且不是该阶段第一次建立内部节点的时候,我们需要用指针从当前内部节点指向本阶段最近一次建立的内部节点

规则三(活跃点不为根节点时候的插入):

如果当前活跃点不是根节点,那么我们每次从活跃点新增一个叶子之后,就要沿着后缀链表到达新的点,并更新活跃节点;如果不存在后缀链表了,我们就转移到根节点,将活跃节点更新为根节点。活跃半径以及活跃边不变。

是不是感觉这里面的“后缀链表”跟我们之前谈到的尾部链表非常相似啊,而且这个活跃点的概念是不是跟之前的尾部链表也有千丝万缕的关系呢,哈哈。留这些疑问!待会儿再说。我们先来通过一个例子来具体体验一把怎么建立后缀树。例子来自上文中提到的那篇原文。

  我们为字符串abcabxabcd建立后缀树吧。

  一开始活跃点三元组为(root,’\0’,0),计数器remainder为1;

第一阶段插入字符a,#=1。

  我们从当前活跃节点(根节点)出发看看有没有那个边的第 0(活跃半径) 个字母是a,发现没有,于是按照规则一插入叶子节点。活跃边更新为接下来要插入的后缀首字母,但是由于已经没有需要插入的了,故而活跃边也不变便;由于活跃半径已经是0,故而不用减1了;计数器remainder减少1变成0。我们获得了这样一棵树





  因为当前#=1,故而上图的这个后缀树就是下图(注意一下,区间[0,1]的含义是第一个字符,这点跟之前的表示方法不太一样)





  此时三元组为(root,’\0’,0),remainder为0;

第二阶段,插入字符b,#=2; 三元组不变仍为(root,’\0’,0),remainder增加1,等于1;

  我们从当前活跃节点(根节点)出发看看有没有哪个边的第 0(活跃半径) 个字母是b,发现没有,于是按照规则一插入叶子节点。情况同上,三元组为(root,’\0’,0),remainder为0;我们获得了这样一棵树:







  因为#=2,故而左图等同于右图。

第三阶段,插入字符c;#=3;三元组不变仍为(root,’\0’,0),remainder增加1,等于1;

  跟第二阶段类似,三元组为(root,’\0’,0),remainder为0;我们获得了这样一棵树:







  因为#=3,故而左图等同于右图。

第四阶段,插入字符a; #=4; 三元组为(root,’\0’,0),remainder增加1,等于1;

  这时,我们从当前活跃节点(根节点)出发看看有没有哪个边的第 0(活跃半径) 个字母是a。结果发现了!!!那么此时,我们更新三元组为(root, ‘a’, 1), remainder不变。更新完系数之后,我们就进入下一阶段了。

  为什么?还记得上一篇文章中,”当新后缀出现在原先后缀树中”是怎么办么?哈哈,就是更新系数后不管了。

  我们获得了这样一棵树:







  看出来了吧,树的形状没有变,但是叶子节点的边自动更新了。这也就是上一篇文章中提到了自动更新叶节点。按上文的说法这里应该有尾部链表从第一个点(边为abca)指向第二个点(边为bca),第二个点指向第三个点(边为ca),第三个点指向根节点,但是呢,显然这个链表上除了根节点都是叶节点,没有保存的必要啊。所以这里没有看到“尾部链表”的痕迹。这也是一种优化,但是,后面你会见到它的。哈哈,所谓神龙见首不见尾!我们接着来看。

第五阶段,插入字符b;#=5; 三元组为(root,’a’,1),remainder增加1,等于2;

这时,我们从当前活跃点(根节点)出发看看边首字母为a(活跃边)的第1(活跃半径)个字母是b。结果又发现了!!

  注意一下,之前因为没有明确的活跃边,故而可以随便找边,但是确定了活跃边之后就只能沿着活跃边来找了。

  因为这一次我们需要插入的后缀除了b之外,还有上次剩余的,故而我们需要插入的后缀是ab ,b。活跃边不变,只是活跃半径需要增加1,等于2。于是三元组变成了(root,’a’,2),计数器remainder不变,仍然为2。

  我们获得了这样一棵树:





第六阶段,插入字符x; #=6; 三元组为(root,’a’,2), remainder增加1,等于3;我们需要插入的后缀有abx,bx,b

  这时,我们从当前活跃点(根节点)出发看看沿着活跃边(边首字母为a)的第2(活跃半径)个字母是不是x。结果没发现!

  于是我们按照规则一,从当前生长点(活跃点沿着活跃边走活跃半径个字符)增加一个叶子,插入x,也就是代表了abx。

获得了这个树:





  插入完abx之后我们需要插入bx了,但是我们需要先调整三元组。活跃边首字母就应该变成b了,活跃半径减少1,于是我们的三元组变成了(root,’b’,1),remainder减少1,等于2。根据规则一,我们再从当前活跃点(根节点)出发看看沿着活跃边(边首字母为b)的第1(活跃半径)个字母是不是x。结果不是!于是再按照规则一,从当前生长点(活跃点沿着活跃边走活跃半径个字符)增加一个叶子,插入x,也就代表了bx。

  我们获得了这个树:





  但是还没完,因为这已经是我们在此阶段建立的第二个内部节点了,根据规则二,故而我们需要建立”后缀链表”。此时三元组变成(root,’x’,0), remainder减少1,等于1。





  接下来就要插入x了。此时活跃半径已经是0了,于是活跃边一定是空的了,我们查看根节点所有边中首字母有没有是x的,结果没有,于是插入x。从当前生长点(此时因为活跃半径是0,活跃点就是生长点)增加叶子,插入x。得到下面这颗树。根据规则一,此时的三元组变成了(root, ‘\0’, 0),remainder减少1,等于0。此阶段顺利结束了。





第七阶段,插入字符a;#=7;三元组为(root, ‘\0’, 0),remainder增加1,等于1。需要增加的后缀只有一个就是a;

还是一样,鉴于活跃半径为0,我们从看看根节点有那个边的首字母为a,结果发现了。于是我们调整三元组为(root,’a’,1)。结束

第八阶段,插入字符b;#=8;三元组为(root, ‘a’, 1),remainder增加1,等于2。需要增加的后缀有两个ab,b;

我们先从当前活跃节点(根节点)出发,沿着活跃边的第1(活跃半径)个字符是不是b,结果是!于是我们调整三元组为(root, ‘a’, 2)。

  注意了,我们已经来到了一个内部节点而不再跟以前一样停留在边上,于是我们再次调整三元组(类似于初始化)为(node1,’\0’,0)。这里,node1代表了上图中从根节点出发经过边ab,到达的那个点。

第九阶段,插入字符c;#=9;三元组为(node1,’\0’,0),remainder增加1,等于3。需要增加的后缀有三个abc,bc,c;

鉴于活跃半径为0,我们直接看看当前活跃点(也就是node1)的边有没有以c为首字母的。结果有,于是更新三元组(node1,’c’,1)。注意一下虽然此时应该最先插入后缀abc,但是由于活跃点已经变化了,我们以当前活跃点为基准,故而活跃边以’c’标志。

第十阶段,插入字符s;;#=10;三元组为(node1,’c’,1),remainder增加1,等于4。需要增加的后缀有四个abcd,bcd,cd,d;

我们从当前活跃点node1出发沿着活跃边c,第1个字符是否是d,发现不是!于是在当前生长点(活跃点沿着活跃边前进活跃半径个字符)增加一个叶子,得到下图的树。





  由于当前活跃点不是根节点,所以按照规则三,我们沿着后缀链表前进,更新三元组为(node2,’c’,1),remainder减少1,等于3。node2为下图中的红色点。





  从node2出发沿着活跃边c出发,第1个字符是不是d,发现不是,于是在当前生长点增加叶子。注意此时要按照规则二建立新的后缀链表了。





  按照规则三,沿着后缀链表,由于已经没有下一个节点了,于是跳回到根节点,更新三元组(root,’c’,1)。remainder减少1,等于2.

从根节点出发沿着活跃边c,第1个字符是不是d?发现不是,于是在当前生长点增加叶子,注意仍按按照规则二建立后缀链表





  之后按照规则一更新三元组,活跃点不变,当前带插入的后缀是d,故而修改活跃边为d,活跃半径减少1变成0。remainder减少1,等于1。

  现在插入后缀d,鉴于活跃半径为0,看看根节点有没有边是以d为首字母的,发现没有!于是增加叶子。





  至此,全都结束了!后缀树建立完毕

  看完上面的建树过程,有点晕吧,哈哈,其实我也是。现在来说说上述算法跟上一篇文章提到的方法之间的关联吧。

  上一篇文章中我们提到的方法是每次沿着优化后的尾部链表更新,增加叶节点。其实我们这么来看,整个尾部链表是由三部分组成的,第一部分是叶子节点;第二部分是“除根节点外会增加叶子的节点”;第三部分是会导致 新后缀出现在原先后缀树中 的节点,这部分节点对树结构没有影响;所谓优化后的尾部链表是指去除第一部分和第三部分。当然喽第二部分第三部分都可能是空的。这样一来其实优化后的尾部链表由两部分组成:“尾部链表”的一部分(在本算法中就是后缀链表)。但是理论上这么说就可以了,实际上怎么来实现呢?

  我们看一下,因为”一朝为叶,终身为叶”,而每次遍历后缀链表的时候都会增加一个叶子,而最终的后缀树也只有|T|个叶子,所以我们一共需要遍历后缀链表|T|次。显然了,后缀链表是很“少”的。故而我们再去保留尾部链表,然后去定位第一个非叶子节点就很划不来了。于是,尾部链表作为一个理解上面的工具,在实践中(上述算法)就不维护啦,我们直接采用后缀链表。

  于是乎,我们回过头来看看上述算法。

  remainder是计数器,表明到当前阶段还需插入几个后缀,每个阶段增加1,是代表了后缀--从当前将要被插入的字符开始到原始串尾的这个后缀。当也叶子被插入的时候,rremainder减少1。

  首先初始化的时候活跃点是根节点,活跃半径是0。于是我们开始准备插入一个字符,如果字符没在根节点的任何一条边(默认为链接各个儿子的边)中出现,那么表明什么意思?表明当前生长点(根节点,所谓生长点就是准备长叶子的点),就是我们“尾部列表中”第二部分的节点,而remainder为1,代表当前只需插入一个叶子,于是插入即可,remainder减少1;如果字符在根节点的某一条边中出现,那么这是什么意思?这表明当前生长点(根节点),是我们第三部分的点,于是我们调整一下当前生长点的位置,这是为什么呢?因为,假设当前要插入的字符是a,下一次要插入b,那么我们这一阶段没插入叶子,下一阶段多了一个“欠债“,就要还要插入ab。那么我们将生长点挪到树中字符a的后面,就意味着我们下一阶段只需要在生长点后面插入字符b就可以了,一步到位,方便快捷。鉴于本阶段没有插叶子,所以remainder不变。调整三元组也是为了”挪动生长点“。

  下一次,我们要插入字符b的时候,我们在当前生长点后面找一找有没有b(其实就是为了验证当前生长点是不是后缀链表中的第三部分的点),如果没有,意味着是第二部分的点(因为肯定不是叶节点,又不是第三部分的点),那么就插入叶节点吧,remainder减少1,调整活跃边为下一个需要插入后缀的首字母,也就是b,活跃半径减少1。这也是为了调整生长点(快速找到下一个插入叶子的点)。于是看看根节点哪个边首字母为b,如果没有就插入新节点,如果有就调整生长点(这一块跟前面类似,不再多说)。我们说说另一种情况,就是我们在生长点(root,’a’,1)之后还找到了b,这说明当前生长点是第三部分的点,那么怎么办呢,继续调整生长点为(root,’a’,2)。

  假设上一次之后,调整生长点为(root,’a’,2),这一次再插入字符c,remainder增加1,等于3。如果当前生长点后没有c,意味着当前生长点是第二部分的点,于是插入叶子c就好了,代表着”欠债“后缀abc。remaidner减少1,调整生长点为(root,’b’,1),为什么如此确定根节点有某条边是以b为首字母呢?原因是这样的,你看啊,我们活跃半径是2,必然是因为我们刚刚插入的那个后缀abc的前两个字符出现在后缀树中,既然ab出现在了后缀树中,那么依据前文中的原理,b也一定出现在后缀树中。大家发现没有,这个调整生长点的过程是不是太迅速啦。

  事实上来说我们就凭借上述的这个过程似乎已经建立后缀树了,但是我们仔细看看,如果定位生长点的时候的这个(root,’a’,2)中的活跃半径不是2,而是一个很大的数字,比如100,那么就算我们知道下一个生长点是(root,’b’,99),真的能定位到准确的生长点么??不一定吧,根节点沿着一条特定的边走99步,可不一定只能到一个点啊,哈哈!

  这就是问题所在,所以我们调整生长点为(root,’a’,2)的时候,如果从当前活跃点开始,沿着活跃边走了活跃半径个距离,我们停在活跃边上面一个隐式节点那就没事,要是停到了内部节点node1,那我们就要更新活跃点为这个内部节点啦!然后调整三元组为(node1,’\0’,0)了。这里假设我们将(root,’a’,2)更新为(node1,’\0’,0)。我们再来看一下,假设下一个字符是c,且node1后面有一个边是以c为首字母的,于是更新三元组为(node1,’c’,1);假设我们之后要插入字符x,而从node1开始没有那个边是以x为首字母的,这时候就要插入叶子x了,代表了后缀abcx,我们下一个就要插入后缀bx了,但是怎么定位到下一个生长点?跟前面一样更新生长点为(node1,’b’,0)?

  肯定不行啊,因为下一个生长点根本就不在node1的子树里面。为什么?因为node1的子树里面任何一个点代表的字符串都是以ab(也就是根节点到node1那个边),而我们插入的是bcx。那么下一个生长点是什么呢?我们猜测一下啊,应该是(node2,’c’,1)其中根节点到node2的路径上面的字符是b!这样就可以啦。首先我们来明确一下存不存在这样一个结果node2的生长点?是存在的,因为既然后缀树中有ab这个内部节点(注意我的用词,ab已经成为i类不节点了,而不是刚刚建立的),那么必然有b这个内部节点(因为假设node1后面的两个儿子分别为x,y,那么后缀树中存在abx,和aby那么也必然存在bx,by所以也必然存在node2);同时后缀树中有字符串abc,那么必然也有串bc,于是证明是有的!下一个问题是,我们怎么快速将生长点从(node1,’c’,1)定位到(node2,’c’,1),所以我们想要是有一条指针从node1指向node2就好了,所以我们应该之前建立这个后缀链表这样下次就方便了。

  什么时候建立呢?我们来看啊,当初我们建立ab这个内部节点的时候,内部节点或者是已经存在了或者下一步就存在(注意跟上面的对比啊,这次是说我们当初建立ab内部节点的时候,所以这个内部节点是刚建立的,故而内部节点b暂时还不一定存在)为什么说下一步就存在呢?(因为: 如果 aS 停在一个内部节点上面,也就是 aS 后有分支,那么当前的 T[0 .. i-1] 肯定有子串 aS+c 以及aS+d
( c 和 d 是不同的两个字符) 这两个不同的子串,于是肯定也有 S+c 以及 S+d 两个子串了。至于“下次扩展时产生”的这种情况,则发生在已经有 aS+c 、 S+c ,刚加入 aS+d (于是产生了新的内部节点),正要加入 S+d 的时候。)下面我们来看啊,既然"下一步"内部节点b一定存在了,那就在"下一步"的时候建立指针从内部节点ab指向内部节点b。

  当我们走到后缀链表的尾部时候,意味着我们已经处理完毕remainder的前两个(就是从根节点到node1的边长,这两个后缀意味着abcx,bcx)”欠债”。于是我们回到根节点,当然了,活跃边和活跃半径不变。那么为什么此时从根节点出发沿着活跃边走活跃半径个字符就能唯一锁定生长点呢?

  答案是这样的,因为啊,我们之前将活跃点更新为node1之后可能还是会更新活跃点,我们将最后更新的活跃点命名为nodeN,此时的三元组为(nodeN,active_edge,active_length),我们知道这个三元组确定的生长点位于nodeN的某条边上的一个隐式节点。我们知道我们路过nodeN这个活跃点的时候,nodeN那边已经存在后缀链接了,所以当我们沿着后缀链接插入叶子,在最终回到根节点时候三元组变为nodeN(root,active_edge,active_length)。一步定位!而且在沿着后缀链表定位生长点的时候也是一步定位!

下面说明一下为什么上述算法是线性的。首先啊,我们来看上面一共涉及到了几个操作:插入叶节点(每次插入消耗常数时间,总叶节点|T|个,故而总时间为O(|T|),建立后缀链表(每次建立一个指针只需要沿着前面建好的后缀链表走一下,然后一步定位,所以每次建立一个指针只需要常数时间;而每次我们沿着后缀链表走都会插入新叶子,故而后缀链表一共也只有|T|这么长,故而一共需要插入O(|T|)次),更新三元组(因为涉及到活跃点的移动,故而我们这么看,没有更新活跃点的时候是常数时间,而更新活跃点的情况有点复杂,我们这么理解:每次更新活跃点都会导致活跃半径相应减小,而活跃半径始在整个建树的过程中最多增加|T|,故而整个更新活跃点所需时间为O(|T|)),其他诸如remainder的调整等等,每次花费常数时间,总和为O(|T|)。综上所述,线性算法降临了!!!!

1 #include <iostream>
2 #include <stdio.h>
3 #include <string>
4
5 const int oo = 1<<25;
6 const int ALPHABET_SIZE = 256;
7 const int MAXN = 5000;
8
9 using namespace std;
10
11 int root, last_added, pos, needSL, remainder,
12     active_node, active_e, active_len;
13
14 struct node {
15 /*
16    There is no need to create an "Edge" struct.
17    Information about the edge is stored right in the node.
18    [start; end) interval specifies the edge,
19    by which the node is connected to its parent node.
20 */
21
22     int start, end, slink;
23     int next[ALPHABET_SIZE];
24
25     int edge_length() {
26         return min(end, pos + 1) - start;
27     }
28 };
29
30 node tree[2*MAXN];
31 char text[MAXN];
32
33 int new_node(int start, int end = oo) {
34     node nd;
35     nd.start = start;
36     nd.end = end;
37     nd.slink = 0;
38     for (int i = 0; i < ALPHABET_SIZE; i++)
39         nd.next[i] = 0;
40     tree[++last_added] = nd;
41     return last_added;
42 }
43
44 char active_edge() {
45     return text[active_e];
46 }
47
48 void add_SL(int node) {
49     if (needSL > 0) tree[needSL].slink = node;
50     needSL = node;
51 }
52
53 bool walk_down(int node) {
54     if (active_len >= tree[node].edge_length()) {
55         active_e += tree[node].edge_length();
56         active_len -= tree[node].edge_length();
57         active_node = node;
58         return true;
59     }
60     return false;
61 }
62
63 void st_init() {
64     needSL = 0, last_added = 0, pos = -1,
65     remainder = 0, active_node = 0, active_e = 0, active_len = 0;
66     root = active_node = new_node(-1, -1);
67 }
68
69 void st_extend(char c) {
70     text[++pos] = c;
71     needSL = 0;
72     remainder++;
73     while(remainder > 0) {
74         if (active_len == 0) active_e = pos;
75         if (tree[active_node].next[active_edge()] == 0) {
76             int leaf = new_node(pos);
77             tree[active_node].next[active_edge()] = leaf;
78             add_SL(active_node); //rule 2
79         } else {
80             int nxt = tree[active_node].next[active_edge()];
81             if (walk_down(nxt)) continue; //observation 2
82             if (text[tree[nxt].start + active_len] == c) { //observation 1
83                 active_len++;
84                 add_SL(active_node); //observation 3
85                 break;
86             }
87             int split = new_node(tree[nxt].start, tree[nxt].start + active_len);
88             tree[active_node].next[active_edge()] = split;
89             int leaf = new_node(pos);
90             tree[split].next[c] = leaf;
91             tree[nxt].start += active_len;
92             tree[split].next[text[tree[nxt].start]] = nxt;
93             add_SL(split); //rule 2
94         }
95         remainder--;
96         if (active_node == root && active_len > 0) { //rule 1
97             active_len--;
98             active_e = pos - remainder + 1;
99         } else
100             active_node = tree[active_node].slink > 0 ? tree[active_node].slink : root; //rule 3
101     }
102 }
103
104 int main() {
105     //
106     return 0;
107 }


1 #include <iostream>
2 #include <stdio.h>
3 #include <string>
4
5 const int oo = 1<<25;
6 const int ALPHABET_SIZE = 256;
7 const int MAXN = 5000;
8
9 using namespace std;
10
11 int root, last_added, pos, needSL, remainder,
12     active_node, active_e, active_len;
13
14 struct node {
15 /*
16    There is no need to create an "Edge" struct.
17    Information about the edge is stored right in the node.
18    [start; end) interval specifies the edge,
19    by which the node is connected to its parent node.
20 */
21
22     int start, end, slink;
23     int next[ALPHABET_SIZE];
24
25     int edge_length() {
26         return min(end, pos + 1) - start;
27     }
28 };
29
30 node tree[2*MAXN];
31 char text[MAXN];
32
33 int new_node(int start, int end = oo) {
34     node nd;
35     nd.start = start;
36     nd.end = end;
37     nd.slink = 0;
38     for (int i = 0; i < ALPHABET_SIZE; i++)
39         nd.next[i] = 0;
40     tree[++last_added] = nd;
41     return last_added;
42 }
43
44 char active_edge() {
45     return text[active_e];
46 }
47
48 void add_SL(int node) {
49     if (needSL > 0) tree[needSL].slink = node;
50     needSL = node;
51 }
52
53 bool walk_down(int node) {
54     if (active_len >= tree[node].edge_length()) {
55         active_e += tree[node].edge_length();
56         active_len -= tree[node].edge_length();
57         active_node = node;
58         return true;
59     }
60     return false;
61 }
62
63 void st_init() {
64     needSL = 0, last_added = 0, pos = -1,
65     remainder = 0, active_node = 0, active_e = 0, active_len = 0;
66     root = active_node = new_node(-1, -1);
67 }
68
69 void st_extend(char c) {
70     text[++pos] = c;
71     needSL = 0;
72     remainder++;
73     while(remainder > 0) {
74         if (active_len == 0) active_e = pos;
75         if (tree[active_node].next[active_edge()] == 0) {
76             int leaf = new_node(pos);
77             tree[active_node].next[active_edge()] = leaf;
78             add_SL(active_node); //rule 2
79         } else {
80             int nxt = tree[active_node].next[active_edge()];
81             if (walk_down(nxt)) continue; //observation 2
82             if (text[tree[nxt].start + active_len] == c) { //observation 1
83                 active_len++;
84                 add_SL(active_node); //observation 3
85                 break;
86             }
87             int split = new_node(tree[nxt].start, tree[nxt].start + active_len);
88             tree[active_node].next[active_edge()] = split;
89             int leaf = new_node(pos);
90             tree[split].next[c] = leaf;
91             tree[nxt].start += active_len;
92             tree[split].next[text[tree[nxt].start]] = nxt;
93             add_SL(split); //rule 2
94         }
95         remainder--;
96         if (active_node == root && active_len > 0) { //rule 1
97             active_len--;
98             active_e = pos - remainder + 1;
99         } else
100             active_node = tree[active_node].slink > 0 ? tree[active_node].slink : root; //rule 3
101     }
102 }
103
104 int main() {
105     //
106     return 0;
107 }


文章说了后缀树构建的最基本的算法,属于In-memory,但是在面对大量数据的时候memory
locality较差,效率很低。还有好多其他构建的算法,如:TDD,WaveFront等。在2012年Essam等人提出的ERA,可以并行化,对于构建很长的字符串的后缀树比较有效,速度也较快。

来自于 http://www.oschina.net/translate/ukkonens-suffix-tree-algorithm-in-plain-english “maicao”的评论
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: