引言:
KMP算法是一个子串匹配算法,即在一个主串 S 中寻找给定字串 T 的算法。KMP算法用于字符串的顺序存储结构的子串匹配。
对于子串匹配,简单粗暴的解法肯定是从主串的第一个字符开始,与待匹配字串进行比较。如果发现与字串不能完全匹配(相等),就倒退到主串的第二个字符开始。。。然后从第三个字符开始。。。,一直到最悲惨的比较到主串的最后一个字符。
在这个算法中,一旦在比较过程中出现主串中的某个字符与子串中的字符不相等的情况,主串的指针 i 就必须得回到刚才开始匹配时的位置的下一个位置(回溯),相当于走 n 步退 n-1 步,嗯,不等于没走,等于走 1 步,之前走的 n-1 步都是无用功。
而KMP算法则不然,KMP算法让主串的指针一直向前走,不用往后退,所以大幅提高了匹配的效率。(或许不够严谨,但先这么说)。
KMP算法的核心(请仔细阅读):
KMP算法的核心是保持主串指针 i 一直向前移动,每次出现主串指针指向的字符与待匹配子串指针指向的字符不等的情况,就根据待匹配子串的已匹配序列计算出子串指针要指向的位置,然后继续匹配。
在KMP算法过程中,主串指针是一直向前移动的,不会后退,每次出现字符不匹配的情况,仅待匹配子串的指针会向后退。
所以,现在问题的关键就是怎样根据待匹配子串的已匹配序列计算出待匹配子串的指针应指向的位置(怎样更新待匹配子串的指针)。幸运的是,找待匹配子串指针位置的方法仅与待匹配子串有关,与主串无关。
KMP算法的关键( next函数 ):
假设主串的 子序列与待匹配串完全匹配且有,那么有 ;
然后讨论子序列 ,如果在子序列 中有 ,且不存在 满足上式,那么就有 ,即待匹配串的前 k 个字符与主串指针 i 前的 k 个字符相等,此时便可直接比较 和 是否相等。由此,匹配仅需从 待匹配串的下标为 k 的字符 与 主串的下标为 i 的字符 起继续进行。
令 next[j] = k, 则 next[j] 表明当待匹配串中 j 号字符与主串中相应字符“失配”时,在待匹配串中需要重新和主串指针 i 当前字符进行比较的字符的位置,即待匹配串指针 j 要跳向的位置。由此,给出待匹配串的 next 函数的定义:
也就是说字符串中 0 号元素为 -1,如果有 k 值满足 ,且不存在 满足上式(也就是说 k 是满足上式的最大值),则 next[j] = k,如果 且不存在 k 使待匹配串中 j 号元素之前的 k 个字符与开头的 k 个字符相同,则 next[j] = 0;
在实际使用中,next函数值是由递推的方式求得的。下面给出思路。(建议直接看伪码,看不懂了再看)
根据定义:
next[0] = -1;
设 next[j] = k,这表明在模式串中存在下列关系(假设模式串为p):
其中 k 为满足 1 < k < j 且满足上式的最大值。此时,next[j+1] 有两种情况:
<1> 若 ,则在模式串中,
并且不存在比 k 更大的值满足上式,所以 next[j+1] = k+1,即 next[j+1] = next[j] + 1
<2> 若 ,则在模式串中,
此时,可把求 next() 函数值的问题看成是一个模式匹配的问题,整个模式串既是主串又是模
式串,而当前在匹配的过程中已有
则当 时,应将模式串向右滑动到模式串的 next[k] 号字符与主串中的第 j 个字符比
较主串和模式串是一个串,都是待匹配串)。若有 ,且 ,则说明在主
串中 j+1 号字符之前存在一个长度为 的最长子串,和模式串中从首字符开始的长度为
的子串相等,即 ,也就是说,next[j+1] = ,
即next[j+1] = next[k] + 1。同理,若 ,则将模式串继续向右移动直至将模式中
号元素与 对齐。。。依此类推,直至 和模式串中某个字符匹配成功或者
不存在任何 满足 ,此时有
KMP算法的伪码描述:
KMP算法伪码描述:
初始化主串i,待匹配串指针j
if (待匹配串的首字符与主串指针当前指向元素不匹配 或 主串与待匹配串的当前字符匹配) {
将主串指针向后移动一个位置
将待匹配串指针向后移动一个位置
}
else {
j = next[j]; // 待匹配串回溯
}
if (待匹配串指针j >= length(待匹配串) {
return i - length(待匹配串);
}
return -1;
next() 函数伪码描述(依据KMP算法给出):
j = 0; next[0] = -1; k = -1;
while (j < length(待匹配串)-1) {
if (k回溯到第一个字符 或 t[j]==t[k]) {
++k;
++j;
next[j] = k;
}
else { // t[j] != t[k], k指针回溯,模式串右移
k = next[k];
}
}
KMP算法示例:
KMP算法主体:
```C
// 使用普通的字符串,字符串必须以'\0'结尾,且模式串不为空串
int KMP(char * s, char * t){ // s为主串 t为模式串
int i = 0, j = 0; // 主串指针 模式串指针
int lenOfS = strlen(s), lenOfT = strlen(t);
int *nextVal = (int *) malloc (lenOfT *sizeof(int));
next(needle, nextVal); // 计算得到 next 函数值,将其存放在nextVal数组中
while (i < lenOfS && j < lenOfT) {
if (j == -1 || s[i] == t[j]) { // 首字符不匹配 或 当前字符匹配,指针增1
++i;
++j;
}
else { // 模式串指针回溯
j = nextVal[j];
}
}
free(nextVal);
// 如果模式串成功匹配,返回首字符位置,否则,返回 -1
return (j >= lenOfNee) ? (i - lenOfNee) : -1;
}
next 函数:
```C
void next(char *t, int nextVal[]) {
int j = 0, k = -1; // 主串指针 模式串指针
int len = strlen(t)-1;
nextVal[0] = -1; // 递推起点
while (j < len) {
if (k == -1 || t[j] == t[k]) { // k回退到首字符 或 t[j]=t[k]
++k;
++j;
nextVal[j] = k; // 相当于 next[j+1] = k+1;
}
else { // 不匹配,k回溯
k = nextVal[k];
}
}
}
总结:
KMP算法相当于,“咦,出现状况了。”“没事儿,你先在原地等会儿,这个小 case 我来解决。” 于是,待匹配子串指针查了一下表,然后‘啪’的一声换了个位置(向后退了几步),然后大家手拉手一起向前走。
(本文参考了严蔚敏老师的《数据结构(C语言版)》第一版)