串
是由零个或多个字符组成的有限序列,一般记为S=‘a1a2……an’
若两个串长度相等且每个对应位置的字符都相等时,称这两个串是相等的
子串
S=‘Hello World’ S1=‘Hello’ S2’World’ S1和S2都是S的子串
串的储存结构
1.定长顺序存储和堆存储结构
#define MAXLEN 255
//定长
typedef struct {
char ch[MAXLEN];
int length;
}SString;
//堆存储
typedef struct {
char *ch;
int length;
}HString;
2.链式存储结构
每个单链表表示一个字母
串的基本操作
StrAssign(&T,chars):赋值操作。把串t赋值为chars
StrCompare(S,T):比较操作,若S>T则返回值>0;若S=T,则返回0;若S<T则返回值<0
StrLength(S):求串长。
SubString(&Sub,S,pos,len)求子串.用Sub储存S中从POS开始长度为len的子串
concat(&T,S1,S2):串联接用T返回连接S1和S2的新串
最小操作子集
串的模式匹配
利用基本操作进行匹配
Index(S,T,pos):定位操作,若主串S中存在与串T值相同的子串,则返回它在主串中的第POS个字符后第一次出现的位置,否则返回0
int Index(SString S,SString T,int pos){
if(pos>0){
int n=StrLength(S);
int m=StrLength(T);
int i=pos;
SString sub;
while(i<n-m+1){
SubString(sub,S,i,m);
if (StrCompare(sub,T)!=0)
i++;
else
return i;
}
}
return 0;
}
简单的模式匹配算法
原理:
设置变量i表示主串的位置,i=1表示主串的第一个元素,设置变量j表示匹配子串的位置,j=1表示第一个元素,比较主串和配置子串的第一个元素,如果相等则,一次比较后面的元素,直到j大于匹配子串的长度则匹配成功,如果主串i对应元素与和匹配子串j对应元素不相等,则i返回j-2个位置,j重置为1,重新开始匹配,若主串一直没有匹配成功而剩余匹配元素低于匹配子串长度,则直接退出
简单匹配代码
//简单匹配算法
int Index2(SString S,SString T,int pos){
int i=pos,j=1;
while (i<=S.length-T.length&&j<=T.length){
if (S.ch[i]==T.ch[j]){
i++;
j++;
} else{
i=i-(j-2);
j=1;
}
}
if (j>T.length)
return i-T.length;
else
return 0;
}
KMP算法
原理
字符串的前缀,后缀匹配部分
1.前缀:一个字符串,从前面开始排,所包含的所有子串。如:“abcdefg”的前缀就是a,ab,abc,abcd,abcde,abcdef
2.后缀:一个字符串,从后面开始排,所包含的所有子串。如:“abcdefg”的后缀就是g,fg,efg,defg,cdefg,bcdefg
3.匹配值为;一个字符串的前缀和后缀共同的元素的最大长度,
如,上面的例子中没有一个元素是相等的则部分匹配值为0
如:“aba”的前缀是a,ab,后缀是a,ba ,其中有共同的元素a,而a的长度是1,所以“aba”的匹配值为1
如:“ababa”的前缀是a,ab,aba,abab,后缀a,ba,aba,baba,其中有共同的元素a和aba而其中aba的长度是3最大,所以ababa的匹配值是3
4.部分匹配值(MP):字符串和他所有的前缀的匹配值,如:ababa,前缀a的匹配值为0,前缀ab的匹配值为0,前缀aba的匹配值为1,前缀abab的匹配值为2,字符串本身ababa的匹配值为3,则字符串的部分匹配值(MP)为00123
计算部分匹配值(next数组的计算)
//kmp计算部分匹配值
void get_next(SString T,int next[]){
int i=1,j=0;
next[1]=0;
while (i<T.length){
if (j==0||T.ch[i]==T.ch[j]){
++i;++j;next[i]=j;
} else
j=next[j];
}
}
字符串匹配
基本原理和简单匹配算法一样,都是主串和匹配串分别设置指针,一个一个比较,最后找出子串,但是简单匹配算法有个缺点,每次比较失败之后i会退回j-2个位置,但是中间明显有一部分是匹配不成功的(因为不包含匹配算法的任何前缀),于是为了避免i回退,我们通过前缀和后缀的比较,如果比较的元素可能出现匹配值的前缀我们再去比较,如果根本不包含匹配值的前缀那就直接跳过刚刚比较过的这一部分,而如果存在相同的部分,只需要把j往后退对应的部分匹配值,这样的话i就永远不会回退。
如图:如果匹配过程中发现不匹配的元素,按照简单匹配算法则要再多匹配3次才可以匹配成功,而通过KMP算法则可以跳过中间两个浅绿色的部分,如图原来已经匹配了5个元素(黄色方框的元素),只需要经过前缀后缀的比较发现,前缀中有个ba,后缀中也有个ba,中间没有相同的前后缀,则直接跳过中间的a和c直接从第二个b开始比较,而因为前面已经比较过了bacba所以后面两个ba已经比较过了,所以字符串的j直接从第三个c开始比较就可以了,不用退回到1开始比较(退回的个数为对应部分匹配值得个数:2个)
int Index_KMP(SString S,SString T,int next[],int pos){
int i=pos ,j=1;
while (i<=S.length&&j<=T.length) {
if (j == 0 || S.ch[i] == T.ch[j]) {
i++;
j++;
} else {
j = j - 1 - next[j - 1];
}
}
if (j>T.length){
return i-T.length;
} else {
return 0;
}
}
KMP改进算法(nextval数组)
在KMP算法中通过next数组来判断子串j指针在比较失败之后的位置,但是如果比较失败的字母和next指向的字母刚好是相等的,那么按照kmp算法的逻辑,就应该比较当前主串指针i指向的字母和next指向的字母,由于之前比较失败的字母和next指向的字母相等,所以主串i和next指向的字母比较是毫无意义的(i和j已经失败了,next=j,所以i和next也必定失败)。如下图字符与第4个字符g不匹配,而第四个字符匹配失败时候j指向1,第一个字符还是g那肯定也不匹配(这次匹配毫无意义)。
所以如果next数组中出现字母与跳转后的字母相等的情况,就直接把这个字母的next 改为跳转之后的next值,新的这个数组被称之为nextval
如果出现子串中多次重复相同的字母nextval数组就比next要快的多
next数组变nextvl数组
for(int j=2;j<=T.length;j++){
if(T.ch[next[j]]=T.ch[j])
nextval[j]=nextval[next[j]];
else
nextval[j]=next[j];
}
真题
2019:abaabaabcabaabc主串匹配abaabc,手动计算比较次数,比较到第6个字符的时候发现不匹配,主串不动,子串退回到前面可能匹配的前缀,从刚才匹配失败的地方重新匹配,比如abaaba匹配失败用了6次,把子串像前平移到匹配失败之前的字符全部重合位置,比如子串前进3个字符就可以和主串的aba匹配成功,这个时候继续比较主串和子串从第6个字符开始,到第10个字符匹配成功,一共匹配了4次,加上前面的6次一共10次。