串的定义
串(String)是由零个或多个字符组成的有限序列(所谓序列就是相邻字符之间具有前驱和后继的关系),又名叫字符串
一般记作string-’a1a2a3…an‘(n>=0),其中string是串的名称,引号包裹的字符序列是串的值,注意引号不属于串的内容。
串中的字符数目n称为串的长度,定义中谈到有限是指长度n是一个有限的数值。零个字符的串称为空串
串的比较
给定两个串s1=“a1a2a3a4…an”,s2=“b1b2b3b4…bn”,当满足一下条件之一时,s1<s2
1、n<m,且ai=bi(i=1,2,3…n)
例如:当s1=“hap”,s2=“happy”,就有s1<s2.
2、存在某个k<=min(m,n),使得ai=bi(i=1,2,3…k-1),ak<bk
例如:s1=“happen”,s2=“happy”,e<y 所有 s1<s2.
串的抽象数据类型
对于线性表来说,线性表更多的关注单个元素的操作,比如插入、删除、查找等,但串中更多的是查找子字符串位置、得到指定位置子字符串、替换子字符串等
ADT 串(String)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继
Operation
StrAssign(T,*chars):生成一个其值等于字符串常量chars的串T
StrCopy(T,S):串S存在,由串S复制得串T
ClearString(S):串S存在,将串清空
StringEmpty(S):若串S为空,返回true。否则返回false
StrLength(S):返回串S得元素个数,即串得长度
StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0
Concat(T,S1,S2):用T返回由S1和S2联结而成得新串
SubString(Sub,S,pos,len):串S存在,1<=pos<=StrLength(S),且0<=len<=StrLength(S)-pos+1,用Sub返回串S得第pos个字符起长度为len的字串
Index(S,T,pos):串S和T存在,T是非空串,1<=pos<=StrLength(S).若主串S中存在和串T值相同的字串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0
Replace(S,T,V):串S,T,V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的字串。
StrInsert(S,pos,T):串S和T存在,1<=StrLength(S)+1.在串S的第pos个字符之间插入串T。
StrDelete(S,pos,len):串S存在,1<=pos<=StrLength(S)-len+1.从串S中删除第pos个字符起长度为len的子窜
endADT
例如index的操作:
//T为非空串,若主串S中第pos个字符之后存在与T相等的字串,则返回第一个这样的字串在S中的位置,否则返回0
int Index(String S,String T,int pos){
int n,m,i;
String sub;
if(pos>0){
n=StrLength(S);
m=StrLength(T);
i=pos;
while(i<=n-m+1){
SubString(sub,S,i,m); //取主串第i个位置
if(StrCompare(sub,T)!=0)
++i;
else //如果两串相等
return i; //返回I值
}
}
return 0; //无字串与T相等,返回0
}
朴素的模式匹配算法
字串的定位操作通常称为串的模式匹配
简单的说,就是对主串的每个字符作为字串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。
假设T[0]与S[0]存放对应串的长度:
//返回字串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0.T非空,1<=pos<=StrLength(S)
int Index(String S,String T,int pos){
int i=pos;
int j=1;
while(i<=S[0] && j<=T[0]){
if(S[i] == T[i]){
++i;
++j;
}else{
i=i-j+2; //i退回到上次匹配首位的下一位。
j=1; //j退回到字串T的首位;
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
KMP模式匹配算法
D.E.Knuth、J.H.Morris、V.R.Pratt(其中Knuth、Pratt共同研究,Morris独立研究)发表的一个模式匹配算法,可以大大避免重复遍历的情况,我们把它称之为克努特-莫里斯-普拉特算法,简称KMP算法。
KMP模式匹配算法原理
假设现在有==字符串S=“abcdefgab”,字符串T=“abcdex”.==如果用之间的朴素算法的话,前5个字母,两个串完全相等,直到第6个字母,"f"与“x”不等,如下图:
接下来,按照朴素模式匹配算法,应该是上图的2、3、4、5、6流程。即主串S中当i=2、3、4、5、6时,首字符与字串T的首字符均不等。仔细观察发现,对于要匹配的字串T来说,“abcdex”首字母“a”与后面的串“bcdex”中任意一个字符都不相等。也就是说,既然"a"不与自己后面任意一个字母相等,那么对于上图的1来说,前5位与主串S相同。那么串T的"a"与串S的2-5位字符也不可能相等。所以其实上图的2-5步的比较都是多余的。
注意这是理解KMP字符串算法的关键,如果我们知道T串中首字符“a”与“T”中后面的字符均不相等。而T串的第二位“b”与S串中的第二位“b”在图1中判断已经是相等的,那么就意味这,T串中首字符“a”与S串中第二位“b”是不需要判断也知道他们是不可能相等的了,这样图2-5其实都是可以省略的。
那么有人就会问,如果T串后面也含有首字符"a"怎么办?
我们来看下一个例子:假设S=“abcababca”,T=“abcabx”.
根据图1所知,串S与串T前5位均相等。根据刚才经验判断,T的"a"与T的“b”、“c”均不相等,所以图2、3可以省略。因为串T的首位“a”与第四位“a”相等,第二位“b”与第五位“b”相等。而在图1时,第四位的“a”与第五位的“b”已经与主串“S”中想对应位置比较过,是相等的,因此可以判定,T的首字符“a”、第二位字符“b”与S的第四位字符和第五位字符也不需要比较了。肯定是相等的。所以图4、5也是可以省略的。也就是说对于在字串中有与首字符相等的字符串,也是可以省略一部分不必要的判断步骤。
对比这两个例子,我们发现在图1时,我们的i值,也就是主串当前位置的下标是6,图2、3、4、5 i值是2、3、4、5到了图6,i值才又回到了6.即我们在朴素的模式匹配算法中,主串的i值是不断地回溯来完成的。而我们分析发现,这种回溯其实可以省略。KMP算法就是省略这些不必要的回溯。
比如第一张图中,由于T=“abcdex”,当中没有任何重复的字符串,所以j就由6变成了1.而第二张图中,由于T=“abcabx”,前缀“ab”与最后“x”之前串的后缀“ab”是相等的,因此j就由6变成了3.所以,我们可以得出规律,j值得多少取决于当前字符之前得串的前后缀的相似度。
我们把T串各个位置的J值的变化定义为一个数组next,那么next的长度就是T串的长度。于是我们可以得到下面的函数定义:
next数组值推导:
T=“ababaaaba”
1、当j=1时,next[1]=0;
2、当j=2时,next[2]=0;
3、当j=3时,next[3]=0;
4、当j=4时,j由1到j-1的串时"aba",前缀字符“a”与后缀字符“a”相等,next[4]=2;
5、当j=5时,j由1到j-1的串是“abab”,由于前缀字符“ab”与后缀字符“ab”相等,所以next[5]=3;
6、当j=6时,j由1到j-1的串是“ababa”,由于前缀字符“aba”与后缀字符"aba"相等,所以next[6]=4;
7、当j=7时,j由1到j-1的串是“ababa”,由于前缀字符“ab”与后缀“aa”并不相等,只有"a"相等,所以next[7]=2;
8、当j=8时,j由1到j-1的串是“ababaaa”,只有“a”相等,所以next[8]=2;
9、当j=9时,j由j-1的串是“ababaaab”由于前缀字符"ab"与后缀“ab”相等,所以 next[9]=3;
KMP模式匹配算法实现:
//计算返回字串T的next数组
void get_next(String T,int *next){
int i,j;
i=1;
j=0;
next[1]=0;
while(i<T[0]){ //此处T[0]为串的长度
if(j==0 || T[i]==T[j]) {
++i;
++j;
next[i] = j;
}else{
j= next[j]; //若字符不相同,j值回溯
}
}
}
//T非空,1<= pos<=StrLength(S)
int Index_KMP(String S,String T,int pos){
int i=pos; //i用于主串S当前位置下标值,若pos不为1,则从pos位置开始匹配
int j=1; //j用于子串T中当前位置下标值
int next[255]; //定义一组next数组
get_next(T,next); //对串T分析,得到next数组
while(i<=S[0] && j <= T[0]){ //若i小于S的长度且j小于T的长度时,循环继续
if(j == 0 || S[i] = T[j]){ //两字母相等则继续,相对于朴素算法增加了j=0的判断
++i;
++j;
}else{ //指针后退开始重新匹配
j=next[j]; //j退回合适的位置,i值不变
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
KMP算法改进
//求模式串T的next函数修正值并存入数组nextval
void get_nextval(){
int i,j;
i=1;
j=0;
nextval[1]=0;
while(i<T[0]){ //此处T[0]为串的长度
if(j==0 || T[i]==T[j]) { //T[i]表示前缀的单个字符,T[j]表示后缀的单个字符
++i;
++j;
if(T[i] != T[j]) //若当前字符与前缀字符不同
nextval[i] = j; //则当前的j为nextval在i位置的值
else
nextval[i]=nextval[j]; //如果与前缀相同,则将前缀字符串的nextval值赋值给nextval在i位置的值
}else{
j= nextval[j]; //若字符不相同,j值回溯
}
}
}
nextval数组值推导:
1、当j=1时,nextval[1]=0;
2、当j=2时,因第二位字符“b”的next值是1,而第一位就是“a”,他们不相等,所以nextval[2]=next[2]=1,维持原值;
3、当j=3时,因为第三位字符“a”的next值为1,所以与第一位的“a”比较得知它们相等,所以nextval[3]=nextval[1]=0;
4、当j=4时,第四位得字符“b”next值为2,所以与第二位的“b”相比较得到结果是相等,因此nextval[4]=nextval[2]=1;
5、当j=5时,next值为3,第五个字符“a”与第三个字符“a”相等,因此nextval[5]=nextval[3]=0;
6、当j=6时,next值为4,第六个字符“a”与第四个字符“b”不相等,因此nextval[6]=4;
7、当j=7时,next值为2,第七个字符“a”与第二个字符“b”不相等,因此nextval[7]=2;
8、当j=8时,next值为2,第八个字符“b”与第二个字符“b”相等,因此nextval[8]=nextval[2]=1;
9、当j=9时,next值为3,第九个字符“a”与第三个字符“a”相等,因此nextval[9]=nextval[3]=0;