KMP算法学习

文章目录

  • 一、初步理解
  • 二、实战代码
    • 1,字符串数据结构
    • 2,朴素解法
    • 3,KMP解法
    • 4,求解next数组
      • next原理
      • 翻译成代码
    • 5,改进KMP算法
      • KMP缺点
      • 求解nextval数组
      • 翻译成代码
      • 最终代码

一、初步理解

「天勤公开课」KMP算法易懂版 讲得很好

KMP 算法:
从主串中快速找出想要的子串(模式串)。幼稚模式串匹配算法 相比会出现比较指针回溯,效率低。
KMP算法学习_第1张图片
特点
1,比较指针左边,上下两个子串匹配。
2,比较指针左边,模式串中有公共前后缀。
KMP算法学习_第2张图片
关键
移动模式串,使得公共前后缀里的前缀 直接移动到原来的后缀所在位置
(因为前后缀是匹配的,移动之前上下是匹配的,所以把前缀移动到后缀 上下也是匹配的。)
而且中间没有跳过 可能匹配的情况,现在只需要直接比较 指针所在位置即可。

实际上,分析时只需要考虑模式串。
KMP算法学习_第3张图片在这里插入图片描述

KMP算法学习_第4张图片
条件
1,如果模式串中有多对前后缀,我们要取最长的那对。
2,前后缀长度要小于比较指针左端的长度。
(如果取等于,则不需要移动了,没有意义。)

考虑一个新的模式串
这里最开始下标对应为1(方便)。
1,假设第1个就不匹配了,那么模式串前移一位,重新比较。翻译成不含有串的移动表述方式:让1号位的字符(A) 与主串下一个位置的字符(B) 比较。
KMP算法学习_第5张图片
2,假设第5位不匹配,由于前后缀长度为2(AB),那么第3号位(2+1,A)与主串当前位比较。
得出一个结论,那个数字为:最大公共前缀长度+1
KMP算法学习_第6张图片
除了第一句话,后面的话除了数字不同,其他都一样,故将第一句话标记为0.
将语句变成数字,则为下图:
KMP算法学习_第7张图片
这个数组表示当模式串不匹配时,下一步哪个位置该与主串当前位比较,故叫做next数组

二、实战代码

视频来源:天勤考研数据结构:KMP算法 我直接从p3开始看起。也讲的很好

1,字符串数据结构

//考研数据结构中好用的两种
//定长存储结构
typedef struct {
	char str[maxSize + 1]; //末尾要加'\0'作为标志
	int length; //为了考研答题方便
}Str;
//变长存储结构
typedef struct {
	char *ch;
	int length;
}Str;
//变长存储结构使用
Str S;
S.length = L;
S.ch = (char*)malloc((L + 1) * sizeof(char)); //分配存储空间
S.ch[length范围内的位置] = 某字符变量; //赋值
某字符变量 = S.ch[length范围内的位置]; //取值
free(S.ch); //释放,参数为首地址

串,这部分下标都是从1开始计数。

2,朴素解法

#include "pch.h"
#include 
#include   
#include
#include
using namespace std;


//变长存储结构
typedef struct {
	char *ch;
	int length;
}Str;

//朴素版查找主串所包含子串的位置
int navie(Str str, Str substr) {
	int i = 1, j = 1, k = i; //下标从1开始。i为主串指针位置,j为子串,k记录主串和子串第一个开始比较的位置
	while (i <= str.length && j <= substr.length) {
		if (str.ch[i] == substr.ch[j]) {//如果相同,两者指针同时前移
			i++;
			j++;
		}
		else {
			j = 1; //否则,j重新回到开始
			i = ++k; //k需要前移一位,主串从i这个位置重新比较。可自行模拟一遍
		}
	}
	if (j > substr.length) //说明子串已经比较完成,有完全匹配的。
		return k;
	else
		return 0;
}

int main()
{	
	Str str, substr; //abc  bc
	str.length = 3;
	str.ch = (char*)malloc((str.length + 2) * sizeof(char)); //因为第0位我们这里没用到
	str.ch[0] = ' ';
	str.ch[1] = 'a';
	str.ch[2] = 'b';
	str.ch[3] = 'c';
	str.ch[4] = '\0';

	substr.length = 2;
	substr.ch = (char*)malloc((substr.length + 2) * sizeof(char));
	substr.ch[0] = ' ';
	substr.ch[1] = 'b';
	substr.ch[2] = 'c';
	substr.ch[3] = '\0';

	int res = navie(str, substr);
	cout << res;
	return 0;
}

3,KMP解法

https://www.bilibili.com/video/av76933202?p=3
由naive算法改装而来:

//KMP版查找主串所包含子串的位置
int KMP(Str str, Str substr, int next[]) { //多加一个next数组 参数
	int i = 1, j = 1; //下标从1开始。i为主串指针位置,j为子串。因为i不需要回溯,所以k不再需要
	while (i <= str.length && j <= substr.length) {
		//如果相同,两者指针同时前移
		if (j==0 || str.ch[i] == substr.ch[j]) { //if条件需要修正,因为j=0时有特例情况
			//j=0为特殊标记,代表i跳过当前位置前移1位,且把j设置为1。正好或上j==0后,下面代码达到效果。
			i++;
			j++;
		}
		else {
			//i不需要回溯,j由next数组提供
			j = next[j];
		}
	}
	if (j > substr.length) //说明子串已经比较完成,有完全匹配的。
		return i - substr.length; //因为没有了k,需要这样计算出来
	else
		return 0;
}

4,求解next数组

直接模拟手工方法,时间复杂度较大,重复操作。
https://www.bilibili.com/video/av76933202?p=4

next原理

针对模式串(子串)求next数组。复制一份,如下。
我们假设Pj这个位置不匹配。下面一行P1到Pt-1是t-1个,当作前缀FL;上面一行Pj-t+1到Pj-1也是t-1个,当作后缀FR。
KMP算法学习_第8张图片
挪动位置,假设红色部分完全相同,即FL=FR,也就是Pj前面的前缀和后缀相同,长度为t-1,那么有 next[j] = 前缀长度+1 = (t-1)+1 = t ,我们想推导一下next[j+1]的值
KMP算法学习_第9张图片
(1)如果Pj=Pt,则相当于FL和FR增加了一个长度,那么next[j+1] = next[j]+1 = t+1
(2)当Pj不等于Pt时,有点像不匹配时的主串和模式串,为了区分,叫假主串,如下图。
KMP算法学习_第10张图片
Sk状态中,1~4匹配,5位置不匹配。我们想办法往Sk+1跳,就是为了解决5位置的不匹配。
如何跳:在有前面next数组的值(已经求得)时,查对应next的值就可以跳了。(这里可以手动模拟验证,FL和FR的内容为“AB”,故next[5]为2+1=3,只需要将模式串跳到第3位进行比较即可)

同样,假模式串也类似操作。
将假模式串往前跳,翻译成t重新指向新的值比较好,也就是通过next数组的值重新赋值给t。这个过程t 可能需要多次调整,即t可能被多次赋值(跳多次),直到Sk处的不匹配被解决,也就是Pj != Pt被解决。
Pj != Pt被解决了,就回到了第(1)种情况。也就是1~被调整后的t子串( P1 ~ P新t)和上面一行匹配。此时调用(1),next[j+1] = 调整后的t + 1
(这个过程j是不动的)
当然,t在被赋值时,可能出现0的情况。就是假模式串不管怎么移动,都找不到FL=FR,可以看作FL=FR=0。可以合并这种情况,next[j+1] = 0+1 = 1 ,也就是主串第j+1个位置直接和第1个位置(最开始的位置)的模式串比较。

故,总结如下:
KMP算法学习_第11张图片

翻译成代码

//求解next数组
void getNext(Str substr, int next[]) { //直接修改next数组,不需要返回值
	//下标从1开始

	//j指向假主串,t为图中假模式串左端的长度。但t在求解过程中,t可能被next数组赋值为0
	int j = 1, t = 0; //t的初值为0,直接满足if的条件,执行next[2]=1
	next[1] = 0; //最开始next值设为0,作为标记
	while (j < substr.length) { //j要小于等于长度,但如果取=也进入,则next[j+1]越界,故不能取=
		if (t==0 || substr.ch[j] == substr.ch[t]) { //当t=0时,下面代码刚好满足next[j+1]=1
			next[j + 1] = t + 1; 
			++t;
			++j;
		}
		else
		{
			t = next[t]; //t=0时,next[j+1]=1
		}
	}
}

5,改进KMP算法

KMP缺点

举个特殊的例子,可以看出KMP算法的缺点。
第5个位置与主串发生不匹配。
KMP算法学习_第12张图片
直到j=0,执行++i; ++j;

1到4位置上的字符相同,因此next[5] 直接赋值为0即可。
对KMP的改进主要体现在next数组的改进上,改进后的数组称之为nextval数组,减少不必要的比较。

求解nextval数组

KMP算法学习_第13张图片
(1)如果Pj!=Pd,那么nextval[j] = d;如果Pj=Pd=Pc=Pb!=Pa,则nextval[j] = a
(2)如果Pk!=Pj,那么nextval[k] = j (=next[k]);如果Pk=Pj,那么nextval[k] = nextval[j] = nextval[next[k]] (相当于转换成只有k的参数,这里j=next[k])

将(2)转换一下:
1> Pk!=Pj,j=next[k],=> Pk!=Pnext[k] => Pj!=Pnext[j] (参数符号只是一个标记,换什么都可以)
nextval[k]=next[k] => nextval[j]=next[j]
2> Pk=Pj,j=next[k], => Pk=Pnext[k] => Pj=Pnext[j]
nextval[k] = nextval[next[k]] => nextval[j] = nextval[next[j]]

结论:
KMP算法学习_第14张图片

翻译成代码

//求解nextval数组
void getNextval(Str substr,int nextval[], int next[]) { //加上nextval参数
	//下标从1开始

	int j = 1, t = 0; 
	next[1] = 0; //最开始next值设为0,作为标记
	nextval[1] = 0; //很明显,同上
	while (j < substr.length) { 
		if (t == 0 || substr.ch[j] == substr.ch[t]) { 
			next[j + 1] = t + 1;  //可以根据条语句,将下面的进行化简。没有必要留下next数组了
			//根据前面分析求得
			if (substr.ch[j + 1] != substr.ch[next[j + 1]]) //不等时
				nextval[j + 1] = next[j + 1];
			else //相等时
				nextval[j + 1] = nextval[next[j + 1]];
			++t;
			++j;
		}
		else
		{
			t = nextval[t]; //t不会在j之后,所以nextval[t]的值早就求得了。故可以用nextval数组代替next数组
		}
	}
}

最终代码

//求解nextval数组-简洁版
void getNextval(Str substr, int nextval[]) {
	//下标从1开始

	int j = 1, t = 0;
	nextval[1] = 0; 
	while (j < substr.length) {
		if (t == 0 || substr.ch[j] == substr.ch[t]) {
			//根据前面分析求得
			if (substr.ch[j + 1] != substr.ch[t + 1]) //不等时
				nextval[j + 1] = t + 1;
			else //相等时
				nextval[j + 1] = nextval[t + 1];
			++t;
			++j;
		}
		else
		{
			t = nextval[t]; //t不会在j之后,所以nextval[t]的值早就求得了。故可以用nextval数组代替next数组
		}
	}
}

(终于完了,有点绕)

你可能感兴趣的:(算法)