后缀树
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 }
以上是关于后缀树的主要内容,如果未能解决你的问题,请参考以下文章
我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段
我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段