原文:http://www.geeksforgeeks.org/pattern-searching-set-8-suffix-tree-introduction/
给定一个文本(字符串)txt[1..n-1] 和一个模式(待搜索字符串)pat[0..m-1],写一个函数search(char pat[], char txt[]),功能是打印出pat[]在txt[]中所有出现的位置。假设 n>m
解决方法:预处理模式还是预处理文本?
一下这些算法是比较经典的解决这个问题的算法:
KMP Algorithm
Rabin Karp Algorithm
Finite Automata based Algorithm
Boyer Moore Algorithm
上述的算法都是预处理模式用以加速搜索的过程。通过这种方法得到的最优时间复杂度为O(n),这里n是文本的长度。在这篇文章里,我们介绍一个预处理文本的方法。后缀树就是根据文本建立的。预处理文本后(建立文本的后缀树),我们可以在O(m)的时间内搜索任意的模式,这里m是模式的长度。
设想你存储了莎士比亚全集并且进行了预处理。你就可以在全集中搜索任意的字符串,而且用时仅与字符串的长度相关。这是一个很大的进步,因为一般情况下模式的长度远低于文本的长度。但是如果文本经常变化,预处理文本的代价就会加大。这种方法对固定的或者变动很少的文本很适合。
后缀树是一种压缩的由给定文本的所有后缀构成的trie树(字典树)。我们介绍过标准trie树。通过下面的字符串数组,让我们了解一下压缩trie树。
{bear, bell, bid, bull, buy, sell, stock, stop}
下图是由上述字符串数组构建的标准trie树:
下图是压缩的trie树。压缩trie树是由标准trie树将一些单个节点组合成链而转换来的。压缩trie树的节点可以通过存储节点的索引范围来表示。
对于一个给定的文本,如何构建后缀树(Suffix Tree)?
通过上述讨论,我们知道了后缀树是由文本的所有后缀组成的压缩tire树。下面是一个构造后缀树的非常抽象的步骤。
1)对给定的文本生成它的所有后缀
2)将所有后缀当做单个字符串来构建压缩trie树。
我们用文本“banana\0”来做个例子,这里“\0”是字符串的终止字符。下面的字符串是“banana\0”的所有后缀:
banana\0
anana\0
nana\0
ana\0
na\0
a\0
\0
假如我们将上述的所有后缀当做单个字符串来构建trie树的话,我们得到了如下图所示的trie树:
如果我们将单个的节点组合成串,我们得到了如下的压缩trie树,这就是文本“banana\0”的后缀树。
注意上述步骤只是人工创建后缀树的过程。我们会分几章来讨论真正的算法和实现过程。
在后缀树中如何搜索模式?
在上面的文字中我们讨论了如何构建后缀树,即模式搜索过程的预处理步骤。下面是在后缀树中搜索模式的抽象步骤。
1)从模式的第一个字符和后缀树的跟节点开始,对每一个字符做如下操作。
......a)对模式中当前的字符,如果在后缀树中当前的节点有一条边,沿着这条边向前。
......b)如果没有边,打印“模式在该文本中不存在”并返回。
2)如果模式中所有的字符都被处理了,即对给定的模式,从后缀树的根存在一条路径,这条路径上的字母组成的字符串与模式相同,那么打印“模式找到”。
我们用模式“nan”作为例子,看看搜索的过程。下图给出了搜索“nan”或者“nana”的路径。
这个方法为什么会有效?
每个文本中可以搜索到的模式(或者说文本的每个子字符串)一定是所有可能的后缀字符串的一个前缀。这个表述貌似有点复杂,但其实内容很简单,你只需要用一些例子验证一下就行。
后缀树的应用
后缀树可以用来解决很多问题。下面是一些比较著名的问题,在这些问题中后缀树都给出了最优时间复杂度的方案。
1)模式搜索
2)查找最长重复子字符串
3)查找最长公共子字符串
4)查找字符串的最长回文
后缀树还有很多其他的应用,详细请看维基百科:http://en.wikipedia.org/wiki/Suffix_tree#Functionality