字符串以及后缀-一些定义
字符串规定以$结尾,也就是说一个n个字符长的字符串s有,s[n]=$
|s| = n 表示字符串s的长度
s[i]表示s中第i个字符,其中i=1,2,…,|s|
s[i…j]表示字符串s的从第i为起始到第j位结束的子字符串
这样就可以定义后缀了:
suff1:=s
suffi:=s[i]s[i+1]...s[s],i=2,..,|s|−1
suff|s|:=s[|s|]=$(空字符串)
相应的前缀(Präfix)就可以如下表达
prej=s[1]...s[j]
如此每个字符串的子字符串就可以如下表示:
s[i...j]=prej−i+1(suffi)
//小问题,为什么要弄个后缀呢,他有什么好处???
动机(Motivation):
后缀(Suffix)在许多重要的应用中起到了重要的作用。比如:
在Text中寻找目标字符串,定位,实现正则表达式
要求(Erfordern):
更高效的存储后缀:包括更少得空间需求以及更短的访问时间
本节主要内容:
1.后缀的数据存储结构
2.如何建立这种数据结构
3.一些后缀的应用
后缀树(Suffix-Baum)
定义
ST(s)代表字符串s的后缀树
ST(s)有|s|个叶子节点(Blätter),分别表示s的|s|个后缀
ST(s)的每个内部节点拥有至少两个子节点
ST(s)的每一条边表示s的一个子字符串
从跟节点到叶节点i的链路表示s的一个后缀
suffi
//插图两张 若能上传
深度
path-label(v)表示ST中的一个节点v
从跟节点到节点v的链路,干好对应了通过点v表示的前缀。
string-depth(v)(String-Tiefe):
就是指从跟节点到节点v的链路上得字符的总数目:|path-label(v)
tree-depth(v)(Baum-Tiefe):
就是指从根节点到目标节点v经过的节点的数目。
因为每条边上描述的字符串的长度至少为1,所以对于ST的每一个节点有:
string-depth(v) >= tree-depth(v)
右侧最大(Rechts-Maximalität)
两个后缀的最长相同前缀(Die Längsten gemeinsamen Präfixes zweier Suffixe),就是对应两个节点在ST中的最长共同路径。
字符串s中的一个重复出现的子字符串t被称为右侧最大(Rechts-Maximalität)有两个条件:
1.t至少在s中出现两次
2.t的后续字符都不全相同
比如:s=mathematik,那么t=mat而不是t=ma
ST中的每一个内部节点都对应了一个右侧最大子字符串(rechts-maximalen Teil string), 相反的如果一个字符串在ST没有找到相应的内部节点,那么他就不是右侧最大子字符串。
后缀指引(Suffix-Verweis)
已知节点v的路径描述为(即路径上描述的字符串)ca,其中c为单个字符,而a为字符串(可以为空)。节点u的路径描述是a。那么我们就称一个从节点v到节点u的指针为后缀指引,标记为SL(v) = u
性质:
每个在以节点v为根节点的子树中的后缀都有相同的前缀ca。相应的拥有相同前缀a的后缀都在以节点u为根节点的子树中。
除根节点外的每一个内部节点都有一个后缀指引(Suffix-Verweis)
节点v下得整棵子树都会在以u节点为根节点的子树下出现
SL(v)=u
⇒
tree-depth(u) >= tree-depth(v)-1
//为什么是大于或等于而不是等于呢????可以举出例子但不知为何??
一般后缀树(generalisierter Suffix-Baum)
前面看到的后缀树都是单一字符串的后缀树,现在来看一下字符串集合S={
s1,...,sk
}的情况。
一般后缀树(generalisierten Suffix-Baum:GST(S))包括所有字符串的后缀,也就是联合了所有的ST(
Si
)
每一个叶子节点都用整数Tupel(i,j)进行描述,他表示
si
的
suffj
每个内部节点都描述字符串集S中的一个字符串的子字符串。或者他也可以用一个整数三元式表示(i,j,l)他表示
si
的[i…j]子字符串
//插图一张
词典序(Lexikalische Tiefensuche im Suffix-Baum)
所有到目前为止所说得后缀树,其中的节点都是按照字典的顺序从左到右排序的
后缀数组及LCP数组
用SA(s)表示字符串s的后缀数组
SA(s)刚好有|S|个项,每个项代表一个后缀。
SA(s)中的项是按照字典序进行排序的
经常和后缀数组搭配使用的数组是LCP(Longest Common Prefix),他表示后缀数组中相邻两个项(后缀)之间的最长相同前缀。
//插一图
由后缀树生成后缀数组
( TiefensuchedurchBaum→EinträgeSuffix−Array )
//Algorithmus SuffixArrayAusSuffixBaum(ST,s):
int[] suffixArray = new int[|s|];
int[] lcpArray = new int[|s|-1];
int p = 1;//aktuelle position in den Arrays数组当前位置
int minDepth = 0; //minimale String-Tiefe seit letztem //Blatt
//按照深度搜索遍历树,即兄弟间不能移动,只能父子间移动
while(<lexikograhpische Tiefensuche über ST>){
//如果当前节点是叶子节点
if(<aktueller Knoten ist Blatt>){
suffixArray[p] = <Beschriftung des aktuellen Knotens>;//把当前节点索引i,写入后缀数组
if(p != 1) lcpArray[p-1] = minDepth;
//minDepth设为当前节点的string-depth
minDepth = string-depth(<aktueller Knoten>);
p = (p+1);
}else{
//minDepth为当前节点的string-depth和minDepth的最小值
minDepth = min(minDepth,string-depth(<aktueller Knoten>));
}
//移动到下一个节点
<gehe zum nächsten Knoten in Tiefensuch-Reihenfolge>
}
//当向上移动时(父节点)缩小minDepth,向下则保持不变。
//当到达叶子节点是把minDepth设为对应节点的string-depth。
//可插图
由后缀数组和LCP数组生成后缀树
//Algorithmus SuffixBaumAussuffixArray(SA,LCP,s):
Knoten root = new Knoten();
Knoten leaf = new Knoten(SA[1]);//空字符串节点
//从根到第一个节点的边
Kante edge = new Kante(root, leaf, "$");
int p = 1; //aktuelle Position in den Arrays
while(p < |s|){
//沿着边以(|s|-SA[p]+1-LCPA[p]的距离往回移动)
<steige auf entlang edge um (|s|-SA[p]+1-LCPA[p])Zeichen>
//如果移动后刚好落在节点上
if(<Aufstieg endet an Knoten>){
leafParent = <Knoten, an dem Aufstieg endet>;
}else{ //aufstieg endet mitten in Kante
leafParent = new Knoten();
//通过插入新的节点开辟新的边
<spalte edge durch einfügen von leafParent
}
leaf = new Knoten(SA[p]);//Knoten mit SA[p] als //Beschriftung
edge = new Kante(leafParent, leaf, s[SA[p+1]+LCPA[p]...|s|];
p = p + 1;
}
//可插图,不过图上都是德语
构建后缀树
McCreight-Algorithmus:
//Algorithmus SuffixBaumAusString(s):
Knoten root = new Knoten();
Knoten leaf = new Knoten();//Knoten für ganzen String //bzw. Suffs
//从根到第一个节点的边
Kante edge = new Kante(root, leaf, suff1);//Kante von //Wurzel zu erstem Blatt
//当前后缀索引
int i=2; //aktuelles Suffix
while(i <= |s|){
//从根节点出发对比新后缀与已经生成的树,知道出现第一个不同的字符,即停止
<Steige von der Wurzel in den Baum ab, so lange die bisher durchschrittene Pfad-Beschriftung mit suff_i übereinstimmt>
//把这段重复的字符串命名为m
<Benenne Anzahl der übereinstimmenden Zeichen mit m>
Knoten leafParent;
//如果结束的位置刚好在节点上
if(<Abstieg endet an Knoten){
//把leafParent设为上面停止时的节点
leafParent = <Knoten, an dem Abstieg endet>;
}else{
leafParent = new Knoten();
//通过插入新节点开辟新边
<spalte zuletzt durchschrittene Kante Einfügen von leafParent>
}
leaf = new Knoten();
edge = new Kante(leafParent, leaf, s[i+m...|s|]
p = p + 1;
}
这种方法构建后缀树的所用的时间是 O(n2) //??