KMP
KMP,是字符串的模式匹配算法,利用已经得到的信息,不需要回溯,所以比暴力解法效率高。关键是求next数组,也有称为前缀数组,那next数组是什么含义。
不需要回溯指的是:主串s的指针i不需要回溯。
1. 暴力解法
暴力解法有个很动听的名字,朴素字符串匹配算法。
暴力解法思路:
模式串中第一个字符和主串中的某个字符比较,如果相等,分别比较主串和模式串中的下一个字符;如果不相等,将这次比较的匹配信息全部丢掉,主串回到这次比较的第一个字符的下一个字符,和模式串的第一个字符比较。
思路清晰,代码实现:
#include <iostream>
using namespace std;
class Solution {
public:
/* 如果存在,则返回第一次在主串中出现的下标,否则返回-1。 */
int find_pos(const char *s, const char *p) {
int slen, plen, i, j;
slen = strlen(s);
plen = strlen(p);
for (i = 0; i < slen; i ++) {
for (j = 0; j < plen; j ++) {
if (s[i + j] != p[j])
break;
}
if (j == plen)
return i;
}
return -1;
}
};
int main()
{
char *s = "abcdefgk";
char *p = "def";
Solution sol;
int pos = sol.find_pos(s, p);
cout << "pos=" << pos << endl;
return 0;
}
2. next数组
next数组含义如:next[j] = k
当模式串p的第j个字符与主串s中第i个字符不匹配时,主串s的指针i不变,模式串p中的指针j退到next[j]指向的位置k,重新进行比较。
next数组前缀后缀的含义:当模式串p的指针j和主串s中的指针i不匹配时,求出在模式串p中指针j前的字符中长度最大的前缀和后缀相等的字符串。
这样的解释好像还没有那么清晰。
next[j] = k含义:
理解[1]:代表p[j]之前的模式串子串中,有长度为k的相同前缀和后缀;
理解[2]:当模式串中第j处字符失配时,下一步next[j]处的字符继续跟主串匹配,相当于模式串向右移动j - next[j]个字符。
把next数组过程理解清楚,代码实现也会容易。
已知next[j] = k,求next[j + 1]的值。
证明:
next[1] = 0(为什么不是1?因为当模式串中第一个字符和主串第i个字符不匹配时,此时模式串中相等的前缀和后缀长度是0,所以是0)
假设next[j] = k, 那么next[j+1] 是多少?
(1)当p[j] = p[k]时,则next[j+1] = k + 1。
因为next[j] = k,则有p[1…k-1] = p[j-k+1…j-1],当p[j] = p[k],则有p[1…k-1、k] = p[j-k+1…j-1、j],next[j+1] = k + 1=next[j] + 1。
(2)当p[j] != p[k]时,则现在模式串p中p[1…k-1、k] != p[j-k+1…j-1、j]。看了半天没看懂,参考严书,学的时候就记得严书写的很好。
此时,可把求next数组值的问题再看成一个模式匹配的问题,整个模式串既是主串又是模式串(前缀和后缀匹配的问题),而当前的匹配过程中,p[j] != p[k]时,应将模式向右滑动至以模式中的第next[k]个字符和主串中的第j个字符相比较。若next[k]=k’,且p[j]==p[k’],则说明在主串中第j+1个字符之前存在一个长度为k’的最长子串,和模式串中从首字符起长度为k’的子串相等,即p[1…k’-1、k’] != p[j-k’…j-1、j],也就是说next[j + 1] = next[k] + 1。
同理,如果p[j] != p[k’],则将模式继续向右滑动直至将模式中第next[k’]个字符和p[j]对齐,……,依次类推,直至p[j]和模式中某个字符匹配成功或不存在任何k’满足p[1…k’-1、k’] != p[j-k’…j-1、j],则next[j + 1] = 1。
数组下标从1开始。
书上举的例子,理解起来会容易一些。看下图,直接截图的。
如图4.6中的模式串,已求出前6个字符的next的值,现求next[7],因为next[6]=3,又p[6] != p[3],则需比较p[6]和p[1](因为next[3]=1),这相当于将子串模式向右滑动,由于p[6] != p[1],且next[1]=0,所以next[7]=1,而因为p[7]==p[1],所以p[8]=2。
算法1:
void calNext(const char p[], int next[])
{
int i, j;
int len = strlen(p);
i = 1;
j = 0;
next[1] = 0;
/* 书上写的是i<len,我总觉得该是i<=len */
while (i < len) {
if (0 == j || p[i] == p[j]) {
++ i;
++ j;
next[i] = j;
} else {
j = next[j];
}
}
return;
}
上面是《严书》写的,代码我稍微修改了下。
void calNext(const char p[], int next[])
{
int i, j;
int len = strlen(p);
i = 1;
j = 0;
next[1] = 0;
/*
* 下标从1开始,next数组长度是len+1,但char数组比较下标还是 从0开始
*/
while (i <= len) {
if (0 == j || p[i - 1] == p[j - 1]) {
++ i;
++ j;
next[i] = j;
} else {
j = next[j];
}
}
return;
}
我是比较清楚了,还有问题的同学也可以看七月算法官网上有视频。
前面定义的next函数在某些情况下还有缺陷。如……不清楚的同学还是请看严奶奶写的《数据结构》课本吧,写的太详细了P84,这里我仅截图。看到这个图种next数组的值,发现和我之前简单理解的有些偏差,证明过程的才是正确的求法。
算法2:
void calNextval(const char p[], int nextval[])
{
plen = strlen(p);
i = 1;
nextval[1] = 0;
j = 0;
while (i < plen) {
if (0 == j || p[i] == p[j]) {
++ i;
++ j;
if (p[i] != p[j])
nextval[i] = j;
else
nextval[i] = nextval[j];
} else {
j = nextval[j];
}
}
}
改进的nextval数组还不是完全理解,先写到这,音乐一直在响,该走了。
2. 使用KMP算法实现字符串模式匹配
现在时明白KMP算法是怎么回事了,那赶紧来实现以下吧。
#include <iostream>
using namespace std;
/* 求出next数组的值,从数组下标0开始 */
void calNext(const char p[], int next[])
{
int i, j;
int len = strlen(p);
i = 0;
j = -1;
next[0] = -1;
while (i < len) {
if (-1 == j || p[i] == p[j]) {
++ i;
++ j;
next[i] = j;
} else {
j = next[j];
}
}
return;
}
int main()
{
char *s = "abcdabcefghi";
char *p = "abce";
int *next = (int *)malloc(strlen(p));
calNext(p, next);
int i, j, slen, plen, res;
i = j = 0;
slen = strlen(s);
plen = strlen(p);
while (i < slen && j < plen) {
if (-1 == j || s[i] == p[j]) {
++ i;
++ j;
} else {
j = next[j];
}
}
if (i == strlen(s))
cout << "fail " << endl;
if (j == strlen(p))
cout << "i =" << i - plen << endl;
cout << "all right" << endl;
return 0;
}
暴露了用C的习惯。对数组名取sizeof,得出的值是数组中所有元素占的字节数,即使将数组名赋给指针p,对指针p取sizeof,返回的是保存指针p所占的字节数,而不是数组中元素占的字节数。
sizeof得出的值实际占用的内存空间的字节数。
参考:
[1] http://www.cnblogs.com/c-cloud/p/3224788.html
[2] 《数据结构》严书讲的好