KMP,一个一言难尽的算法!
今天是5.20号,看女朋友不如KMP!
(仔细体会其深刻含义!!!我可没说这是拼音 )
好了,进入正题~
Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内查找一个模式串 P 的出现位置
KMP算法 的操作流程如下:
假设现在文本串 t 匹配到 i 位置,模式串 p 匹配到 j 位置
如果 j = -1,或者当前字符匹配成功(即 t[i] == p[j] ),都令 i++,j++,继续匹配下一个字符;
如果 j != -1,且当前字符匹配失败(即 t[i] != p[j] ),则令 i 不变,j = next[j]。此举意味着失配时,模式串 p相对于文本串 t 向右移动了 j - next [j] 位!
换言之,将模式串 P 失配位置的 next 数组的值对应的模式串 P 的索引位置移动到失配处
代码如下:
int KMP(const char* t, const char* p)
{
int next[MAX];
createNext(p, next);
int n = strlen(t),i=0;
int m = strlen(p),j=0;
while (i < n && j < m)
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
//匹配失败p向右移动了 j - next [j] 位
}
if (j >= m) //返回第一次出现的下标
return i - m;
else //匹配失败返回-1
return -1;
}
那么next数组是什么?又怎样构造出来的呢?
通俗一点讲,next[j] 表示,在模式串的 j 个字符失配了,然后下一次匹配从 next[j] 开始(next[j] 中保存的是该失配字符的前一个字符在前面出现过的最近一次失配的字符后面的一个字符的位置)。是不是有点绕?不慌,慢慢往下看就明白了。
next数组的构造分三步即可完成。
第一步:
找到模式串的非空子串
假设模式串为ABABA,则模式串ABABA的非空子串为
A | ||||
---|---|---|---|---|
A | B | |||
A | B | A | ||
A | B | A | B | |
A | B | A | B | A |
第二步:
计算各子串的前缀和后缀相等的最大长度。
可能有的人不知道前缀和后缀是什么意思,我解释一下。
所谓前缀就是这个子串除了最后一个字符以外,一个字符串的全部头部组合;
后缀则是除了第一个字符以外,一个字符串的全部尾部组合。
以ABAB这个子串为例,它的前缀有:
A AB ABA 从前往后找
它的后缀有:
B AB BAB 从后往前找
那么它们的前缀和后缀相等的最大长度为2,即AB。
按照以上方法计算其余的子串即可。
0 | A | ||||
---|---|---|---|---|---|
0 | A | B | |||
1 | A | B | A | ||
2 | A | B | A | B | |
3 | A | B | A | B | A |
第三步:
利用上一步得到的数据,得到next数组。
怎么做呢?
首先,我们有以下数据
最大公共长度数组
0 | 0 | 1 | 2 | 3 |
---|
然后将这组数据向右移动一位得到
0 | 0 | 1 | 2 |
---|
此时3被挤掉
最后在第一位添上-1得到next数组
A | B | A | B | A |
---|---|---|---|---|
-1 | 0 | 0 | 1 | 2 |
可能有的人会问,为什么这么做呢?
因为若模式串匹配失效,影响 j 指针回溯的位置的其实是以模式串第0位之第 j −1 位的子串的前缀和后缀相等的最大长度值,所以为了编程的方便, 我们不直接使用最大公共长度数组,而是将这个长度数组向后(即向右)偏移一位。我们把新得到的这个数组称为next数组。
代码如下:
void createNext(const char* p, int* next)//模式串的自匹配
{
int m = strlen(p);
int i = 0, j = -1;
next[0] = -1;
while (i < m)
{
if (j == -1 || p[i] == p[j])//abab
{
next[++i] = ++j;
//改进,如果相等,判断下一个是否也相等
//if (p[++i] == p[++j])
// next[i] = next[j];//当两个字符相同时,就跳过
//else
// next[i] = j;
}
else//不匹配
j = next[j];
}
}
好了,再贴以下完整代码
#include<iostream>
#include<string.h>
using namespace std;
const int MAX=100;
void createNext(const char* p, int* next)
{
int m = strlen(p);
int i = 0, j = -1;
next[0] = -1;
while (i < m)
{
if (j == -1 || p[i] == p[j])//abab
{
next[++i] = ++j;
//改进
//if (p[++i] == p[++j])//如果相等,判断下一个是否也相等
// next[i] = next[j];//当两个字符相同时,就跳过
//else
// next[i] = j;
}
else//不匹配
j = next[j];
}
}
int KMP(const char* t, const char* p)
{
int next[MAX];
createNext(p, next);
int n = strlen(t),i=0;
int m = strlen(p),j=0;
while (i < n && j < m)
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
}
if (j >= m) //返回第一次出现的下标
return i - m;
else
return -1;
}
int main()
{
char t[] = "bbababbabababa";
char p[] = "ababa";
cout << KMP(t, p);
return 0;
}
应用
找出给定字符串在文本中出现的次数,每两次匹配可重叠。
如文本abababa中出现多少次aba,输出结果为3
#include<iostream>
#include<string.h>
using namespace std;
const int max = 100;
void createNext(const char* p, int* next)
{
int m = strlen(p);
int i = 0, j = -1;
next[0] = -1;
while (i < m )
{
if (j == -1 || p[i] == p[j])
{
if (p[++i] == p[++j])//使用了改进的next求法
next[i] = next[j];
else
next[i] = j;
}
else
j = next[j];
}
}
int KMP(const char* t, const char* p)
{
int next[max];
createNext(p, next);
int ans = 0;//记录出现多少次
createNext(p, next);
int n = strlen(t), i = 0;
int m = strlen(p), j = 0;
while (i < n )//找完所有的文本
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
if (j == m)//j等于模板串长度说明成功匹配到了一次
{
ans++;
j = next[j];
}
}
return ans;
}
int main()
{
char t[15] = "abababa";
char p[5] = "aba";
cout << KMP(t, p);
return 0;
}
ps: 结尾强烈吐槽学校某文君老师,您一个行政岗的人跑来教学岗教书,写一节课的代码,要找一节课的bug,严重怀疑您是在拖延时间!到下课时,让我们自学后面难好多的算法,美名其曰锻炼我们,您可真“厉害”呢!师傅您都还没领我们进门呢!