首页 > 编程语言 >KMP算法

KMP算法

时间:2023-09-26 22:34:32浏览次数:35  
标签:字符 匹配 回溯 前面 位置 回溯到 算法 KMP

KMP算法可以看做是对暴力求解的一种改进,在前面的暴力算法中,i指针和j指针都是要回溯的,这是不合理的,因为当发现不匹配的时候,已经扫描到的区域我们其实是已知的,如下图所示
1695758258.jpg
当我们发现不匹配后,我们其实已经知道了主串的第1到第5个字符是什么,其实就是模式串前面的字符,KMP算法就是将这些信息利用起来从而将时间复杂度优化到O(n)
既然前面的信息我们已经有了,我们其实完全不用回溯i指针,只需要回溯j指针,下面我们来看如何利用起来。
就那上面的例子来说,当第六个元素不匹配后,主串的15和模式串的15其实是一样的,读者可以看一下,我是否可以直接将j回溯到3号元素的位置而不回溯i,显然是可以的,因为i前面的两个字符是a和b,而3号位置的两个元素也是a和b,相当于少匹配了两个字符,只回溯j,不回溯i,这就是KMP算法的核心思想。
下面我们来看一下为什么我可以把j移动到3号位置,而不动i,为什么不是其他的位置呢?
可以对比上图,首先我们知道下一个要匹配的字符是i,我们假设我们要回溯到模式串第k个位置,那么这个k必须具备这样一个性质,就是k前面的所有字符必须和i前面的字符匹配上,我们可以看到上图,主串第6号位置前面的字符为abaab,如果j回溯到模式串3号位置,模式串3号前面的a和b正好可以和主串6号位置前面的ab匹配上,此时就可以继续往下匹配,但如果j回溯到4号位置,那模式串4号位置前面的aba就无法和主串6号位置前面的aab匹配上,所以这就不可行。
如果能理解上面加粗的话,那么我们就可以再深入一步,当匹配到6号位置的时候,实际上我们直到主串15的位置和模式串15的位置一定是一样的,所以,i前面末端的所有字符,就等于j前面的末端的所有字符,所以,我也可以这么说,我们假设匹配失败要回溯到模式串的第k个字符,那么k前面的所有字符必须和j前面的字符匹配上,因为j前面的字符和i前面的字符是一样的,我希望读者可以深入理解一下这句话,如果理解透彻了,就会发现,这个k的取值现在和主串就没关系了,发现了吗?只要k前面的所有字符能够和i前面的字符匹配上就可以了,所以能决定k取值的只有模式串本身和j目前的取值。
1695759227.jpg
既然回溯位置只和模式串本身和匹配失败的位置有关系,我们就针对这个模式串来分析一下j处于不同的情况下需要回溯的情况,从后往前分析
如果匹配到第六个字符失败了,假设我们要回溯到第k个位置,那么k位置前面的所有字符应该和第六个字符前面的字符匹配上,很显然应该指向3的位置,因为3号位置前面的所有字符为ab,而6号位置前面的字符也为ab,可以匹配上。
如果匹配到第五个字符失败了,假设我们要回溯到第k个位置,那么k位置前面的所有字符应该和第五个字符前面的字符匹配上,可以观察出应该指向2号位置,因为2号位置前面的所有字符为a,5号字符前面的字符也是a,如果指向其他地方都是匹配不上的,比如指向4,4前面的字符为aba,而5前面的字符为aab,很显然匹配不上
如果匹配到第四个字符失败了,按照前面的规则,我们也可以看出,如果我们回溯到2号位置,那么2号位置前面的a和4号位置前面的a匹配上了,所以应该回溯到2号位置
如果匹配到3号位置失效,此时不论k回溯到1还是2,都无法满足规则,如下图所示
1695759637.jpg
可以观察到,这种情况下,下一个要匹配的字符其实是模式串的1号位置,所以当第三个位置匹配失败的时候,我们应该回溯到模式串的1号位置
如果第二号位置匹配失败呢,如下图所示
1695759799.jpg
其实这种情况很简单,j是回溯,又不能往后跑,所以j只能回溯到1号位置,所以其实对于任意的模式字符串,只要是第二个元素匹配失败,他都只能回溯到第一个元素上去。
情况特别一点的是第一个位置匹配失败,如下图所示
1695759891.jpg
这个时候由于第一个元素直接就匹配失败了,我们应该继续匹配下一个元素,应该让i+1,j保持不动,这样固然没有问题,但是我们发现,我们每次进行匹配都会让i的值和j的值同时加一,我们可以利用这个同时+1的算法,让j的值回溯到0,然后j和i同时+1即可,这样做虽然对代码逻辑没有任何影响,但可以减少代码量,也算是一种优化。
上面我们分清楚了j处于每一个位置时匹配失败的回溯位置,如下所示

  • 第六个元素匹配失败,i=3
  • 第五个元素匹配失败,i=2
  • 第四个元素匹配失败,i=2
  • 第三个元素匹配失败,i=1
  • 第二个元素匹配失败,i=1
  • 第一个元素匹配失败,i=0(i和j都要加一)

我们可以将这个信息列成一张表
1695760752.jpg
我们称这张表为next表,next[j]表示第j个元素匹配失败的时候j指针应该回溯的位置
有了这张表之后,我们就可以尝试来写出KMP算法了

int KMP(myString *s,myString *t,int *next){//s为主串,t为模式串,还需要把next数组传入
    //初始化1和j
    int i = 1;
    int j = 1;
    while(i < s.length && j < j.length){
    	if(s[i] == j[i] || j == 0){
            //如果能匹配上,匹配下一个元素,如果j为0,也让他们都加一
            i++;
            j++;
        }else{
            //如果匹配不上,进行回溯,这里只回溯j
            j = next[j];
        }
    }
    //出循环的时候,说明i和j至少有一个越界
    if(j > t.length){//如果j比t.length大,说明j被完完整整匹配了一遍,此时j的值为t.length+1
        return i-t.length;//返回值应该是开始匹配的第一个元素位置,应该是i-j+1,化简后为i-t.length
    }else{
        return -1;
    }
    
}

如果理解了KMP算法的核心逻辑,那么这个算法理解起来就没有什么难度,唯一的难点在于第六行代码,当j==0的时候,我们同样要让i和j相加,因为当j==0的时候,表示上一次匹配的第一个元素匹配就失败了,所以需要将j回溯到0,让i和j都加1,这是前面提过的。
KMP算法由于i指针不回溯,最坏时间复杂度只会到O(n),以上就介绍完了KMP算法。

综合来看KMP算法的最坏时间复杂度其实是O(m+n),这是因为求next数组其实是需要一个O(m)时间复杂度的算法

下面讲一下手算next数组的技巧,next[j]其实就是第j个字符匹配不上的时候应该回溯到的位置,而回溯到的那个位置前面所有的字符,应该和第j个位置前面的字符匹配上,这是前面讲过的。
在讲技巧之前,先来了解一下前后缀的概念,字符串的前缀,就是字符前面的n个连续字符字符,例如"abcd"的前缀一共有"a","ab","abc","abcd"四个,而后缀正好相反,就是尾部n个连续字符,这个字符串的后缀有"d","cd","bcd","abcd"四个。
我们计算next[j],其实就是计算前面的第1到第j-1这个字符串除自身以外的最长公共前后缀的个数+1
我们举个例子来理解,例如"google"这个字符串
如果第四个位置匹配失败,前面三个字符就是goo,他的前缀有g,go,goo三个,后缀有o,oo,goo三个,我们抛开goo自身,前缀就只有g,go,后缀就只有o,oo,这显然没有公共前后缀,公共前后缀就是0,所以0+1就是1,所以next[4] =0+1=1。
同理如果我们要求next[5],也就是第5个字符匹配失败应该回溯的位置,前4个字符除去自身以外的前缀有g,go,goo,后缀有g,og,oog,这个时候发现前缀的g和后缀的g相等,没有其他的前后缀相等,他的长度为1,所以next[5] = 1+1=2
对于第二个元素匹配失败的情况,由于其前面只有一个字符,公共前后缀长度必为0,所以一定回溯到1。
而对于第一个元素匹配失败的情况,一定是回溯到0这个位置,至于为什么我们前面提到过,如果忘记了可以翻看前面的算法。

考研并不要求掌握求next数组的计算机算法,只要求可以手算即可

标签:字符,匹配,回溯,前面,位置,回溯到,算法,KMP
From: https://www.cnblogs.com/zhangyunling/p/17731430.html

相关文章

  • 手写数字数据集AutoEncoder降噪算法
    对训练数据加噪声的方法,在训练里面对x做如下处理,添加椒盐噪声:bs,ch,h,w=x.shapex=x.reshape(bs,ch,h*w)+0.2*np.random.normal(size=28*28)x=x.to(torch.float32)数据集里面的标签label无用,因为AutoEncoder去噪是无监督方法。......
  • 2023年最新京东app端sign签名算法与cipher加解密逆向分析(2023-09-26)
    前言:  本文仅供学习交流,只提供关键思路不会给出完整代码,严禁用于非法用途,若有侵权请联系我删除!技术交流合作请私信!一.工具的选择(抓包工具的选择,是门学问)用到工具如下:1、安卓手机一台,系统版本:android6.01;型号:小米MI4LTE  之所以要选择android6手机,原理如下:  ......
  • 文心一言 VS 讯飞星火 VS chatgpt (101)-- 算法导论9.3 7题
    七、用go语言,设计一个O(n)时间的算法,对于一个给定的包含n个互异元素的集合S和一个正整数k≤n,该算法能够确定S中最接近中位数的k个元素。文心一言:要设计一个O(n)时间的算法来找到集合S中最接近中位数的k个元素,我们可以使用快速选择算法(QuickSelect)。该算法基于快速排......
  • 文心一言 VS 讯飞星火 VS chatgpt (101)-- 算法导论9.3 7题
    七、用go语言,设计一个O(n)时间的算法,对于一个给定的包含n个互异元素的集合S和一个正整数k≤n,该算法能够确定S中最接近中位数的k个元素。文心一言:要设计一个O(n)时间的算法来找到集合S中最接近中位数的k个元素,我们可以使用快速选择算法(QuickSelect)。该算法基于快速排序的......
  • 本地测试Spark的svm算法
    上一篇介绍了逻辑回归算法,发现分类效果不好,通过这次的svm发现是因为训练数据不行,于是网上找了部分训练数据,发现实际上分类效果还可以。    训练数据,第一个值是标签,下面的数据是某种花的相关特征。1|5.1,3.5,1.4,0.21|4.9,3,1.4,0.21|4.7,3.2,1.3,0.21|4.6,3.1,1.5,0.21......
  • 较难理解的字符串查找算法KMP
    时间复杂度O(n)的子串查找算法。经典实例主字符串(s):abcabcabd模式串(t):abcabd比较次数   主字符串   模式串   备注一   abcabcabd   abcabd   红色和绿色表示正在比较的子串,红色表示不同部分,绿色表示相同部分。二   abcabcabd   abcabd  ......
  • 9.26算法
    /** * Definition for singly-linked list. * struct ListNode { *     int val; *     ListNode *next; *     ListNode() : val(0), next(nullptr) {} *     ListNode(int x) : val(x), next(nullptr) {} *     ListN......
  • 【算法】栈与队列
    1栈与队列理论基础队列先进先出,栈先进后出;不允许有遍历行为,不提供迭代器2用栈实现队列题目:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):实现 MyQueue 类:voidpush(intx) 将元素x推到队列的末尾intpop() 从队列......
  • 算法训练day20 LeetCode654
    算法训练day20LeetCode654.617.700.98654.最大二叉树题目654.最大二叉树-力扣(LeetCode)题解代码随想录(programmercarl.com)使用递归返回节点地址,输入父节点地址,数组终止条件是输入地数组为空单层操作:如果输入数组为空,则返回父节点地址否则找到数组中最大......
  • 基于图像形态学处理的目标几何形状检测算法matlab仿真
    1.算法运行效果图预览  2.算法运行软件版本matlab2022a 3.算法理论概述      目标几何形状检测是计算机视觉领域中的重要任务之一,旨在从图像中自动识别和定位不同的几何形状,例如矩形、圆形、三角形等。这些形状检测在许多领域中都具有广泛的应用,如工业自动化......