本人是普通本科院校的一名大二学生,为提高自身能力特地锻炼自己发表一些文章,题解等,
望各位大佬能够对我这个菜鸟所做多多指正,在此谢过各位啦~~~
文章的开始,我想向KMP算法的三位创始人Knuth,Morris,Pratt致敬,如此思维之精妙,堪称大神。
KMP算法是字符串匹配算法中一个很重要的算法,KMP算法的作用是在一个字符串中寻找子串的位置,也叫串的模式匹配。如主串s="aaabbaab",子串t(也叫模板串)="aab",要寻找aab在主串中的位置。
字符串匹配的暴力做法就是分别将两个指针i,j指向主串和子串的第一个字符,从第一个字符开始,将两个字符串的字符一一比对,如果不匹配,那么主串的i指针指向主串当前的下一个位置,继续将两个字符匹配,如果不匹配那就继续移动i指针,如果匹配了,就将指向子串的j指针继续往子串的下一个字符移动如果匹配,继续执行,直到j指针遍历完整个子串为止,如果不匹配,那么就是将指向字串的j指针重新指向子串的第一个为止,这个过程就叫回溯,继续将主串和子串的i,j指针进行匹配,直到找到子串的位置为止。那么此时我们发现,每次我们匹配时j指针总会有一定的概率回溯,如果是非常庞大的数据,时间消耗会大大增加,那么问题来了,如何对这个步骤进行优化呢?这,就是KMP算法的魅力所在。
KMP算法主要分两步,第一步是求next数组,第二步是进行匹配。next数组是对回溯的优化,就是说如果移动i指针,匹配之后移动j指针,不匹配时,先写一个函数在子串中找到子串的最长公共前后缀,比如子串“aba”,最长公共前后缀的长度为2,然后最长公共前后缀为“ab”.暴力算法是不匹配时将子串的j指针移到子串的开始位置,但是KMP算法优化后是将j指针移到最长公共前后缀的后缀位置的起点继续进行匹配,此时我们就不需要再把j指针移到起点了,就相当于和美女看不对眼,就退而求其次,但是不是退回起点。
下面是KMP算法的代码详解:
#include<stdio.h>
#define N 100010
#define M 100010
char p[N],s[N];
int n,m;//n是子串的长度,m是主串的长度
int ne[N];//next数组
int main()
{
scanf("%d%s%d%s",&n,&p,&m,&s);
//求next数组的过程
for (int i = 1, j = 0; i < n; i++)//下标从0开始
{
while (j&& p[i]!= p[j]) j = ne[j-1];//求字串的next数组,j指向子串的起始位置,i指向子串的起始位置的下一个位置
if (p[i] == p[j]) j++;//如果相等,j指向子串的下一个位置
ne[i] = j;//子串中下标为i的字符前的字符串最长相等前后缀的长度为j
}
for (int i = 0, j = 0; i <m; i++)//i的话是遍历一遍主串的所有字母,j是从子串每次从零开始遍历,看能不能匹配成功,
{
while (j && s[i] != p[j])//j未回到起点,并且长串里的s[i]和短串里的p[j]匹配失败
{
j = ne[j-1];//把j移到next[j-1]的位置。
}
if (s[i] == p[j ]) j++;
if (j == n)//j走到子串的头了
{
//匹配成功
printf("%d ", i - n+1);
}
}
return 0;//时间复杂度为O(n)
}
ne数组就是next数组,存储的是j指针移动的下一个位置。
以下是函数详解:
//求next数组的过程
void GetNext(vector<int>& next, const string& p)
{
next[0] = 0;//next数组初始化
for (int i = 1, j = 0; i < p.size(); ++i)//i是表示后缀尾,j表示前缀尾以及i之前最长相等前后缀长度
{
while (j && p[i] != p[j])//当i和j不匹配时,j向前跳转,当j已经跳转到开头时,不再跳转,防止死循环
{
j = next[j - 1];
}
if (p[i] == p[j]) j++;
next[i] = j;//下标为i的字符之前的最长相等前后缀的长度
}
}
//KMP算法实现过程
int KMP(const string& s, const string& p)
{
vector<int> next(p.size(), 0);//初始化next数组
GetNext(next, p);
for (int i = 0, j = 0; i < s.size(); ++i)
{
while (j && s[i] != p[j])
{
j = next[j - 1];
}
if (s[i] == p[j]) j++;
if (j == p.size()) return i - p.size() + 1;//下标从1开始的话,return 后面加一,GetNext里的next数组里的for循环i初始化为2
}
return -1;
}
这里是两种情况,第一种是下标从1开始,第二种是下标从0开始。所以写法上会有所不同。i指向主串的当前位置,j指向子串的当前位置。
这里对应leetcode第28题,实现strStr():
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
代码如下:
void GetNext(vector<int>& next,const string& p)
{
next[0]=0;
for(int i=1,j=0;i<p.size();++i)
{
while(j&&p[i]!=p[j]) j=next[j-1];
if(p[i]==p[j]) j++;
next[i]=j;
}
}
class Solution {
public:
int strStr(string haystack, string needle) {
if(needle.size()==0) return 0;//重点,needle可能为空。
vector<int> next(needle.size(),0);
GetNext(next,needle);
for(int i=0,j=0;i<haystack.size();++i)
{
while(j&&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;
}
};
比上面的代码多了一步判断,就是说needle字符串有可能为空,此时要多考虑一种情况。
多做算法题可以让我们的思维更缜密,更灵活。正所谓读万卷书,行万里路。我们不能改变过去,但是我们可以把握当下,展望未来。