KMP简陋理解——学习笔记

温馨提示:这篇文章只是我的学习笔记,看了一些相关视频和其他博主的文章,做出的总结,还有一些是我自己的一些理解,可能写得乱七八糟,还请见谅。深入的理解可能这篇文章是讲不到的,觉得不合适的同学请另寻解答,当然也欢迎您的浏览

KMP算法是朴素的匹配算法BF算法的升级版。
BF算法的时间复杂是O(n * m),KMP算法的时间复杂度是O(n+m)。
其改进在于:每当一趟匹配过程中出现字符比较不等时,不需要回溯i指针,而是利用已经得到的“部分匹配”的结果将模式串向右“滑动”尽可能远的一段距后,继续进行比较。课本原话
例子:
主串S = “ababcabcacbab”
模式串P = “abcac”

当模式串和主串在 i=3 这个地方匹配失败时,不再将其整体右移一个位置,重新从头开始匹配,而是根据模式串的前缀表(就是next数组)相应位置的值,将模式串 nxet[i] 右移到主串i指针的位置继续进行比较,简单来说就是:在比较时,实现最大的移动量。由此消除了主串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的位置开始比较即可
如果 p[i] == p[ next[i] ] , 则 next[i+1] = next[i] + 1 .

如果不相等,将 p [ next[next[i] ]拿出来和p[i]继续比较,如果相等,则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中第一次出现的位置
	}
}

猜你喜欢

转载自blog.csdn.net/buibuilili/article/details/107537573