KMP算法超级详解

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

我们为什么要KMP算法?

之前,我们用 暴力算法实现了 strstr 的函数,如果不记得或者没看过
看这里 : 处理字符串的函数
然后找到目录的寻找子串,就是暴力算法
abcabcdabcbcabca 里面找 bcda,怎么找?
我们一般的暴力算法实现是
如果我们要在
主串有一个 指针 a ,子串有一个指针 b
在这里插入图片描述
第一个 a 不匹配 a 向下移动,直到找到子串的开头,然后把 子串的头 用 p 储存起来
然后看子串的每一个字符是不是和 a 后面的字符相同
在这里插入图片描述
一旦发现不同,退回到主串 的 子串头的位置
子串重新回到头的位置
在这里插入图片描述
a在找到子串的头的位置
在这里插入图片描述
然后子串字符和主串字符一个个匹配 一直到匹配到 b >= lensub
在这里插入图片描述
结束匹配,成功嘞,返回 p(子串头相对主串 0 的偏移量)
失败了,最终返回 -1

这有一个弊端,就是主串和子串在匹配失败后,主串要退回到 开始和子串匹配的位置,子串也要退回到子串的开头
有没有一种方式,只要子串回退,但是主串不用回退,一直向前走,不回头的号方法呢?

有,就是KMP算法,这是由三位大佬Knuth,Morris和Pratt提出的,为了纪念,直接用三人名字首字母来命名这个算法。
妥妥大神级别,让我们站在大神的肩膀上看看这个 KMP算法

KMP 算法的思路

这个好难讲,我尽量写明白,看不懂评论留言我再改改
我们换个例子
abcabcdabcbcabca 里面找 bcbca
在这里插入图片描述
和之前一样,子串和主串相同 指针向下走
在这里插入图片描述
如果不一样的时候,我们主串不能退回,子串回退,怎么退呢?
这个问题很重要
在这里插入图片描述
我们看一下下图,指针b 之前的 bc 和 指针a之前的 bc是一样的
在这里插入图片描述

如果 a 退回去了,是不是要退到 p 的位置就能满足 开头和子串的开头相同
也就是 m 指向的位置

那 a 相当于子串的什么位置?
在这里插入图片描述
是不是相当于 b2的位置,那也就是说 b 退到 b2的位置是不是就能够满足 a继续走下去 也能和子串下一项匹配的条件了

然后我们的目的就是 如何让子串回退到 b2的位置了
这个时候 我们回退的位置就是要求解 next数组

next 数组的计算

肉眼看

这个就是重点的重点了,能看到这的都是大佬,谢谢支持,如果哪里讲的不好,看不懂的,请务必把我拎出来揍一顿

next数组记录的就是 b 要回退的位置
在这里插入图片描述
如果说 子串能走到 b 的位置,说明在主串,a前面的部分字符是和子串一样的,不然b也跑不到这么后面

还是假设 a 要退回到 p 的位置,那就意味着,p指向的开头是必须和 子串 m的开头是一样的

但是我不能跑到主串的位置,让主串告诉我,我这里和子串你一样,你子串退到那边,咱们就能继续了对吧

那我们就要子串里面知道退回的值
怎么实现?
在这里插入图片描述
我们可以发现,黄色框的字符串刚好是相同的

也就是说 从 m处的 b 向后数到 c 和 从 b 处前的 c向前数到 b 的两个字符串是一样的
字符串长度是 2 ,这个长度恰好就是 b 要退到的在子串的索引的位置,也就是 next 数

这个就是 next 数的计算,就是 要回退的索引值 的大小

如果我们假设要求 next 值的数叫做目标数
方法 1

  1. 以 子串头 为开头, 以b数前的字符为结尾,相同的字符串的长度

方法2
2. 从子串开头向后数 的同时 目标数前一个字符开始先前数 ,两个相同字符串的长度

举个例子
在这里插入图片描述
如果我们要求得 a 的next 数 怎么办?
我们从 c (a的前一个字符)先前数,数到b开头的字符串,同时 从开头的 b 向后数,数到 c 结尾的字符串,发现两个字符串一不一样?
一样!
这个字符串长度是多少?
2!
好,这个就是 a 的next数
那么子串中的每一个数,都能够得到一个 next值

做个小练习
我们求一下主串的 next数组

在这里插入图片描述
我们求一下这个数组的 next 数组吧
我们定义一下 0 位置 的 next 值是 -1,1位置的next 值 是 0
我举例子求一下 a和 b 的next 值
在这里插入图片描述
首先 我们从 目标数的前一个数c 向前数 找到子串开头 也就是 a 开头的第一个字符串
是 abc
然后 从子串开头 从 a 向后数 找到 以目标数前一个数 c 为结尾的字符串
还是 abc 长度是 3
所以 a 的next 值是 3

然后是 b
在这里插入图片描述
我们还是先前数 数到 和字符串开头一样的字符 a
那就是 abcbc
然后 从 字符串开头 数到和 目标数前一样的字符 c
在这里插入图片描述
得到 abcabc
abcbc != abcabc 完全不一样 next值是 0

我们把整个字符串的的next值全部求出来是这样的

在这里插入图片描述

计算机的求解

我们观察一下有没有什么数学关系
总不能说,让计算机和人一样,从字符串开头数,然后又从 目标数前先前数,又要移动好几十次,不是浪费时间么
我们看看这两个框
在这里插入图片描述
有什么规律?
他是递增的,我们得出一个结论,如果 next 数要增加只能递增
那不就好办了?
在这里插入图片描述
如果我们要求解 6 这个位置的 next 数
我们的数据来源 是不是上一个值? 那我们退一下
在这里插入图片描述
现在指向的字符的 next值是 2, 说明什么?
说明 从字符串头开始的长度为 2 的字符串 和 现在的目标数前©的字符串相同
也就是 ab 和 ab 相同

在这里插入图片描述
那如果 再分别从 两个字符串向下数
是不是就是满足 指针a 和 b 位置的数相同就可以了
a 的位置正好就是 b 的 next值
所以只要 sub[x-1] == sub[next[x-1]];
如果相同,那 x 的 next值就是 他的前一个数 的 next+1了

如果不相同怎么办 ?
在这里插入图片描述
如果 在索引 7 的时候 我们向前走一格到 b 处,字符是 b
对应 next 值是3,指向的是 a
a != b
那就要继续索引回去,a 的 next值是 0
那就回到 0 处 还是 a
a != b
还是继续回退,next就是 -1
啥都没有,可以看成什么都可以和 -1的字符相等
匿名 == b
最终的 next + 1 = -1 + 1 = 0
得到
在这里插入图片描述

C 语言代码

我们来实现一下
从 KMP开始看,看到 GetNext 函数

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//输入:一块空间和子字符串
//输出;无
//功能;将一块空间储存子串的 next数组
void GetNext(char* sub,int* next,int lensub)
{
	//初始化
	next[0] = -1;
	next[1] = 0;
	int i = 2;//目标数从第三个数开始,因为前两个有了
	int k = 0;//i 前一个数的 next 值
	
	while(i<lensub)
	{
		if(k == -1||sub[i-1] == sub[k])
		{
			next[i] = k+1;
			k++;
			i++;
		}
		else
		{
			k = next[k];//继续回退
		}
	}
	
}

//输入:主串,子串,在主串中开始查找的位置
//输出:子串在主串的偏移量
//功能 在一个字符串中找到子串的位置
int KMP(char* str,char* sub,int pos)
{
	//输入处理
	assert(str != NULL && sub != NULL);
	int lenstr = strlen(str);
	int lensub = strlen(sub);
	if(lensub == 0 || lenstr == 0)return -1;
	if(pos<0 || pos >= lenstr) return -1;
	
	//得到 next 数组
	
	int * next = (int *)malloc(lensub*sizeof(int));
	GetNext(sub,next,lensub);
	
	int i = 0;//str 上的偏移
	int j = 0;//sub 上的偏移
	while(i<lenstr && j<lensub)
	{
		if(j == -1||str[i] == sub[j])//回退是有可能回到-1的
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];//子串回退
		}
	}
	if(j>=lensub)
	{
		return i-j;
	}
	else
	{
		return -1;
	}
}
int main()
{
	printf("%d",KMP("abcabcdabcbcabca","bcbca",0));//打印子串在主串的偏移量 
}

总结

  1. 掌握求解 next的思路和为什么要求解 next的思路
  2. 具体 在计算机上如何求解 next 要用到每一个值的上一个 next值,不满足条件的继续回退
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值