和其他大佬一样,先说一下暴力求解字符串匹配问题,为什么一定要说呢,因为kmp是根据暴力改进而来,理解了暴力后kmp就不难理解了。
比如从s中匹配p,(匹配就是查找s中有没有一个子串等于p),给s串一个i,p串一个j,如果相同就均加一,如果不相等就把i回溯到开始的位置也就是i-j+1,然后把j归0。并不难理解,接下来看代码:
#include <iostream>
using namespace std;
int slove(string s, string p)
{
int i=0, j=0;
while (i < s.size() && j < p.size())
{
if (s[i] == p[j])
{
i++;
j++;
}
else //回溯操作
{
i = i-j+1;
j = 0;
}
}
if (j == p.size())
{
return i-j;
}
else
{
return -1;
}
}
int main()
{
string s, p;
cin >> s >> p;
int t = slove(s, p);
if (t == -1)
{
cout << "无法匹配" << endl;
}
else
{
cout << t << endl;
}
return 0;
}
接下来通过图片看一下首先给出s和p:(即使理解了也看一下,因为我是想引出kmp)
进行第一次匹配,我们发现到D的时候无法匹配了:
这个时候 i 等于5, j 等于5,然后回溯,把 i 变为i-j+1,j = 0
然后就到了下面的图片:
我们发现第一个就不相等,那就继续回溯,这个时候i=1, j = 0,回溯就变成了i++;直到碰到下一个A后:
但实际许多人已经发现了,中间的匹配B, C的过程是多余的,我们有没有一种方法可以跳过它或者说我们有没有办法能预先知道这中间是不匹配的。
这也就是接下来的next数组:next[i] 表示p串中 0~i-1这个前缀的最长相同前后缀
前缀后缀之类的概念不知道的可以去百度,这里就不说了,直接说一下算法的想法。
当到了如下图的时候,
我们把i值不变,也就是红线,然后把 j 变为next[j], 这一步相当把p串移动了j-next[j]步。
那么next数组到底是什么?
给出每一位最长的相同的前缀和后缀长度
然后给出p串的next数组
这个数组代表的意思是到当前位置得前一位最长的相同的前缀和后缀长度是多少,一定要知道next里面放的是前一位得最长相同前缀和后缀字符的个数,通过上面两个表理解。比如ABABC, 对于第二个A就是0,B就是1。这有什么用呢。这样就能知道了当前s与p不匹配的时候最多能移动多长的长度,像ABABC和ABAC匹配的时候,ABAC能知道我向后移动两个就行,这样就不会错过和第一个相等的字符了。
这个算法时间复杂度为O(m+n),最坏的情况下是O(N)的时间复杂度。
具体实现代码如下:
#include <iostream>
#include <string>
using namespace std;
const int MAX_N = 500001;
int Next[MAX_N];
void GetNext(string p)
{
int k = -1, j = 0;
Next[0] = -1;
//因为是找前一位,所以最后一位不需要在寻找了
while (j < p.size()-1)
{
if (k == -1 || p[j] == p[k])
{
k++;
j++;
Next[j] = k;
}
else
{
k = Next[k];
}
}
}
int KMP(string s, string p)
{
int i=0, j=0;
int pLen = p.size();
int sLen = s.size();
while (i < sLen && j < pLen)
{
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
j = Next[j];
}
}
if (j == p.size())
{
return i-j;
}
else
{
return -1;
}
}
int main()
{
string s, p;
cin >> s >> p;
GetNext(p);
int t = KMP(s, p);
if (t == -1)
{
cout << "字符串匹配失败" << endl;
}
else
{
cout << t << endl;
}
return 0;
}