前言:一般的书籍或文章讲解KMP算法都是从问题分析、思路讲解再到代码讲解。但是我觉得先看懂代码、再捋清思路反而会更容易,所以本文会以代码+代码分析的方式来讲解KMP算法。
一、BF算法的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int BF(const char* txt, const char* pat)
{
int i = 0;
int j = 0;
int txt_len = strlen(txt);
int pat_len = strlen(pat);
while (i < txt_len && j < pat_len)
{
if (txt[i] == pat[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if (j == pat_len)
{
return i - j + 1;
}
return -1;
}
int main()
{
char txt_str[101];
char pat_str[11];
puts("Please input main string (Max 100 chars):");
gets(txt_str);
puts("Please input pattern string (Max 10 chars):");
gets(pat_str);
printf("Matched position: %d\n", BF(txt_str, pat_str));
return 0;
}
二、BF算法没有处理好的地方
当主串的第4个字符和模式串的第4个字符不匹配时:
- 主串:aaaaaaac,模式串:aaab
BF算法:从主串的第2个字符和模式串的第1个字符开始向后匹配;
KMP算法:从主串的第4个字符和模式串的第3个字符开始向后匹配。 - 主串:aaacaaab,模式串:aaab
BF算法:从主串的第2个字符和模式串的第1个字符开始向后匹配;
KMP算法:从主串的第4个字符和模式串的第3个字符开始向后匹配。
可以看到BF算法在字符不匹配时,下一次比对字符的位置会做没有意义的回退。
三、KMP算法的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void init_next(const char* pat, const int pat_len, int* next)
{
next[0] = -1;
int j = 0;
int k = -1;
while (j < pat_len - 1)
{
if (k == -1 || pat[j] == pat[k])
{
j++;
k++;
next[j] = k;
}
else
{
k = next[k];
}
}
}
int KMP(const char* txt, const char* pat)
{
int i = 0;
int j = 0;
int txt_len = strlen(txt);
int pat_len = strlen(pat);
int* next = (int*)malloc(pat_len);
init_next(pat, pat_len, next);
while (i < txt_len && j < pat_len)
{
if (j == -1 || txt[i] == pat[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
free(next);
next = NULL;
if (j == pat_len)
{
return i - j + 1;
}
return -1;
}
int main()
{
char txt_str[101];
char pat_str[11];
puts("Please input main string (Max 100 chars):");
gets(txt_str);
puts("Please input pattern string (Max 10 chars):");
gets(pat_str);
printf("Matched position: %d\n", KMP(txt_str, pat_str));
return 0;
}
四、KMP算法的处理方式
- 设主串的长度为n,模式串的长度为m,则BF算法的时间复杂度是O(n*m),空间复杂度是O(1),KMP算法的时间复杂度O(m+n),空间复杂度O(m)。
- KMP算法分为两步,第一步是构造next数组(由init_next函数实现,时间复杂度为O(m),空间复杂度为O(m)),第二步是顺序逐个字符遍历主串(由KMP函数中的while语句实现,时间复杂度为O(n))。
- 模式串为aaab时,next数组为{-1, 0, 1, 2}。
- 变量i指示主串当前比对的索引位置,变量j指示模式串当前比对的索引位置,变量k指示模式串下一次比对的索引位置。
- next数组的值只与模式串有关,与主串无关,一共pat_len个元素,元素next[j]的值存储的是pat[j]与txt[i]不匹配时下一次与txt[i]比对的pat[k]的模式串索引位置k,k一定比j小。数组中-1和0代表重新从pat[0]开始比对,1代表重新从pat[1]开始比对,2代表重新从pat[2]开始比对,依此类推。
- next[0]一定等于-1,next[1]一定等于0,next[j]的值只与pat[0]到pat[j-1]这j个字符有关,next[j]的值与pat[j]是什么字符无关。
- 现在来讲讲next数组的值要怎么确定。字符串的前缀指除最后一个字符以外,字符串的所有头部子串。字符串的后缀指除第一个字符以外,字符串的所有尾部子串。以模式串aaab为例,next[0]和next[1]是固定值,next[2]的值由pat[0]到pat[1]这个字符串"aa"决定,"aa"的前缀为{a},后缀为{a}, { a } ∩ { a } = { a } \{a\}\cap\{a\}=\{a\} {a}∩{a}={a},{a}中最长的子串的长度为1,故next[2]等于1,next[3]的值由pat[0]到pat[2]这个字符串"aaa"决定,"aaa"的前缀为{a,aa},后缀为{a,aa}, { a , a a } ∩ { a , a a } = { a , a a } \{a,aa\}\cap\{a,aa\}=\{a,aa\} {a,aa}∩{a,aa}={a,aa},{a,aa}中最长的子串的长度为2,故next[2]等于2。
- 从代码实现的角度来说,在init_next函数中变量k存储的是当前子串(pat[0]到pat[j-1]这j个字符)的最长相等前后缀的前缀最后一个字符的索引位置,大家可以加一些打印自己跑一下代码。
- KMP函数中while循环里做的事情就很简单了。如果当前字符相同,则继续比对下一个字符,如果当前字符不同,则将主串中的当前字符继续与next数组指定的模式串中的字符比对。如果j到达模式串的结尾则退出循环(说明已经匹配到了结果),如果i到达主串的结尾了j依然没有到达模式串的结尾则说明主串中不存在模式串。
五、KMP算法的进一步优化
- 我们再来思考一种情况,当模式串为abab时:
原来的KMP算法的next数组:{-1, 0, 0, 1}
改进后的KMP算法的next数组:{-1, 0, -1, 0} - 当txt[i]和模式串的第4个字符’b’不匹配时,老算法会回到模式串的第2个字符’b’开始比对(注意上面两个next数组的第4个元素的差异),改进后的算法会回到模式串的第1个字符’a’开始比对,因为txt[i]既然不等于模式串的第4个字符’b’,那么txt[i]也不会等于模式串的第2个字符’b’。
- 改进前后next数组的差异都与上面这种原因类似,许多资料也将改进后的next数组称为nextval数组。
- 将init_next函数里if语句中的
next[j] = k;
语句替换为如下代码即可:if (pat[j] != pat[k]) { next[j] = k; } else { next[j] = next[k]; }