因此,我们拥有一棵起始树,这棵树看起来如下:
而其意义如下:
我们看到了两点:
当然,后缀树表示的如此良好只是因为我们的字符串没有包含任何重复。现在我们看一个更真实的字符串:
abcabxabcd这个字符串像前面例子里一样是abc开始的,接着重复ab ,紧跟着x,再接着重复abc,紧跟着d。
步骤1到3:经过了前三个步骤后,我们拥有了前面例子的那棵树:
步骤4:我们移动#到位置4。这隐含地更改所有已经存在的边为如下:
注意:当发现我们需要插入的最终后缀已经存在在这棵树里的时候,这棵树本身根本就没有改变(我们只是修改了活动节点和剩余后缀数)。那么这棵树就不再是能准确的表示至当前位置的后缀树了,不过它包含了所有的后缀(因为最终的后缀a隐含地包含了)。因此,除了修改变量外(所有这些变量都是定长的,因此空间复杂度是 O(1)),在这一步里没有做其他工作。
步骤5:我们修改当前的位置#为5。这将自动地如下更新这棵树:
为了清晰地说明:我们需要在当前的步骤里插入ab和b,不过由于ab已经找到,所以我们修改了活动点,而且甚至不试图插入b。为什么?因为如果ab处于这棵树里,那么它的每个后缀(包括b)也一定在这棵树里。也许仅仅是隐含性的,不过它一定在这棵树里,因为这是我们迄今为止建立这棵树所采用的方法。
我们增加#而前进到步骤6。这棵树自动修改为:
由于剩余后缀数是3 ,我们不得不增加abx,bx和x。活动点告诉我们ab结束在哪儿,因此我们仅仅需要跳过这儿,然后插入x。x确实还不在这棵树里,因此我们分割abcabx边,插入一个内部节点:
这时我们处理了abx,并且把剩余后缀数减为2。现在我们需要插入下一个剩余后缀bx。但是在我们做这些之前,我们需要修改活动节点。分割并插入一条边遵循的规则称作规则1,如下,而且它适用于活动节点是根节点的情况(针对下面后续的其他情况,我们将要了解规则3)。规则1如下:
向根节点插入遵循:
因此,新的活动节点三元组(root,'b',1)表明要做的下一个插入在bcabx边,第一个字符之后,即b之后。我们可以确定插入点的时间复杂度为 O(1),并且检查x是否已经出现在树里。如果它出现在这条边里,我们将结束当前的步骤,保持一切为原样。然而如果x没有出现在这条边里,那么我们分割这条边而插入它:
我们设置#=7而前进到步骤7,这将像往常一样自动添加下一个字符a到所有的叶子边上。然后我们试图插入新的最终字符到活动节点(根节点),然后发现它已经存在在这棵树里了。因此我们结束当前的步骤,不插入任何边,并且修改活动点位(root,'a',1)。
设置#=8进入步骤8,我们添加b,像以前所看到的,这仅仅意味着我们修改活动点位(root,'a',2) ,而且不需要做其他事情就增加剩余后缀数。因为b已经出现在这棵树里。然而我们(在 O(1)时间复杂度里)注意到活动节点现在是一条边的结尾。我们通过重置活动节点位(node1,'\0x',0)来体现这个。这儿,我们用node1来指ab边结束的哪个内部节点。
接着设置#=9进入步骤9,我们需要插入'c',这将有助于我们理解的最后一条技巧:
评论删除后,数据将无法恢复
评论(9)
应用的话很广泛的,比如:在生物信息学中的DNA索引,利用后缀树来表示DNA碱基的信息。
文章说了后缀树构建的最基本的算法,属于In-memory,但是在面对大量数据的时候memory locality较差,效率很低。还有好多其他构建的算法,如:TDD,WaveFront等。在2012年Essam等人提出的ERA,可以并行化,对于构建很长的字符串的后缀树比较有效,速度也较快。
引用来自“kuntzuo”的评论
原文在何处?