后缀树

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀树相关的知识,希望对你有一定的参考价值。

后缀树是一种数据结构,可以对任意字符串建立后缀树。称用于建立后缀树T的字符串S为后缀树的数据源。

后缀树有以下优点:

  利用后缀树可以以O(M)的时间复杂度在数据源中查找某一字符串K是否是其子串,其中M为K的长度。

  可以在建立后缀树的同时确定数据源中的最长的重复子串的起始位置和长度。

  可以利用后缀树以O(M)的时间复杂度查找另外一个字符串K与数据源的最长重复子串,M为K的长度。

接下来说明建立后缀树的时间复杂度和空间复杂度:

  建立后缀树的时间复杂度为O(N),N为数据源S的长度。而后缀树的空间复杂度是O(DN),D为使用的字符集的大小。因此后缀树实际上是一种空间和时间的抉择中偏向后者的产物。


如何建立后缀树

  首先说明后缀树中的结点,结点有如下属性:

  repr,表示结点代表的字符串部分。

  begin,end分别表示repr在源字符串中的起始和结束下标,

  size表示end-begin,

  children,表示孩子节点的数组,且children[i]表示以字符i开头的结点。

  link,链接这个结点到另外一个特殊的结点

  father,父亲结点

   

下面给出一些定义:

  定义1:由结点N[0],...,N[k]代表的结点序列称为结点N[k]的路径,其中N[0]是根结点,且N[i]是N[i+1]的父节点,i=0,...,k-1。而N[0].repr+...+N[k-1].repr称为结点N[k]的前缀,记作N[k].prefix。称N[k]的深度为k,记作N[k].depth。

  定义2:若结点A.prefix移除了第一个字符后与B.prefix一致,则称B是A的后置结点。显然在后缀树中一个结点至多只有一个后缀结点,而link就用于指向这唯一的后置结点。

 

下面给出一些初始化数据:

  root为一个空结点,root.link=root,root.father=root,其begin和end均为0。active是一个移动指针,初始时指向root,在整个流程中指向。ul和ur用于分别表示源字符串的未处理部分,初始时为0。father用于表示link为空的某个结点。因此整个流程可以用:(active, ul, ur)来完整表示。S表示源字符串。

 

下面给出实际操作流程PRO:

  先令ur自增,并设置father为空。表示未处理部分扩大。之后进入流程JUDGE:

  如果ur==ul,则结束流程,否则进入下面判断流程。

  记p=min(ul + active.size + active.prefix.length, ur)。若p <= ur,则进入情况1,否则进入情况2。

  情况1:

    判断active.children[S[p]]是否为空,为空进入情况3,否则进入情况4。

  情况2:

    如果active.repr[p - ul - active.prefix.length] == S[p],则进入情况5,否则进入情况6。

  情况3:

    建立一个空结点empty,并令empty.begin=p,empty.end=S.length,empty.link = root, 并设置active.children[S[p]]为empty。之后设置father=active,并将active设置为则设置为active.father.link,并令ul自增,之后重新执行流程JUDGE。

  情况4:

    将active设置为active.children[S[p]]。若father不为空,且active.prefix.length + 1 == father.prefix.length,则设置father.link=active。之后重新执行JUDGE流程。

  情况5:

    结束流程。

  情况6:

    将active从不匹配处分裂为两个结点active和post,且post.children=active.children,active.children重新分配,active.end=post.begin-1,之后进入情况3。

  


算法正确性的说明:

  由于ul和ur分别表示未处理部分的开端和结束下标,而ul增加当且仅当情况3的发生,这时候后缀S[ul...S.length]已经通过新增empty结点插入到了后缀树中,而流程结束当且仅当ur达到S.length,此时S[ul...ur]已经在后缀树中出现过了,接下来只需要阐清S[ul+1...ur], S[ul+2...ur]等都已经在后缀树中出现过了,这里ur=S.length。由于S[ul...ur]出现在了后缀树中,这说明前面的一次后缀插入S[x...S.length]的发生,且S[ul...ur]是S[x...S.length]的前缀,若ur-ul+x<ul,那么显然S[ul+1...ur], S[ul+2...ur]等因为S[x+1...S.length],S[x+2...ur]的插入也已经在后缀树中出现过了。若ur-ul+x>=ul,那么问题就转移到了形如S[ul...S.length]在后缀树中出现,而前面也已经通过S[x...S.length]进行了这样的证明。因此上面的流程会确保所有S的后缀必然在后缀树中出现过。

 

 

算法复杂度的说明:

  先说明当一次PRO流程完成后,对于每一个结点(除了root),其后置结点都存在。假设在某次PRO被调用前上述命题成立,那么当我们进入情况3,重设了active,记之前的active为A。显然A的前置结点也是存在的,通过重设active为A.father.link,在后续的步骤必定会将active设置为A的后置结点(通过情况4)。这是由于我们在前若干次执行JUDGE时,都会带来active.prefix的逐步增大。因此必定有一个点会使得active.prefix=A.prefix[1...]。此时也会导致active被正确的设置。之后等到我们将active设置为最终的结点,此时active.prefix+active.repr包含了A.prefix[1...]+A.repr,之后利用结点分裂或插入空白结点带来A新建的后继结点的后置结点的创建。因此利用归纳法可以保证每一次PRO结束后,每个结点的后置结点都存在,且带子结点的后置结点的link都被正确赋值。

  这里继续说明对于每一个link非空的结点node,node.link.depth >= node.depth - 1。由于整个流程都不会发生结点的合并,只会发生结点的分裂,故结点的深度是非严格递增的。当通过情况3设置了father属性为A=active时,之后重新设置active为A.father.link,此时active的深度应该不小于A.father.depth - 1=A.depth - 2,而由于情况4的发生必定会导致active的深度增加,故此时active的深度最少也是A.depth-1,满足了前面的要求,通过数学归纳法可以轻松得到node.link.depth >= node.depth - 1这一命题的成立。

  每次ur的增加,都会带来PRO被调用,以及情况3,4,5,6中某一被触发。若情况3被触发会导致ul的自增,因此在整个算法的过程中情况3至多发生不超过S.length次。情况4会会导致active的深度增加1,而整个过程中只有情况3会减少active的深度,且减少量不超过1,因此情况4最多发生S.length+S.length=2*S.length次。情况5会导致整个流程的结束,因此发生次数不会超过PRO的发生次数,而PRO总共发生S.length次。而每次情况6发生都会直接导致情况3的发生,故情况6发生次数也不可能超过S.length次。由于每一种情况其时间复杂度都是常量级别的,故总的时间复杂度为O(S.length)。

  对于空间复杂度,由于只有情况3和6会创建新的结点,而每种情况最多发生S.length,故其最多创建了2*S.length个结点,空间复杂度为O(S.length)。


 给一份自己手写的代码,只用了少量测试数据进行测试,不能保证完全正确,但是确实是按照前面提到的思路和流程写的:

技术分享
  1 package cn.dalt.reuse.string;
  2 
  3 import java.util.Iterator;
  4 import java.util.LinkedList;
  5 import java.util.function.Supplier;
  6 
  7 /**
  8  * Created by dalt on 2017/11/5.
  9  */
 10 public class SuffixTree {
 11     private char[] source;
 12     private Node root;
 13 
 14     private SuffixTree() {
 15     }
 16 
 17     public boolean contain(char[] data) {
 18         return new CharVisitor(data).matchUtil() == data.length;
 19     }
 20 
 21     public static SuffixTree build(char[] source, char minChar, char maxChar) {
 22         return new SuffixTreeBuilder().build(source, new Supplier<CharMap>() {
 23             @Override
 24             public CharMap get() {
 25                 return new RangedCharMap(minChar, maxChar);
 26             }
 27         });
 28     }
 29 
 30     public static interface CharMap extends Iterable<CharMap.CharEntry> {
 31         Object get(char c);
 32 
 33         void put(char c, Object v);
 34 
 35         CharEntry getFirst();
 36 
 37         CharEntry getLast();
 38 
 39         public static interface CharEntry {
 40             char getKey();
 41 
 42             Object getValue();
 43         }
 44     }
 45 
 46     public static class CharEntryImpl implements CharMap.CharEntry {
 47         char c;
 48         Object v;
 49 
 50         public CharEntryImpl(char c, Object v) {
 51             this.c = c;
 52             this.v = v;
 53         }
 54 
 55         @Override
 56         public char getKey() {
 57             return c;
 58         }
 59 
 60         @Override
 61         public Object getValue() {
 62             return v;
 63         }
 64 
 65         @Override
 66         public String toString() {
 67             return c + ":" + v;
 68         }
 69     }
 70 
 71     public static class RangedCharMap implements CharMap {
 72         int offset;
 73         CharEntry[] values;
 74         LinkedList<CharEntry> list = new LinkedList<>();
 75 
 76         public RangedCharMap(int begin, int end) {
 77             offset = begin;
 78             values = new CharEntry[end - begin + 1];
 79         }
 80 
 81         @Override
 82         public Object get(char c) {
 83             CharEntry entry = values[c - offset];
 84             return entry == null ? null : entry.getValue();
 85         }
 86 
 87         @Override
 88         public void put(char c, Object v) {
 89             CharEntry entry = new CharEntryImpl(c, v);
 90             values[c - offset] = entry;
 91             list.add(entry);
 92         }
 93 
 94         @Override
 95         public CharEntry getFirst() {
 96             return list.getFirst();
 97         }
 98 
 99         @Override
100         public CharEntry getLast() {
101             return list.getLast();
102         }
103 
104         @Override
105         public Iterator<CharEntry> iterator() {
106             return list.iterator();
107         }
108 
109         @Override
110         public String toString() {
111             return list.toString();
112         }
113     }
114 
115     public static class SuffixTreeBuilder {
116         int ul;
117         int ur;
118         int p;
119         Node root;
120         char[] s;
121         Supplier<CharMap> supplier;
122         Node active;
123         Node father;
124 
125         SuffixTree build(char[] source, Supplier<CharMap> supplier) {
126             s = source;
127             this.supplier = supplier;
128 
129             ul = 0;
130             ur = -1;
131             root = new Node();
132             root.children = supplier.get();
133             root.begin = 0;
134             root.end = 0;
135             root.link = root;
136             root.parent = root;
137             root.prefixLen = 0;
138 //            root.source = s;
139 
140             active = root;
141 
142             for (int i = 0, bound = source.length; i < bound; i++) {
143                 pro();
144             }
145 
146             SuffixTree suffixTree = new SuffixTree();
147             suffixTree.source = source;
148             suffixTree.root = root;
149             return suffixTree;
150         }
151 
152         private void pro() {
153             ur++;
154             father = null;
155             judge();
156         }
157 
158         private void judge() {
159             if (ur < ul) {
160                 return;
161             }
162 
163             int activeUntil = ul + active.size() + active.prefixLen;
164             if (activeUntil <= ur) {
165                 p = activeUntil;
166                 case1();
167             } else {
168                 p = ur;
169                 case2();
170             }
171         }
172 
173         private void case1() {
174             if (active.children.get(s[p]) == null) {
175                 case3();
176             } else {
177                 case4();
178             }
179         }
180 
181         private void case2() {
182             if (s[active.begin + p - ul - active.prefixLen] == s[p]) {
183                 case5();
184             } else {
185                 case6();
186             }
187         }
188 
189         private void case3() {
190             Node empty = new Node();
191             empty.begin = p;
192             empty.end = s.length;
193             empty.link = root;
194             empty.children = supplier.get();
195 //            empty.source = s;
196             empty.parent = active;
197             empty.prefixLen = active.prefixLen + active.size();
198             active.children.put(s[p], empty);
199             father = active;
200             active = active.parent.link;
201             ul++;
202             judge();
203         }
204 
205         private void case4() {
206             active = (Node) active.children.get(s[p]);
207             if (father != null && active.prefixLen + 1 == father.prefixLen) {
208                 father.link = active;
209                 father = null;
210             }
211             judge();
212         }
213 
214         private void case5() {
215         }
216 
217         private void case6() {
218             Node post = new Node();
219             int splitPos = active.begin + (p - ul - active.prefixLen);
220             post.begin = splitPos;
221             post.end = active.end;
222             post.children = active.children;
223             post.link = root;
224 //            post.source = s;
225             post.parent = active;
226             active.end = splitPos;
227             active.children = supplier.get();
228             active.children.put(s[splitPos], post);
229             post.prefixLen = active.prefixLen + active.size();
230             case3();
231         }
232 
233     }
234 
235     private static class Node {
236         int begin;
237         int end;
238         CharMap children;
239         Node link;
240         Node parent;
241         int prefixLen;
242 
243 //        char[] source;
244 
245         public int size() {
246             return end - begin;
247         }
248 
249 //        @Override
250 //        public String toString() {
251 //            return String.valueOf(source, begin, end - begin);
252 //        }
253     }
254 
255     private class CharVisitor {
256         char[] text;
257         int textIndex;
258         Node node;
259         int nodeIndex;
260 
261         public CharVisitor(char[] text) {
262             node = root;
263             this.text = text;
264             textIndex = 0;
265             nodeIndex = node.begin;
266 
267             if (text.length != 0) {
268                 node = (Node) node.children.get(text[0]);
269                 if (node != null) {
270                     textIndex = 1;
271                     nodeIndex = node.begin + 1;
272                 }
273             }
274         }
275 
276         public boolean finished() {
277             return textIndex == text.length;
278         }
279 
280         public Node matchedLastNode() {
281             return node;
282         }
283 
284         public boolean matchNext() {
285             char c = text[textIndex];
286             if (node != null && nodeIndex >= node.end) {
287                 node = (Node) node.children.get(c);
288                 nodeIndex = node.begin;
289             }
290             if (node == null) {
291                 return false;
292             }
293             if (source[nodeIndex] == c) {
294                 textIndex++;
295                 nodeIndex++;
296                 return true;
297             }
298             return false;
299         }
300 
301         public int matchUtil() {
302             while (!finished() && matchNext()) ;
303             return textIndex;
304         }
305     }
306 }
View Code

 

以上是关于后缀树的主要内容,如果未能解决你的问题,请参考以下文章

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段

Sublime Text3自定义代码片段

后缀树

后缀数组与后缀树

sublime的片段功能