KMP算法详解

KMP算法

K M P KMP KMP算法是字符串匹配中的一个较好的算法,它的目的是在给定一个模式串 p p p和原字符串 q q q中,找到 p p p q q q中第一次出现的位置。
在这里插入图片描述

​ 如上图所示,可以很简单的看出字符串 p p p在字符串 q q q中第一次出现的位置,如果位置从1开始计算的话,那么第一次出现的位置就是3。那么如何使用一个简单的程序来实现该功能呢,首先最容易想到的就是暴力法,不断的使用 p p p去对应 q q q中的字符,如果遇到一个不相等的,则 p p p从第一个字符开始重新开始和 q q q后续的字符进行比较。这其中会产生一些重复比较的过程,如果去掉这些重复比较的过程,就是 K M P KMP KMP算法所作的事情,关于哪里重复,后续会继续讲到。

​ 下面的代码为普通的暴力法,是参考我之前写过的 K M P KMP KMP的内容https://blog.csdn.net/weixin_44267007/article/details/109272225,这里面 p p p是原字符串, q q q是模式串,和我们之前的定义刚好相反,但是接下来还是按照之前的定义来进行讲解,暴力法代码实现太过简单,就不赘述了。

int getIndex(string p,string q)
{
    int i=0;
    int j=0;
    while(i<(int)p.length() && j<(int)q.length())
    {
        if(p[i] == q[j])
        {
            i++;
            j++;
        }
        else
        {
            i=i-j+1;
            j=0;
        } 
        if(j==q.length())
            return i-q.length();
    }
    return -1;
}

前置知识

​ 在深入了解 K M P KMP KMP算法之前,还需要对于串中的一些概念和 C + + C++ C++中字符串库 s t r i n g string string做一些介绍,因为这些工具都会大量运用在之后的原理解释中。

串的基本概念

​ 在字符串的基本概念这一块,主要介绍字符串的前缀后缀,以及最长相同前缀后缀。接下来放出定义。

前缀:从字符串第一个字符开始的任意长度的连续子串

后缀:从字符串最后一个开始往前的任意长度的连续子串

举个例子,对于字符串“ababc",前后缀如下所示。

定义
前缀“”(空串),“a”,“ab”,“aba”,“abab”,“ababc”
后缀“”(空串),“c”,“bc”,“abc”,“babc”,“ababc”

​ 通过上述例子应该对前缀和后缀有了了解,那么最长相同前缀后缀,对于一个字符串,在知道其前缀后缀的基础上,如果前缀和后缀中相同的串就叫做该串的相同前缀后缀,而在所有的相同前缀后缀中,最长的一个叫做最长相同前缀后缀。

​ 例如对于字符串"eefegeef"而言,最长相同前缀后缀为“eef”,长度为3。也可以通过下图来理解,即两个字符串的最长相交长度。
在这里插入图片描述

C + + C++ C++ s t r i n g string string库的函数

​ 接下来对于代码实现中需要使用到的 s t r i n g string string库函数进行分析, s t r i n g string string库需要在代码最前面的 i n c l u d e include include区域添加 C + + C++ C++中的头文件 < s t r i n g > <string> <string>。接下来主要对其中的一些函数进行介绍,其实也很简单。

​ 首先是length()和size()函数,这两个函数的功能都是返回一个字符串的长度,这里的长度不包括在 C C C语言中学习到的结束符。但是需要注意的是,这两个函数返回的值不是 i n t int int类型,而是 u n s i g n e d _ i n t unsigned\_int unsigned_int类型,也就是无符号整数。所以在比较大小的时候需要先将其转换成 i n t int int类型,使用强制类型转换即可,否则根据程序设计中的特点,可能会出现一些意外的错误,这里列举一种,例如 i n t int int类型中的-1和 u n s i g n e d _ i n t unsigned\_int unsigned_int中的1进行比较,程序会先将两边的数字统一转换为 u n s i g n e d _ i n t unsigned\_int unsigned_int类型的整数,那么看着是-1,实际在比较的时候就是一个非常大的正数,也就是 2 32 − 1 2^{32}-1 2321。故最终会得到“-1>1”的情况。

​ 接下来对于该库的使用,在定义字符串时,可以直接使用 s t r i n g string string作为关键字来进行变量定义,而字符串中的某一个元素使用起来和字符数组基本没有差异。

KMP核心算法—next数组

​ 对于 K M P KMP KMP算法来说,其最核心的部分就是其中的 n e x t next next数组,首先抛开 K M P KMP KMP算法不谈,先讲讲什么是 n e x t next next数组,这里先定义一个字符串 P P P,则其对应的 n e x t next next数组的定义如下所示。
n e x t [ i ] = { − 1 , i = 0 max ⁡ { k ∣ 0 < k < i 且 P 0 P 1 . . . P k − 1 = P i − k . . . P i − 1 } next\left[ i \right] =\begin{cases} -1, i=0\\ \max \left\{ k|0<k<i\text{且}P_0P_1...P_{k-1}=P_{i-k}...P_{i-1} \right\}\\ \end{cases} next[i]={1,i=0max{k∣0<k<iP0P1...Pk1=Pik...Pi1}
​ 从上述公式中可以看出, P 0 P 1 . . . P k − 1 = P i − k . . . P i − 1 P_0P_1...P_{k-1}=P_{i-k}...P_{i-1} P0P1...Pk1=Pik...Pi1中对应的 k k k值其实就是字符串 P P P的一个子串的最长相同前缀后缀,这个子串从 P P P的0位置开始,到 i − 1 i-1 i1处为止,故也可以理解为字符串 P P P的前 i i i个字符构成的字串。

​ 通过上述分析也就对 n e x t next next数组的定义有了一个理解, n e x t next next数组记录的是字符串 P P P的不同前缀的最长相同前缀后缀的长度。其中 n e x t [ i ] next[i] next[i]的值为 P P P的前 i i i个字符构成的子串的最长相同前缀后缀。

​ 接下来就要考虑如何计算这个 n e x t next next数组,这里就需要使用到 K M P KMP KMP算法的思想,也就是使用前后缀匹配来进行重复过程的去除。以下图为例,
在这里插入图片描述

​ 如上图所示,此时b和c不匹配,使用传统暴力法来求解的话,则是需要将方框中的两个字符进行比较,也就是于原字符串后移一个字符,然后模式串需要从头开始进行比较判断。

​ 但是仔细观察该例子可以看出,这样匹配下去是没有用的,中间部分依旧匹配不上。这里就可以利用前面的最长前缀后缀的思想,例如在这里,下面的字符串匹配到字符c才出现不匹配,这说明c之前的字符全部都匹配上了。那么如果已经知道了c字符处的 n e x t next next数组值,可以看出就是1,那么接下来直接将 P [ 1 ] P[1] P[1] Q Q Q中的b字符(当前不匹配的字符)进行比较即可。

​ 那么为什么上述方法就可以呢?仔细思考 n e x t next next数组的定义,以这个具体例子为例,c处的 n e x t next next数组值为1,说明abca这个字符串的最长前缀后缀长度为1,即前缀a=后缀a,而后缀a和原字符串当前匹配的b字符之后的a字符是匹配的,那么使用 n e x t next next数组值,即将 P [ 1 ] P[1] P[1] Q Q Q中的b字符进行比较时,此时的 P [ 0 ] = a P[0]=a P[0]=a是和b字符前面的a字符是匹配的。
在这里插入图片描述

​ 在上述例子中,设第一个不匹配的字符在模式串 P P P中位置为 i i i,那么根据 n e x t next next数组可以知道 P P P中前 n e x t [ i ] next[i] next[i]个字符和后 n e x t [ i ] next[i] next[i]个字符是相同的,而后 n e x t [ i ] next[i] next[i]个字符是和原字符串 Q Q Q中当前不匹配的字符前面的 n e x t [ i ] next[i] next[i]个字符是相匹配的,故前面 n e x t [ i ] next[i] next[i]的内容和原字符串 Q Q Q中当前不匹配的字符前面的 n e x t [ i ] next[i] next[i]个字符是相匹配的,所以接下来只需要将之前不匹配的字符和 P [ n e x t [ i ] ] P[next[i]] P[next[i]]进行比较即可,即 i = n e x t [ i ] i=next[i] i=next[i]

​ 在知道了 n e x t next next数组有什么用以及 K M P KMP KMP算法的思想之后,接下来就可以利用上述思想来计算 n e x t next next数组的值。首先 n e x t [ 0 ] = − 1 next[0]=-1 next[0]=1,也可以等于0,这个只是不同的写法而已,这里统一规定 n e x t [ i ] = − 1 next[i]=-1 next[i]=1。接下来根据定义即可,假设已知 i i i位置处的 n e x t [ i ] = k next[i]=k next[i]=k,即 P 0 P 1 . . . P k − 1 = P i − k . . . P i − 1 P_0P_1...P_{k-1}=P_{i-k}...P_{i-1} P0P1...Pk1=Pik...Pi1,那么很明显,如果 P [ k ] = P [ i ] P[k]=P[i] P[k]=P[i],那么就可以得到 n e x t [ i + 1 ] = k + 1 next[i+1]=k+1 next[i+1]=k+1;反之,则需要使用前面的思想继续匹配,即 P [ i ] P[i] P[i] P [ n e x t [ k ] ] P[next[k]] P[next[k]]进行比较,如果相同,则 P [ i ] = n e x t [ k ] + 1 P[i]=next[k]+1 P[i]=next[k]+1
在这里插入图片描述

​ 如果直到遇到 n e x t [ k ] = − 1 next[k]=-1 next[k]=1,那么说明此处没有最长相同前缀后缀,则原字符串从不匹配的下一个字符进行计算,模式串从0开始。代码如下。

// 获取next数组
void getNext(int* next, string p)
{
    int j = 0;  // 从0开始
    int k = -1; // next[0] = -1
    next[j] = k;
    while (j < (int)p.size() - 1)   // 逐个字符进行比对
    {
        if (k == -1 || p[j] == p[k])    // 匹配成功或最长相同前缀后缀为0
        {
            j++;
            k++;
            next[j] = k;
        }
        else    // 匹配失败
            k = next[k];
    }
}

KMP匹配

​ 在得知了 n e x t next next数组如何计算之后,其实 K M P KMP KMP算法中的过程也就基本出来了,也是默认开始时逐个字符的去进行比较,在遇到不匹配的情况时,使用 n e x t next next数组去更新下一次去匹配的位置,这样的好处在于不需要将原字符串 Q Q Q的指针进行回溯,即只需要遍历一次 Q Q Q字符串即可。

// KMP算法
int KMP(string p, string q)
{
    int* next = new int[(int)p.size()]; // 定义next数组
    getNext(next, p);   // 获取next数组
    int i = 0;  // 模式串p的下标
    int j = 0;  // 原字符串q的下标
    while (i < (int)p.size() && j < (int)q.size())  // 逐个匹配即可
    {
        if ( i == -1 || p[i] == q[j])   // 如果匹配成功或p无法与q当前字符匹配,则跳过
        {
            i++;
            j++;
        }
        else    // 匹配失败则用next数组更新
            i = next[i];
    }
    delete []next;
    if (i == (int)p.size()) // 如果i走到了末尾则说明匹配成功
        return j - i;

    return -1;  // 说明匹配失败
}

next数组计算例题

​ 完成了上述过程即算是基本对于KMP算法有了一个基本的了解,接下来对于 n e x t next next数组的计算进行一个巩固。
在这里插入图片描述

接下来对该问题进行解答
在这里插入图片描述

在这里插入图片描述

KMP算法改进

​ 接下来对 K M P KMP KMP算法进行一个简单的改进,主要在 n e x t next next数组的计算上面进行一些简单的改进。首先还是看看为什么可以进行改进,以下面这个例子为例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mPRjbklG-1627655510996)(…\4.串\pics\fd77685cc76eddad2c3d328fc8663ec.png)] 从上面简单看出,由于在模式串中可能存在 P [ k ] = P [ n e x t [ k ] ] P[k]=P[next[k]] P[k]=P[next[k]]的情况,如果 P [ k ] ! = P [ j ] P[k]!=P[j] P[k]!=P[j],那么其实使用 k = n e x t [ k ] k=next[k] k=next[k]中会做一些无用功,那么有效的做法就是将所有 P [ k ] = P [ n e x t [ k ] ] P[k]=P[next[k]] P[k]=P[next[k]]的next值进行统一。故整个流程如下所示。

  1. 首先计算出next数组

  2. 遍历一次字符串,比较 P [ i ] = P [ n e x t [ i ] ] P[i]=P[next[i]] P[i]=P[next[i]]

    2.1 如果相等,则 n e x t v a l [ i ] = n e x t v a l [ n e x t [ i ] ] nextval[i]=nextval[next[i]] nextval[i]=nextval[next[i]]

    2.2 如果不相等,则 n e x t v a l [ i ] = n e x t [ n e x t [ i ] ] nextval[i]=next[next[i]] nextval[i]=next[next[i]]

接下来对于 n e x t v a l nextval nextval数组的计算进行一个简答的练习。
在这里插入图片描述
在这里插入图片描述

其他字符串知识点

计算最长前缀后缀

在这里插入图片描述

计算循环节

在这里插入图片描述

补充字符使原字符串变为循环串

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值