字符串匹配算法KMP详解

本人是普通本科院校的一名大二学生,为提高自身能力特地锻炼自己发表一些文章,题解等,
望各位大佬能够对我这个菜鸟所做多多指正,在此谢过各位啦~~~

文章的开始,我想向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字符串有可能为空,此时要多考虑一种情况。

多做算法题可以让我们的思维更缜密,更灵活。正所谓读万卷书,行万里路。我们不能改变过去,但是我们可以把握当下,展望未来。

  • 16
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值