Ukkonen的后缀树算法在普通英语中
这一点我感觉有点厚。 我花了好几天的时间试图完全包裹后缀树构造,但因为我没有数学背景,所以很多解释在我开始过度使用数学符号学的时候就避而远之。 最接近我找到的一个很好的解释是使用后缀树的快速字符串搜索,但是他掩盖了各个点,并且该算法的某些方面仍然不清楚。
我相信,Stack Overflow对这个算法的逐步解释对于除了我之外的许多其他人是无价的。
作为参考,这里是Ukkonen关于算法的论文:http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf
到目前为止,我的基本理解是:
基本算法似乎是O(n2),正如在大多数解释中指出的那样,因为我们需要遍历所有前缀,那么我们需要遍历每个前缀的每个后缀。 由于使用了后缀指针技术,Ukkonen的算法显然是独一无二的,但我认为这是我无法理解的。
我也无法理解:
这里是完整的C#源代码。 它不仅工作正常,而且支持自动封装,并呈现出更好看的输出文本图形。 源代码和示例输出位于:
https://gist.github.com/2373868
更新2017-11-04
多年后,我发现了后缀树的新用法,并在JavaScript中实现了该算法。 Gist在下面。 它应该没有缺陷。 将它转储到js文件中, npm install chalk
从同一位置npm install chalk
,然后使用node.js运行以查看一些丰富多彩的输出。 在同一个Gist中有一个精简版,没有任何调试代码。
https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6
以下是试图通过首先显示字符串简单(即不包含任何重复字符),然后将其扩展为完整算法时所做的操作来描述Ukkonen算法的尝试。
首先,几个初步的陈述。
我们正在建设的东西,基本上就像是搜索引擎。 所以有一个根节点,从它出来的边缘导致新的节点,以及更远的边缘,等等
但是 :与搜索特里不同,边缘标签不是单个字符。 相反,每条边都使用一对整数[from,to]
来标记。 这些是指向文本的指针。 从这个意义上讲,每个边缘都带有一个任意长度的字符串标签,但只需要O(1)个空间(两个指针)。
基本原则
我想先演示如何创建一个特别简单的字符串的后缀树,一个没有重复字符的字符串:
abc
该算法从左到右逐步工作 。 字符串的每个字符都有一个步骤 。 每个步骤可能涉及多个单独的操作,但是我们会看到(最终观察结果)操作总数为O(n)。
所以,我们从左边开始,首先插入单个字符a
,方法是从根节点(在左侧)创建一个边到叶,并将其标记为[0,#]
,这意味着边表示子字符串从位置0开始到当前结束。 我使用符号#
表示当前的结束,它位于位置1(紧接在a
之后)。
所以我们有一个初始树,看起来像这样:
这意味着什么:
现在我们进入第2位(在b
之后)。 我们在每一步中的目标是将所有后缀插入到当前位置 。 我们这样做
a
-edge扩展到ab
b
插入一个新边 在我们的表示中,这看起来像
它的意思是:
我们观察两件事:
ab
的边缘表示与初始树中的边缘表示相同 : [0,#]
。 它的含义已经自动更改,因为我们将当前位置#
从1更新为2。 接下来,我们再次增加位置并通过将c
添加到每个现有边并为新后缀c
插入一个新边来更新树。
在我们的表示中,这看起来像
它的意思是:
我们观察到:
#
自动更新,并且可以在O(1)时间内插入最后一个字符的一个新边。 因此,对于长度为n的字符串,只需要O(n)时间。 第一次延伸:简单的重复
当然,这个效果很好,只是因为我们的字符串不包含任何重复。 我们现在看一个更真实的字符串:
abcabxabcd
它以前面的例子中的abc
开始,然后ab
重复,然后是x
,然后abc
重复,然后是d
。
步骤1到3:在前3个步骤之后,我们获得了上例中的树:
第4步:我们将#
移动到位置4.这隐含地将所有现有边缘更新为:
我们需要在根上插入当前步骤的最后一个后缀a
。
在我们这样做之前,我们引入了两个变量 (除#
),这当然是所有时间都存在的,但到目前为止我们还没有使用它们:
(active_node,active_edge,active_length)
remainder
是一个整数,表示我们需要插入多少个新后缀 这两者的确切含义很快就会变得清晰起来,但现在让我们来说一下:
abc
示例中,活动点总是(root,'