KMP可实现代码及解析

提示:君子学以聚之,问以辨之,宽以居之,仁以行之

考研中会简单模拟KMP过程,并且可求出KMP的next数组即可

【KMP算法之求next数组代码讲解-哔哩哔哩】 https://b23.tv/lRr3VCx 建议先看视频 这个视频前两年看的 看完kmp的核心你就懂了 再次重申一定要看视频


前言

提示:这里可以添加本文要记录的大概内容:
简单来说 暴力算法的不好之处在于每一次失配之后都只是向右移动一个位置 改进的kmp 就是每次失配之后 ,移动很多的位置,跳过那些不可能匹配成功的位置


提示:以下是本篇文章正文内容,下面案例可供参考

一、KMP之求next数组代码讲解(按照视频所写)

一定看完视频 坚持看完就会理解next 再来看我胡
都知道kmp主要就是nxet数组的求解 但是难的却也就是next 大家不知道怎么求
首先我们先把代码写下来

1.1原代码

int GetNext(char ch[],int length,int next[]){//length为串ch的长度
	next[1]=0;
	int i=1,j=0;
	while(i<=length){
		if(j==0||ch[i]==ch[j])next[++i]=++j);
		else j=next[j];
	} 
} 

这里再强调一个点,有的书是规定next[1] = -1,那么我们只需要让next数组中所有的值都减去1即可 其实-1与初始化0 就是相当于与你是找坐标 还是找位置
next[0]没有实际意义,模式串的第一个字符从next[1]进行存储,当然next[1]也可以存放数组的长度,作为一种标记 下文会说前缀用j表示,后缀用i表示。

1.2 几个概念

  1. 前缀:包含首个字符但不包括末位字符的字串
  2. 后缀:包括末尾字符但是不包括首位字符的字串
  3. next数组定义:当主串与模式串的某一位字符不匹配时 模式串要回退的位置 也就是若是失配了此时从哪个位置开始重新匹配
  4. next[j]:其值=第j个字符前面j-1个字符组成的字串的前后缀重复字符的个数+1;

是不是依然不知道它的用处?没关系!!!看一眼 心中有个印象 继续往下看

1.3 手算Next 数组

这里我们使用word写 我自己看的也舒服一些 这里看不懂为什么这样求也没有关系 看完可能你在回头来看 你就恍然大明白 了
请添加图片描述

求解之前建议再看一遍上面四个概念
当 j=1时:规定next[1]=0 初始化为0
当 j=2时:j前字串为’a’ next[2]=1
当 j=3时:j前字串为‘ab’因为其前字串为‘ab’所以next[3]=1
当 j=4 时:j的前字串为‘aba’前后缀有一个重合所以next[4]=2
当 j=5 时:j的前字符串为‘abaa’前后缀有一个字符重合,所以next[5]=2
当 j=6 时:j 的前字符串为‘abaab’ 最长重复前后缀为ab 所以next[6]=3;
当 j=7时:j的前字符串为‘abaabc’没有重复 所以next[7]=1
当 j=8时:j的前字符串为’abaabca’最长前后缀为a 所以next[8]=2

请添加图片描述
通过计算 我们发现了规律如下
1、next[j]的值每一次最多加1:因为过去一位最多增加一个字符 所以说你重合的字符个数最多也是加一
2:模式串最后一位字符不影响next数组的结果:因为我们要比较的是最后一个字符的前面的字符串 所以无论他是什么都没有它的参与
此时再来看一下代码 我们应该可以理解if 中条件的一部分了 若是不能理解也没有事情 、我们继续往下看

1.4、公式推理

其中有两个重要的推理就是
1、next[j+1]的最大值为next[j]+1
2、next[2] 恒等于 1
3、若是P(k1) != P(j) 那么next[j+1]可能的最大值为next[next[j]]+1 由此类推可以高效求出next[j+1]
这三个推理很重要
第一个我们应该是可以理解的 但是第二个表示的含义又是什么:这里我先来解读一波 就像上面的图中 我们来看J等于6的时候 其中若是在J等于6的时候 此时我们注意到紧邻着它的ab 与开头的ab是一致的下标是3 所以 我们是不是有理由相信这个下标表示的是告诉机器,这个串中下标之前的字符串是一样的 此时不用比较了 直接从第三号位置开始就行了
(看不懂解读也没有关系 下面有图 有助于理解)

1.5、图解KMP

请添加图片描述
若是此时然我们来求next[10]的值 则我们必然是知道next[9]的值 我们这里假设next[9]=4 假设什么值都可以但是 这里不能超过7 ,由next计算方法可知最长前后缀是3 则可以的到下图
请添加图片描述

理想情况下:若是J=4 与J=9 字符是一样的 则此时的next[10]应该为5
若是不相等呢:我们再假设next[4]=3 则有下图
请添加图片描述
前面我们知道1-3 与6-7是相等的则可以得到下图
请添加图片描述
所以是不是也就意味着这四个部分是一样的 这样我们就知道就上图而言 第一部分与第四部分是一样的 则此时我们只需要比较J=3 时与J=9字符是否一致 若是一致则可得到最长前后缀是3 则可知道next[10]=4 若是不一样则继续分成更小的部分 这里我们假设next[3]=2 则表示 第一个字符与第二个字符是相等的 也说明第一个字符与最后一个字符是相等的 则我们来比较J=2与J=9 的字符是否一致 若是一致,next[10]则等于3 若是依然不相等 由上文中写到的规律知道,next[2]=1 然后比较J=1与J=9若是相等的话 则next[10]=2 若是依然不等,由next的初始化可知 此时将递推结束 则next[10]=1 含义也就是前后缀即使一个字符也没有相等

1.6、回顾代码

代码如下(示例)

int GetNext(char ch[],int length,int next[]){//length为串ch的长度
	next[1]=0;//作为一种标记
	int i=1,j=0;//j表示前缀的后一个字符 i表示后缀的后一个字符
	while(i<Length){
		if(j==0||ch[i]==ch[j])next[++i]=++j);
		else j=next[j];
	} 
} 

**其实i和j就是两个指针 j指向的是第一部分(前缀)的后一个字符 , i 指向的最后一部分(后缀)的后一个字符 若是相等则为前一个next[]值加一 ,若是不相等 则将此时前缀细分为前缀和后缀, J 就跳到此时新分的前缀中后一个字符 继续比较 ,直到第一部分(前缀)的后一个字符与最后一部分(后缀)的后一个字符相等 若是一直不相等 最终j将会来到零号位置 就说明此时串中最前面部分(前缀)与最后面部分(后缀) 没有一个相同哪怕是一个字符,此时i指针也要往后移动 j 指针也要移动,所以给此时的next中数组值赋值为0 等你使用该next 与文本串匹配的时候 遇到等于0 的也是两个指针都向后移动重新匹配; **
注意这里的next数组 表示的含义是位置, 若是发生失配 ,此时无论是文本串还是模式串都最好不要直接从头开始重新匹配,而是根据已经匹配的部分来进行判断,此时模式串中前面醉经要跳到哪里,若是都不行,那就i从下一个位置重新开始对比
在这里插入图片描述
next[j]的值(也就是k)表示,当P[j] != T[i]时,j指针的下一步移动位置。
先来看第一个:当j为0时,如果这时候不匹配,怎么办?
像上图这种情况,j已经在最左边了,不可能再移动了(不能分出更小的前缀了),也就是意味着第一个字符就不同,这时候i指针也要向后移动了(本来再j向前跳的时候i是不动的 此时跳不了)。所以在代码中才会有next[1] = 0, 也就是一个标记的作用 ; 是为了配合if 中的这个条件 j==0; 你可能会问能不能标记成其他值 我想其实也可以但是这样的话 ,你需要对next[2]进行赋值
注意以上是以坐标的形式解析的 下面写是使用的下标 因为这样对字符串好操作一些 方便理解

二、KMP 代码解析

由上面的推导我们可以知道 其实next的构成是与文本串是无关的,上面暴力的时间复杂度之所以为M*N 原因是 模式串发生失配的时候 文本串也是从头开始匹配,模式串也是从头开始, 是通过模式串来构建next数组 next数组的作用是 模式串若是失配不必每一次都继续从第一个字符开始从匹配 文本串也不必回溯 ,可以看出匹配的过程是O(m),之前还要单独生成next数组,时间复杂度是O(n)。所以整个KMP算法的时间复杂度是O(n+m)的。
基于上面的思想 若是模板串通过与文本串的对比 模式串的前面已经有部分已经与文本串匹配了,则我们文本串的指向不必改变 只需要移动模式串即可,但是移动多少呢 ,当然是最长相等前后缀,所以也就有的说next里面存放的是移动的长度 有的说存放的是最长前后缀

可执行代码;

这里解析就放在代码中了 因为拿出来反而失去了连贯性

版本一(next[1]=0)

#include<bits/stdc++.h>
using namespace std;
typedef struct String{
	int length;
	char* ch;
}String;
void InitString(String &T){
	T.length=0;
	T.ch=NULL;
} 
void get_next(String T, int next[]){
	next[1] = 0;//同样的也是设置一个标识位  至于为什么是0因为0+1等于1匹配的时候就知道两个都要向后移动 
	int i = 1;//此时我们看的是next[2]也就是第二个字符  此时i应该指向第一个字符 
	int j = 0; //j的指向要比i小 此时只能是1  还有一种方式来解释 只有让j等于0  next[2] 才能等于1 next[2]=1 这是固定的 
	while (i < T.length){
		if (j == 0 ||T.ch[i-1] == T.ch[j-1]){
			++ i; ++ j;
			next[i] = j;
		}
		else 
			j = next[j];
	}
}
//其实这也可以理解为两个一样的字符串 来获取next的过程 
void InGet(String &T){
	int i=0;
	cout<<"请输入一个字符串"<<endl;
	T.ch=(char*)malloc(sizeof(char));
	T.length=0;
	scanf("%s",T.ch); 
	while(T.ch[i]) {
		i++;
		++T.length;
	}
}
int Index_KMP (String S, String T, int next[]){    //S为长串,T为短串
	int i = 1, j = 1;
	while (i <= S.length && j <= T.length){//因为表示的是位置 所以认为需要比对到= 
		if (j == 0 || S.ch[i-1] == T.ch[j-1]){
			++ i; ++ j;
		}
		else
			j = next[j];
	}
	if (j >T.length)//因为是位置 这里需要> 单纯的等于可能不行 
		return i - T.length;
	else 
		return 0;
}
int main(){
	int choice; 
	while(1){
	cout<<"请输入你的选择 1继续输入"<<endl;
	if(0==choice)  break; 
	String S,T;
	InitString(S);InitString(T);
	InGet(S);
	InGet(T); 
	int next[T.length+1];
	get_next(T,next);
	for(int i=1;i<T.length+1;i++){//输出naxt 要从1 开始 
		cout<<next[i]<<" ";
	}
	cout<<"此时的位置是"<<Index_KMP(S,T,next);
}
}

版本二(next[0]=-1)

#include<bits/stdc++.h>
using namespace std;
typedef struct String{
	int length;
	char* ch;
}String;
void InitString(String &T){
	T.length=0;
	T.ch=NULL;
} 
//其实这也可以理解为两个一样的字符串 来获取next的过程
void get_next(String T, int next[]){
	next[0] = -1; //上面解析中写的是位置 这里改成坐标的形式  这里赋值为零 是作为一个标记 若是此时j 等于零 表示的是此时指向的是i与第零个进行比较
	//但是并不存在第零个字符  也就是来到这个标记之后  模式串的后缀与文本串的前缀没有一个字符是相等的  所以此时都要往后移动一个单位,顺便也来说
	//一下j=1的则表示 此时应该比较i 位置与j所指向的第一个位置 进行比较 此时并不能确定是否字符相等  若是不相等 则j下一个值就是j=0  
	int i = 0;//i的指向往往都是待求字符的前一个字符  此时要求的是next[1]  所以 所以i应该指向零 
	int j = -1; //而j呢,往往指向的是前缀的后一个字符 ,用来跟i指向的位置进行对比,但是并不能超过或者等于i,此时只有一个字符所以没有前缀,也没有后缀,所以此时只能为-1 当然也可以另一种来看就是只有j=-1 配合if 中的条件语句 才能使得next[1]=0;
	while (i <T.length){  
		if (j == -1||T.ch[i] == T.ch[j]){
			++ i; ++ j;
			next[i] = j;
		}
		else 
			j = next[j];
	}
} 
void InGet(String &T){
	int i=0;
	cout<<"请输入一个字符串"<<endl;
	T.ch=(char*)malloc(sizeof(char));
	T.length=0;
	scanf("%s",T.ch); 
	while(T.ch[i]) {
		i++;
		++T.length;
	}
}
int Index_KMP (String S, String T, int next[]){    //S为长串,T为短串
	int i =0;//i指向的是文本中的第一个位置 
	int j =0;//模式串的第一个位置 
	while (i <S.length && j <T.length){
		if (j == -1 || S.ch[i] == T.ch[j]){//若是模式串中的j 一直向前寻找前缀的过程中 一直未 找到 直到此时j==-1  两个也都向后移动一个位置 
			i+=1; j+=1;
		}
		else
			j = next[j];
	}
	if (j==T.length)//说明前面全部匹配成功了 
		return i-T.length;
	else 
		return 0;
}
int main(){
	int choice; 
	while(1){
	cout<<"请输入你的选择 是否继续输入字符  是的话选1"<<endl;
	cin>>choice;
	if(0==choice) break;
	String S,T;
	InitString(S);InitString(T);
	InGet(S);
	InGet(T); 
	int next[T.length+1];
	get_next(T,next);
	for(int i=0;i<T.length+1;i++){
		cout<<next[i]<<" ";
	}
	cout<<"此时的位置是"<<Index_KMP(S,T,next)<<endl;
}
}

其实也没有什么改动也就是下标对比的时候进行了一点改动 以及输出位置的改动 有兴趣的可以自己写一遍 有的时候光看不代表就会

三、关于nextval的计算

下面从代码的角度讲述一下求解nextval[]的思路:
利用next数组与MaxL求nextval数组 我们这里的方法适用于next 是以0开始的 若是你要求-1,那么按照这个方法求出0 的到时候整体减一就可以了 你要明白-1(下标) 0(位置) 其实就还好 这里我们只提供一种方式 多了 你反而易错

  1. 我们先引入一个数组也就最大前后缀数组MaxL,不要告诉我你不知道怎么求最大前后缀 此时求最大前后缀是包括当前字母的
  2. 求next 数组 这个很快的 与MaxL是斜对角线加一 所以几乎也不费时间
  3. 首个为0,从左至右,比较MaxL与next。
  4. 若不同,填入next的值,例如,对于序号2,MaxL[2]=0,next[2]=1,则nextval[2]=1。
  5. 若相同,填入下标为该值的nextval,例如,MaxL[3]=next[3]=1。则nextvaL[3]填入下标为1的nextval,即nextval[3]=nextval[1]=0;再例如,next[3]=MaxL[3]=1 则此时填入的是nextval[3]=nextval[1]也就是1
  6. 有一点要注意的就是 nextval 与next的起始是一样的
下标123456789101112
P:ababaaababaa
MaxL001231123456
next[]:011234223456
nextval:010104210104

总结

若是文章对你的提升由哪怕一点帮助的话 请答应我 不要吝啬你的点赞评论 你的鼓励对作者是一种莫大的鼓励

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值