KMP算法——小白讲解

更新 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中第一次出现的位置
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值