前置知识:串的基本概念
关于子串的几个概念:
- 前缀:指除最后一个字符以外,字符串的所有头部子串。
- 后缀:指除第一个字符外,字符串的所有尾部子串。
- 部分匹配值:指字符串的前缀和后缀的最长相等前后缀长度。
例如,对
"ababa"
这个字符串,其前缀为{a,ab,aba,abab}
,后缀为{a,ba,aba,baba}
,相交等于{a,aba}
,最长的相等的前后缀长度为3
。
同理,"a"
的最长相等前后缀长度为0
,"ab"
为0
,"aba"
为1
,"abab"
为2
。
故字符串"ababa"
的部分匹配值为00123
。
KMP算法
在朴素模式匹配中,指针的大量回溯及重复比较是其低效率的根源。
若在已匹配相等的前缀序列中,有某个(前缀的)后缀正好是模式串的前缀,则可将模式串向后滑动到与这些相等字符对齐的位置,主串指针i
无需回溯,直接从该位置开始继续比较。这就是KMP算法的核心思想。
稍微以我个人的理解讲一下,简而言之,就是在主串和模式串的匹配过程中,若匹配途中出现某一位匹配失败——此时,前面匹配成功的部分中,主串部分出一个后缀,模式串部分出一个前缀,若主串的这个后缀等于模式串的这个前缀,那么,此时的i
无需回溯,只需让模式串指针j
移动到模式串的这个前缀的下一位继续与主串进行匹配即可。
代码实现如下:
#define MaxLen 259//预定义最大串长,致敬CNCS传奇指挥259
typedef struct {//静态数组存储
char ch[MaxLen];//字符数组储存串
int length;//串长
}SString;
int nex[MaxLen] = { 0 };//next数组
int Index_KMP(SString S, SString T) {//KMP算法
int i = 1, j = 1;
for (; i <= S.length && j <= T.length; ++i, ++j) {
if (j == 0 || S.ch[i] == T.ch[j])continue;
//若j为0或当前位匹配成功,则继续匹配
else j = nex[j];//否则向右移动模式串
}
if (j > T.length)return i - T.length;//匹配成功
else return 0;//匹配失败返回0
}//最坏时间复杂度O(n+m)
对模式串的这种移动方式,需要预处理一个next[]
数组,用于存放模式串的“移动位数”。其实本质上就是对模式串自己和自己进行一次前后缀的“优化过”的朴素匹配(匹配过程中不断地调用了next
数组,若模式串长度为m
,则时间复杂度为O(m)
)。
void Get_Next(SString S) {//获取next数组
nex[1] = 0, nex[2] = 1;
for (int i = 1, j = 0; i < S.length;) {
//i记录next数组下标,j记录模式串前缀下标
if (j == 0 || S.ch[i] == S.ch[j])nex[++i] = ++j;
//若第i位和第j位匹配,则与主串匹配时到模式串的第i位不匹配的话,模式串直接移动到第j位
//若能一直匹配下去,则next值的计算实际上等价于next[j+1]=next[j]+1
else j = nex[j];//否则令j=next[j],若无next值则会让j回到0
}
/* //王道书上写法如下
int i = 1, j = 0;
nex[1] = 0;
while (i < S.length) {
if (j == 0 || S.ch[i] == S.ch[j]) {
++i, ++j;
nex[i] = j;//若pi=pj,则next[j+1]=next[j]+1
}
else j = nex[j];//否则令j=next[j],循环继续
}
*/ return;
}
next数组的进一步优化
在模式串中出现大量重复字符的时候,对next
数组可以进一步优化处理。
例如,对串"aaaab"
,若第4位匹配失败的时候,按照之前的算法,next[4]
是等于3
的,这意味着j
的值要回溯到3
,而next[3]
的值又是2
,以此类推,最后一定会回到j=0
的时候,并且之前全都匹配失败,因为模式串的前四个字符都相等。
为此,可以对next
数组进一步优化。观察可知,当第4位匹配失败时,主串中的当前子串其第4位一定不为a
,而模式串的前四位都为a
,因此可以直接让j
的值回溯到0
。
对next
数组再次递归处理,得到nextval
数组。此时匹配算法不变,但是把next
数组换成nextval
数组。
int nextval[MaxLen];
void Get_Val(SString S) {//从预处理的next数组得到nextval数组
for (int i = 1; i <= S.length; ++i) {//按位循环处理模式串
if (S.ch[i] == S.ch[nex[i]])nextval[i] = nex[nex[i]];
//若第i位与第next[i]位字符相同,则nextval[i]直接等于next[i]的next值
else nextval[i] = nex[i];//否则nextval[i]的值与next[i]的值一样
}
return;
}
如果模式串的长度为m
,则这种方法得到nextval
数组的时间复杂度为
O
(
m
2
)
O(m^2)
O(m2)。实际上,可以在计算next
时对算法略加改进,直接求出nextval
。
void Get_NextVal(SString S) {//直接求nextval数组
nextval[1] = 0;//第1位的nextval值一定为0
for (int i = 1, j = 0; i < S.length;) {//循环处理模式串,i为nextval数组下标
if (j == 0 || S.ch[i] == S.ch[j]) {
//若j为0,或者第i位和第j位匹配成功
++i, ++j;//继续后移一位i和j
if (S.ch[i] != S.ch[j])nextval[i] = j;
//若后一位不匹配,则说明匹配到第i位时失配可直接跳到第j位
else nextval[i] = nextval[j];
//若后一位匹配,则匹配到第i位失配时跳到的位置和第j位失配跳到的位置相同
}
else j = nextval[j];//匹配失败,j跳到nextval[j]的位置
}
return;
}
完整代码可以看我的Github:传送门
在408考研初试中,对KMP算法的考察不多,只要求掌握next
数组和nextval
数组的手算即可,但不失为一个难点。本章知识点需结合习题,进行充分理解,对初学者来说,更是需要反复阅读,手动推演来巩固学习成果。
以上。