更新 2020/10/9 (感觉还是写得很乱)
KMP算法是朴素的匹配算法BF算法的升级版。
BF算法的时间复杂是O(n * m),KMP算法的时间复杂度是O(n+m)。
其改进在于:每当一趟匹配过程中出现字符比较不等时,不需要回溯i指针,而是利用已经得到的“部分匹配”的结果将模式串向右“滑动”尽可能远的一段距后,继续进行比较。
例子:
主串S = “ababcabcacbab”
模式串P = “abcac”
当模式串和主串在 i=3 这个地方匹配失败时,不再将其整体右移一个位置,重新从头开始匹配,而是根据模式串的前缀表(就是next数组)相应位置的值,进行模式串的移动,主串指针不回溯。
简单来说就是:失配时,根据模式串失配位置前的子串的相同前后缀的最大长度进行调整,重新匹配。
比如上图第二趟匹配,失配时,因为失配位置前四个字符构成的字符串的相同前后缀的最大长度为1,那么,下一次匹配时模式串的第一个字符就不用再比较了,直接用第二个字符与刚才失配位置的主串字符比较。
由此消除了主串i指针的回溯,减少了很多不必要的比较,提高了效率。
模式串的前缀表(next数组)就是KMP算法的精髓与灵魂所在,next[i] 表示模式串的前 i 个字符构成的字符串的最长前缀和最长后缀相同的长度。
前缀是要比原字符长度短的,例如“abcac”的前缀有:“a” , “ab” , “abc” , “abca”
后缀也是要比原字符长度短的,例如“abcac”的后缀有:“c” , “ac” , “cac” , “bcac”
next[0]初始化为-1,匹配失败时,如果我们假设-1下标这个位置是存在的,将其移动到匹配失败的位置,由下图可直观看出,相当于将整个模式串向右移动一个位置。
例子:
next[1]表示P的前 1个字符构成的字符串,即"a"的最长前缀和最长后缀相同的长度,为0
next[2]表示P的前 2个字符构成的字符串,即"ab"的最长前缀和最长后缀相同的长度,为0
next[3]表示P的前 3个字符构成的字符串,即"abc"的最长前缀和最长后缀相同的长度,为0
next[4]表示P的前 4 个字符构成的字符串,即"abca"的最长前缀和最长后缀相同的长度,为1
那我们要怎么用代码实现,求出nxet数组呢?
next[0]已经被初始化为了-1。所以我们从next[1]开始求,即从下标为1的位置开始比较即可!
int i = 0;
int j = next[i];
如果 p[i] == p[ j ] , 则 next[i+1] = next[i] + 1 .
如果不相等,j = next[j] ,如果相等,则next[i+1] = next[next[i]+ 1,否则继续套娃。如果到达边界都没有匹配成功 ,即 j== -1,则 next[i+1] = 0;
void GetNext(string P, int next[]) //next[i]表示模式串P的前i个字符的最长前缀和最长后缀相同的长度
//例如next[2]表示p[0]+p[1]的最长前缀和最长后缀相同的长度 //就是当前位置记录前一个的
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1; //设成-1,可以在匹配不上的时候,并且已经回溯到了第一个字符时,作为边界条件,然后模式串会整体右移一个位置
next[0] = -1;
while (i < p_len)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
next[i] = j;
}
else j = next[j];//匹配不上,移回next数组对应的位置继续匹配,直到符合要求或者达到边界
//这里用了个中间变量,套娃就比较好写了 但本质还是套娃
//cout << i << " " << j << endl;//可以输出一下观察是怎样变化的
}
}
水平有限,有些地方解释的不好。
完整的代码
#include <iostream>
#include <cstring>
#include<string>
#include<string.h>
using namespace std;
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[]) //next[i]表示模式串P的前i个字符的最长前缀和最长后缀相同的长度
//例如next[2]表示p[0]+p[1]的最长前缀和最长后缀相同的长度 //就是当前位置记录前一个的
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1; //设成-1,可以在匹配不上的时候,并且已经回溯到了第一个字符时,作为边界条件,然后模式串会整体右移一个位置
next[0] = -1;
while (i < p_len)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
next[i] = j;
//if (P[i] != P[j]) next[i] = j;
//else next[i] = next[j];
}
else
j = next[j];//匹配不上,移回next数组对应的位置继续匹配,直到符合要求或者达到边界
//这里用了个中间变量,套娃就比较好写了 本质还是套娃
//cout << i << " " << j << endl;//可以输出一下观察是怎样变化的
}
}
/* 在 S 中找到 P 第一次出现的位置 */
int KMP(string S, string P, int next[])
{
GetNext(P, next);
int i = 0; // S 的下标
int j = 0; // P 的下标
int s_len = S.size();
int p_len = P.size();
while (i < s_len && j < p_len)
{
if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j]
{
i++;
j++;
}
else
j = next[j]; // 当前字符匹配失败,进行跳转
}
if (j == p_len) // 匹配成功
return i - j;
return -1;
}
int Next[1000010] = { 0 }; //next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度
int main()
{
int n;
cin >> n;
while (n--)
{
string s1, s2;
cin >> s1 >> s2;
int v = KMP(s1, s2, Next);
cout << v << endl; //s2在s1中第一次出现的位置
}
}