from:http://www.acmerblog.com/suffix-tree-6152.html
后缀字典树
想象一下,你刚成为一个DNA序列化项目中的程序员。研究人员正在对病毒的遗传物质进行切片和切块,以产生核苷酸序列的片段。他们把这些遗传物质序列片段送人你的服务器进行匹配,希望能够在基因组数据库中定位到这些序列。一个特定的病毒的基因组可能有数十万的核苷酸,并且在你的数据库中存储了成千上万的病毒的信息。希望你能够实现一个C/S结构的项目,来实时的为研究人员提供服务。怎么做才比较好呢?
因为数据库是不变的,所以对它进行预处理以使得搜索变得简单。一种预处理的方式是建立一个字典树(Trie树) ,该字典树中存放的是给定字符串的所有后缀,用这种方式生成的字典树,称为后缀字典树(suffix trie),后缀字典树只差一步就是我最终要引入的数据结构——后缀树。字典树是一种N叉树,其中N是字母表的大小。
对于字符串:“banana\0″ 的所有后缀为:
1 | banana\0 |
2 | anana\0 |
3 | nana\0 |
4 | ana\0 |
5 | na\0 |
6 | a\0 |
7 | \0 |
其标准的后缀字典树结构是这样的:
很显然,有了这样的一个树,我只需要4次字符比较就可以确定pattern:“nana”是否出现。当然仅仅有一个小问题会拖后腿儿,那就是创建字典树所耗费的时间。
但是,构造后缀字典树需要O(N2)时间和空间复杂度。这种平方级的性能使它不能在最需要用到它的地方使用,即不能在大数据量场合使用。
后缀树
Edward McCreight 在1976年提出了一个合理的解决方法摆脱了后缀字典树在应用上的困境,他发表的论文中提出了后缀树(suffix tree)。
一个给定的文本text的后缀树就是一个压缩的后缀字典树。压缩至的是路径压缩,去除了只有一个子边的节点。例如对上上图中最左边的 banana\0 ,即为一个没有分叉的单边,可以进行压缩:
上面这个图是靠是手动生成的,实际上这里还是有一些比较复杂的算法,后续再做讨论。节点数量的减少,使构造后缀树的时间和空间复杂度从O(N2)减少到了O(N)。在最坏的情况下,一个后缀树最多包含2N个节点,其中N是输入文本的长度。所以对输入文本的一次性投资,可以使我们的每次搜索都受益。
应用
(1). 查找字符串o是否在字符串S中。
方案:用S构造后缀树,按在trie中搜索字串的方法搜索o即可。
原理:若o在S中,则o必然是S的某个后缀的前缀。
例如S: leconte,查找o: con是否在S中,则o(con)必然是S(leconte)的后缀之一conte的前缀.有了这个前提,采用trie搜索的方法就不难理解了。
(2). 指定字符串T在字符串S中的重复次数。
方案:用S+’$’构造后缀树,搜索T节点下的叶节点数目即为重复次数
原理:如果T在S中重复了两次,则S应有两个后缀以T为前缀,重复次数就自然统计出来了。
(3). 字符串S中的最长重复子串
方案:原理同2,具体做法就是找到最深的非叶节点。
这个深是指从root所经历过的字符个数,最深非叶节点所经历的字符串起来就是最长重复子串。
为什么要非叶节点呢?因为既然是要重复,当然叶节点个数要>=2。
(4). 两个字符串S1,S2的最长公共部分
方案:将S1#S2$作为字符串压入后缀树,找到最深的非叶节点,且该节点的叶节点既有#也有$(无#)。
后缀树和后缀数组都是处理字符串的有效工具,前者较为常见,但后者更容易编程实现,空间耗用更少;后缀数组可用于解决最长公共子串问题,多模式匹配问题,最长回文串问题,全文搜索等问题;
后缀数组见《挑战程序设计竞赛》。