梦回KMP算法

今天偶然间刷到一题,在目标字符串中查找模式串的起始位置,第一想法就是公共库带的indexOf这类的API,如果是自己实现的话就是经典的KMP算法,但算法实现已经完全模糊了,估计一开始也没掌握,难得再遇,所以打算记录下我关于KMP算法的理解

KMP算法是一个经典的字符串查找算法,全称是Knuth-Morris-Pratt,取自三个发表者的名字

算法背景

假设当前有一个T目标字符串和一个P模板字符串,我们希望在T中查找到P这个模板串的位置

算法思想理解

暴力求解的话就是直接二重循环进行查找

  • 假设遍历T串的下标为ti
  • 遍历P串的下标为pi
  • T[ti + pi]P[pi]匹配失败时,只是将P串向后移动一位(ti+1)其实有点浪费,毕竟在冲突位之前的匹配区域内都是遍历过的,那么是否可以根据前面匹配区域的特性来决定是否可以将P向右多移几位

举个🌰
T = ‘ABCDABXABCDABDE’
P = ‘ABCDABD’

可以看出在第一次匹配中,P串的最后一位匹配失败
在这里插入图片描述

此时按二重暴力的逻辑是P串右移动一位重新进行匹配,但其实下标从[ti,pi - 1]这个范围内都是匹配上的,根据这一段匹配子串的前后缀可以决定是否可以将P串向右滑动更多的距离

  • 假设P可以向右滑动长度为x的距离则
    -在这里插入图片描述

当匹配串前y个和后y个相同时,可以直接移动到后y个的起点位置,同时应当取最大y值的情况,避免遗留,保持最小有效移动

取匹配串的前缀集合,上例中为{A, AB, ABC, ABCD, ABCDA}, 和后缀集合{B, AB, DAB, CDAB, BCDAB},前后缀集合交集中长度最大的匹配项位置即是P串要移动到的开头对对齐位置

假设交集串的起始位置为i,交集串长度为len,则对齐后,ti = i,,pi = len
其中len的可以根据适配时的位置下标和交集后缀串的起始位置退出,同理知道长度后也可以推出i的位置

取前后缀集合时应当都是真前后缀集合,因为取到本身时就相当于无移动

到这里基本就是kmp算法的实现思路,核心就是利用前面已匹配上的子串的相同的前后缀,使得子串移动时能进行一次最小的有效移动


但为了不每次都去重新求最大的相同前后缀串,所以出现了next数组的预处理,预处理的遍历对象为P, 毕竟匹配上的前串必然是P的前串。

next[i]应当为[0, i - 1]范围内最长前后缀串中后缀的起点即
在这里插入图片描述

那么有了next数组后当出现不匹配时,len=pi - next[pi]; pi=len; ti = next[pi], 当没有重复公共前后缀时,相当于P串直接跳过这个区域,ti = pi + 1; pi = 0;

算法代码

1、next数组赋值

为了符合常规我们采取记录len的形式赋值next,next[i] = [0, i - 1]区域内最长交集后缀长度,同时当i==0时赋值-1

当我们知道next[0 .. i]范围内的值时,则能递推出next[i + 1]的值
int k = next[i]; // [0, i - 1] 范围内的最长交集后缀
表示0~(k - 1)的前缀子串和(i - k) ~ (i - 1)的后缀子串是相等的
此时如果p[k] == p[i]那么next[i + 1] = k + 1;比较好理解

但值p[k] != p[i]时情况就有点复杂
首先因为p[k] != p[i],所以next[i + 1]的值必然是小于k + 1
在建立在p[0, k-1]和p[i-k, i-1]这两段相等的基础上当p[k] != p[i]时
递归求出p[0, k-1]这一段的最长相交前后缀为x,x=next[k], (0~k-1段区域)
对于该x如果此时p[x] == p[i]那么p[i]=x+1;否则继续向下试探下一个x

在这里插入图片描述

关于k = next[k]这种试探方法,可以通过上图来理解,
在图1中可以看到在进行p[i]和p[k]的匹配时失配了,那么求出(0~k-1)段的最长交叉缀长度x(=next[k]),该区域如图2的绿色区域所示,绿色区域1,2,3,4的子串是相等的,那么这个时候去匹配p[x]和p[i],若相等,则next[i]=x+1,代表的最长相交前后缀区域就是【绿块1+x块】和【绿块4+i块】
若不匹配则继续想下试探绿块区域内的前后缀

int* getNext(string p) {
        int len = p.length();
        int *next = new int[len];
        next[0] = -1;
        int i = 0;
        int k = -1;
        while(i < len - 1) {// 因为每次循环都是对i+1的赋值
            //这个if包含了next[1]=0和递推规律
            if (k == -1 || p[k] == p[i]) {
                i++;
                next[i] = k + 1;
                k = next[i]; //保持前一个值的k
            } else {
                k = next[k];
            }
        }
        return next;
    }

2、查找算法

有了next数组后我们就知道当我们在i位置失配时前面的[0,i-1]匹配串的最大相交缀串
根据前面的移动后使得P前缀对齐T后缀的思路
我们设两个指标变量,延续前面假设的ti和pi,其中ti表示T中正在进行匹配区域的起点,而pi是P中正在进行试探匹配的点,而和pi对于匹配的下标为(ti + pi)

按位匹配时T和P的下标分别为T[ti+pi] EqualTo P[pi];
当pi==P.length()时匹配完成,ti为匹配到串的起点
当T[ti+pi] == P[pi]时,pi++试探下一个位置
当T[ti+pi] != P[pi]时,因为T[ti~pi-1]==P[0~pi]
所以由k = next[pi]获取T[ti~pi-1](即P[0~pi])的最大相交缀串长度
那么下一次匹配段的开头ti=(ti+pi)-k;	//(ti+pi为T上当前游标位置)
pi=k; //因为(0~k-1)这一段是交缀区域无需重新匹配
代码
 int kmp(string t, string p) {
        if (p.length() == 0) {
            return 0;
        }
        int *next = getNext(p);
        int ti = 0, pi = 0;
        while((ti + pi) < t.length() && pi < p.length()) {
            if(t[ti + pi] == p[pi]) {
                pi++;
            } else {
                int k = next[pi];
                if (k==-1) { // k==-1的情况只有在0-0的位置不匹配时出现
                    ti++;
                } else {
                    ti = ti + pi - k;
                    pi = k;
                }
            }
            if (pi == p.length()) {
                return ti;
            }
        }
        return -1;
    }

一开始本来只是想简单记录下kmp算法找回下当初学习的记忆,但当我想尽可能清晰的解释kmp算法时,发现我可能根本没有理解。
就像getNext()的代码乍一看很简单,基本看过后就可以敲出来,但真要问k=next[k]是为什么又很难解释清楚,而解释不清楚有时候也是自己关于算法的思路理解不清晰,借着这篇博客也算是对kmp进行了重游吧

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值