前言
KMP算法比较晦涩难懂,本文主要记载我对KMP算法的理解以及思路,主要参考代码随想录和网上大神的求解思路,如有错误望指正。
本篇推文主要面对有一定基础的读者,适用于对KMP有一定了解的读者。
一、next数组
我们知道KMP算法的核心就是求解next数组,next数组就是前缀表(本推文不介绍减一的next数组,只介绍将前缀表当作next数组的情况),而前缀表求的是最长相同前后缀的长度,因此next数组的定义比较简单,但是使用代码求解有一定难度,对于代码随想录中的求解方法我有点不太理解,因此我采用了网上其他大神的求解思路,可能具体思路大致一样,但是代码具体实现思路不一致,下面介绍我对求解next数组的理解。
我们其实可以将next数组的求解问题看作一个类似于动态规划的问题,next[i]表示下标i的最长相同前后缀的长度(本文记作len),求解思路如下:
用一个具体例子进行解释:
以a a b a a为例,很显然next[0]=0,next[1]=1,next[2]=0,next[3]=1,next[4]=2
如果我们对这个字符串新增加一个字符,那么next[5]等于多少呢?肯定是要根据具体的情况进行讨论
在介绍具体思路之前我们还需要明确一点就是next数组的含义,前面我们介绍过next[i]表示下标i的那个字符的最大共同前后缀长度len,那么其实这个len表现在字符串中我们可以理解为,len是指向最长前缀的下一个字符,是什么意思呢,我举个例子说明一下:
以字符串str=“aabaa”为例
next[0]=0,next[1]=1,next[2]=0,next[3]=1,next[4]=2
我们分析next[4]=2,即最长共同前后缀长度为2,即为字符串”aa“,而如果将len作为一个索引的化,那么len就是指向最长共同前缀的
下一个字符即”b”,意思就是str[len]=“b“,就是这么简单。
我们了解到len的具体含义之后,就可以继续之前的讨论,之前我们提到,如果对一个字符串新增加一个字符,那么新增加的这个字符的len是多少?,具体情况如下:
还是以字符串”aabaa”为例,大致可以分为两种情况:
1,增加的字符=“b“
2,增加的字符!=”b”
为什么要和b进行比较呢,其实很容易理解,因为next[4]=2,那么代表着这个字符串的前两个字符和最后两个字符是一样的,现在我要新加一个字符,那么这个字符只需要和第三个字符进行比较即可,如果相同,那么len直接加1即可,如果不同再进行讨论。但是这里存在了一个问题那就是我怎么知道每次要和谁进行比较,这次是和第三个字符进行比较,下次由于len的改变就不是和第三个字符进行比较了,这里就要用到之前我们提到的len的意义了,len指向的就是最长共同前缀的下一个字符,在“aabaa”这种情况下,len指向的不正是字符“b“吗,所以我们便有了以下伪代码:
if(str[len]==str[i])
{
len++;(这个len表示当前字符串末尾下标的最长共同前后缀长度)
next[i]=len;
i++;
}
else
{
其他处理;
}
以上就是当新添加的字符与len指向的字符一样的情况,如果不一样的,这里我们又要进行讨论,具体原因如下:
这次我们多举几个例子从中发现规律
以字符串”aabaa"为例,我们新添加一个字符c
那么此时的next[4]=2,next[5]=0;
以字符串”ababcabab"为例,我们新添加一个字符a
此时的next[8]=4,next[9]=3
继续以字符串”ababcabab"为例,我们新添加一个字符d
此时的next[8]=4,next[9]=0
以字符串”aabac"为例,新添加字符a
那么此时的next[4]=0,next[5]=0;
以字符串”aabaac"为例,新添加字符a
那么此时的next[5]=0,next[6]=0;
很明显我们可以知道当添加的字符和len指向的字符不一样时,可以分为两种情况,那就是len是否为0,如果len为0,那么不论你添加什么字符,你的len还是0,不会变,但是如果len不为0,这是求解next数组最难理解的地方,下面我会进行详细分析,先把len=0的伪代码写出来:
if(str[len]==str[i])
{
len++;
next[i]=len;
i++;
}
else
{
if len==0
next[i]=0;
i++
else
另行讨论;
}
接下来讨论len不为0的情况,我还是举具体例子进行理解
以字符串”ababcabab"为例,我们新添加一个字符a
那么next[9]等于多少和谁有关呢,这也是很难理解的地方,下面我进行详细分析
首先我们加了一个不等于len指向的字符的字符a,新的字符串为ababcababa
那么其实求向字符串”ababcabab"增加字符a的len就是求向字符串”abab“新增加字符a的len,原因如下:
由于a!=c,而已知abab为next[8]的最长公共子序列,此时新增加一个不等于c的字符,那么next[9]的最长共同前后缀序列一定小于4,
即最长相同前后缀一定是ababa(最后的这个a代表的就是新增加的a)中的一部分。
此时我们就可以知道求next[9]就是求ababa的next[4],即求向字符串”ababcabab"增加字符a的len就是求向字符串”abab“新增加
字符a的len。
现在我们就知道了求解next[9]就是求解在abab上新添加一个字符的最长前后缀长度,这又是一个新的求最长前后缀长度的问题,这不就进入了循环吗,我们只需要再按照前面的思路再进行一次判断,先判断这次的字符和len指向的字符是不是一样,注意此时的len改变了,不再是ababcabab的len,而是abab的len,此时的len的计算方法也很简单,就是下标为len-1的最长前后缀长度,不就是next[len-1]。因此求解next数组的代码如下:
while(i<str.size())
if(str[len]==str[i])
{
len++;
next[i]=len;
i++;
}
else
{
if len==0
{
next[i]=0;
i++;
}
else
len=next[len-1];
}
二、代码及运行结果
有了next数组之后,KMP算法就比较简单了,代码我直接给出:
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
void getNext(vector<int> &next, const string& s) {
int len = 0;
next[0] = 0;
for (int i = 1; i < s.size(); i++) {
if (next[len] == next[i])
{
len++;
next[i] = len;
i++;
}
else
{
if (len == 0)
{
next[i] = 0;
i++;
}
else
{
len = next[len - 1];
}
}
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
vector<int> next(needle.size(), 0);
cout << "字符串" << needle << "在字符串" << haystack << "中第一次出现的位置是:" << endl;
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while (j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size()) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
int main()
{
Solution s;
string haystack = "saaseasad";
string needle = "sad";
cout << s.strStr(haystack, needle);
return 0;
}
运行结果如下:、
总结
以上就是我对KMP算法的理解,KMP算法的难点在于如何建立next数组,本文主要针对next数组的建立进行分析,希望对读者有所帮助,如果文中有错误的地方,望指正。