算法学习之KMP算法,包你学会!
时间复杂度:o(m+n)
特点:每当一趟匹配过程中出现字符比较不等时,不需回溯主串迭代器,而是利用已经得到的“部分匹配”的结果将模式串向右“滑动”尽可能远的一段距离后(即移动模式串迭代器)继续比较。
相关概念:
- 字符串前缀:必须包含第一个字符,但不包含最后一个字符的连续子串。(aba 的字符串前缀有:a ab)
- 字符串后缀:必须包含最后一个字符,但不包含第一个字符的连续子串。(aba 的字符串后缀有: a ba)
- 最长相等前缀和后缀:就是最长的相同的前缀和后缀。
算法精髓:主串与模式串匹配过程中比较模式串中的第k个字符不等时,模式串迭代器重新指向模式串前k个字符的最长相等前缀的后一个字符,再继续比较。
算法原理:
正在比较a与c,很明显不相等。主串的迭代指针不动,模式串的迭代指针应该回溯到最长相等前缀的后一个字符,即‘a’。然后再继续比较。
这是因为当主串中的某个字符与模式串中的第k个字符不等时,包括该字符在内的前k个字符与模式串中的前k个字符拥有相同的最长相等前后缀。因为该字符的前k-1个字符是和模式串的前k-1个字符完全相等的。
应用实例:在主串“abababc”中查找“ababc”
我们已经知道了KMP算法就是在比较过程中不断地移动模式串指针到最长相等前缀后一个字符。但还有一些细节需要我们处理:
-
什么时候移动主串迭代器?
在字符匹配相等时、当模式串迭代器指向首字符前一个字符时(比如数组索引为-1)
-
什么时候返回结果?
在模式串迭代器指向末尾字符后一个字符时,在主串迭代器指向末尾字符后一个字符时
-
模式串中所有子串(包括自己)的最长相等前后缀的长度怎么求?
暂且按下不表,假设我们已经求出,并将其存放在next数组中
代码:
typedef struct str {
char* val;
int length;
}String;
int* getNext(String sub);
//使用KMP算法得到模式串的索引
//str:主串
//sub:模式串
//返回:找不到返回-1,否则返回首字符在主串中的索引
int indexOf_kmp(String str, String sub) {
int i = 0, j = 0;//i,j分别迭代主串、模式串
int* next = getNext(sub);//next数组存储模式串所有子串的最长相等前后缀的长度
while (i < str.length-1 && j < sub.length-1) {
if (j == -1 || str.val[i] == sub.val[j]) {
i++; j++;
}
else {
j = next[j];//next[j]存储着前j个字符的最长相等前后缀的长度,而最长相等前缀后一个字符的索引正好等于该长度
}
}
free(next);
if (i == str.length)
return -1;
return i - 1 - sub.length;
}
求解next数组:
算法精髓:两个迭代器初始时刻一个指向第一个字符,一个指向第二个字符,求next[j]只需比较两个迭代器指向的字符,如果相等则next[j]=next[i]+1,再分别增加两个迭代器。如果不相等,则令第一个迭代器指向第next[i]个字符,再进行比较,相等则next[j]=next[next[i]]+1。如果不相等,则继续令第一个迭代器指向第next[next[i]]个字符……直到相等再分别增加两个迭代器。(其实就是反复使用KMP算法精髓)
在求解之前,我们先来规定一下一些特殊情况:
- 第一个字符不存在前缀、后缀,规定next[0]=-1
- 如果一直比较不相等,直到第一个迭代器指向next[0],即-1时,令next[j]=0
算法原理:
若一个椭圆代表一个字符,假设两个迭代器指向的字符相等,则next[j]=next[i]+1
假设两个迭代器指向的字符不等,且next[i]=7,则i=7
如果还不相等,假设next[7]=3,则i=3
如果相等了,则第一个蓝框和最后一个蓝框向右扩展一格,即next[j]=next[next[i]]+1。如果不相等则继续让i=next[i],直到i=-1或者相等。
代码:
int* getNext(String sub) {
int* next = (int*)malloc(sizeof(int)*sub.length);
next[0] = -1;
int i = 0, j = 1;
while (j < sub.length) {
if (i == -1) {
i = 0;
next[j] = 0;
j++;
}
else if (sub.val[i] == sub.val[j]) {
next[j] = i + 1;
i++;
j++;
}
else {
i = next[i];
}
}
return next;
}
可以发现存在一些冗余代码,我们优化一下:
int* getNext(String sub) {
int* next = (int*)malloc(sizeof(int)*sub.length);
next[0] = -1;
int i = 0, j = 1;
while (j < sub.length || sub.val[i] == sub.val[j]) {
if (i == -1) {
next[j++] = ++i;
}
else {
i = next[i];
}
}
return next;
}
大功告成!