KMP算法-程序员宅基地

技术标签: 算法  c++  数据结构  

KMP算法

  • KMP算法是一种改进的字符串匹配算法,是由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此叫做KMP算法。
  • KMP算法的核心就是利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,以达到快速匹配的目的。

说明一下: 如果要在字符串a中寻找字符串b第一次出现的位置,那么字符串a就叫做文本串,而字符串b叫做模式串。

KMP算法的核心思想

KMP算法的核心思想

下面通过抽象图来讲解KMP算法的核心思想,文本串和模式串的长度分别为 n n n m m m 。如下:

在这里插入图片描述

假设现在正在以文本串中下标为 s s s 的字符开始与模式串进行匹配,并且已经匹配到了模式串中下标为 j j j 的字符,即文本串中的子串 [ s , i ] [s, i] [s,i]与模式串中的子串 [ 0 , j ] [0, j] [0,j] 相同。如下:

在这里插入图片描述

在继续比较后续字符的时候,如果文本串中的第 i + 1 i+1 i+1 个字符与模式串中的第 j + 1 j+1 j+1 个字符相同,就让指向文本串和指向模式串的指针同时向后移动即可。如下:

在这里插入图片描述

但在比较后续字符的过程中,如果发现文本串的第 i + 1 i+1 i+1 个字符与模式串的第 j + 1 j+1 j+1 个字符不相同,此时说明以文本串中下标为 s s s 的字符开始无法匹配成功。如下:

在这里插入图片描述

这时最简单的做法就是,以文本串中下标为 s + 1 s+1 s+1 的字符开始重新尝试与模式串进行匹配,这意味着指向文本串的指针和指向模式串的指针都需要进行回退。如下:

在这里插入图片描述

但在KMP算法中并不是这样做的,对于文本串来说,如果在 ( s , i ] (s, i] (s,i] 这个下标范围内有一个下标 k k k ,使得以文本串中下标为 k k k 的字符开始能够与模式串成功匹配,即文本串中的子串 [ k , k + m − 1 ] [k, k+m-1] [k,k+m1] 与模式串相同。如下:

在这里插入图片描述

因为文本串中的子串 [ k , k + m − 1 ] [k, k+m-1] [k,k+m1] 与模式串相同,所以文本串中的子串 [ k , i ] [k, i] [k,i] 与模式串中的子串 [ 0 , x ] [0, x] [0,x] 相同,其中 x − 0 + 1 = i − k + 1 x-0+1=i-k+1 x0+1=ik+1,即这两个子串的长度相同。如下:

在这里插入图片描述

除此之外,因为文本串中的子串 [ s , i ] [s, i] [s,i] 与模式串中的子串 [ 0 , j ] [0, j] [0,j] 相同,所以文本串中的子串 [ k , i ] [k, i] [k,i] 又与模式串中的子串 [ y , j ] [y, j] [y,j] 相同,其中 j − y + 1 = i − k + 1 j-y+1=i-k+1 jy+1=ik+1,即这两个子串的长度相同。如下:

在这里插入图片描述

所以模式串中的子串 [ 0 , x ] [0, x] [0,x] 与子串 [ y , j ] [y, j] [y,j] 相同,而这两个子串就是模式串中的子串 [ 0 , j ] [0, j] [0,j] 的一对相同前后缀,因此如果存在上述所说的下标 k k k 那么子串 [ 0 , j ] [0, j] [0,j] 就一定存在相同前后缀,此时模式串中的指针只需要回退到前缀的下一个位置继续进行匹配即可,而文本串中的指针不需要回退。

示意图如下:

在这里插入图片描述

KMP算法的核心思想:

  • 在文本串与模式串匹配的过程中,指向文本串的指针不需要回退,指向模式串的指针只需要在字符匹配失败的时候适当回退即可,不一定会回退到模式串的开头。
  • 当文本串中的第 i + 1 i+1 i+1 个字符与模式串中的第 j + 1 j+1 j+1 个字符匹配失败时,如果模式串的子串 [ 0 , j ] [0, j] [0,j] 存在最长相同前后缀,则直接让指向模式串的指针回退到对应前缀的下一个位置继续进行匹配即可。
  • 当字符匹配失败时,如果模式串的子串 [ 0 , j ] [0, j] [0,j] 不存在相同前后缀,则说明在文本串的 ( s , i ] (s, i] (s,i] 下标范围内不存在所谓的下标 k k k ,这时让指向模式串的指针回退到模式串的开头即可。

说明一下:

  • 因为在进行字符串匹配时,是要在文本串中找到模式串第一次出现的位置,所以要让上图中的下标 k k k 尽量靠前,也就是要让 [ k , i ] [k, i] [k,i] [ 0 , x ] [0, x] [0,x] [ y , j ] [y, j] [y,j] 这三个子串的长度尽可能长,所以当出现字符匹配失败时,需要找到子串的 [ 0 , j ] [0, j] [0,j] 的最长相同前后缀。
  • 当字符匹配失败时,说明以下标为 s s s 的位置开始匹配无法匹配成功,因此下标 k k k 的取值范围是 ( s , i ] (s, i] (s,i] ,所以 [ k , i ] [k, i] [k,i] 的长度必须小于子串 [ s , i ] [s, i] [s,i]的长度。
  • 子串 [ 0 , x ] [0, x] [0,x] [ y , j ] [y, j] [y,j] 的长度和 [ k , i ] [k, i] [k,i] 是相同的,子串 [ 0 , j ] [0, j] [0,j] 的长度和 [ s , i ] [s, i] [s,i] 是相同的,因此子串 [ 0 , x ] [0, x] [0,x] [ y , j ] [y, j] [y,j] 的长度就必须小于子串 [ 0 , j ] [0, j] [0,j] 的长度,虽然我们要的是子串 [ 0 , j ] [0, j] [0,j] 的最长相同前后缀,但是这个前后缀的长度必须小于子串 [ 0 , j ] [0, j] [0,j] 的长度,所以在KMP算法中,对于只有一个字符的字符串来说,它没有最长相同前后缀。

KMP算法的前缀表

KMP算法的前缀表

  • 在文本串与模式串匹配的过程中,如果文本串中的第 i + 1 i+1 i+1 个字符与模式串中的第 j + 1 j+1 j+1 个字符匹配失败,那么就需要根据模式串的子串 [ 0 , j ] [0, j] [0,j] 的最长相同前后缀,来判断应该让指向模式串的指针回退到哪个位置。
  • 在匹配模式串中的任意一个字符时,都有可能会出现匹配失败的问题,因此在进行字符串匹配之前,我们需要提前找出以模式串中每一个字符为结尾的子串的最长相同前后缀,以供后续字符串匹配时使用。
  • 由于字符串中的字符下标是从0开始的,因此模式串中的子串 [ 0 , j ] [0, j] [0,j] 的最长相同前后缀的前缀的下一个位置的下标值,实际就是最长相同前后缀的长度,所以只需要计算出以模式串中的每一个字符为结尾的最长相同前后缀的长度即可。
  • KMP算法中的前缀表的长度与模式串的长度相同,其中前缀表中的第 i i i 个位置,记录的就是模式串中的子串 [ 0 , i ] [0, i] [0,i] 的最长相同前后缀的长度。

生成前缀表的过程

下面通过抽象图来讲解KMP算法中生成前缀表的过程,模式串的长度为 m m m 。如下:

在这里插入图片描述

假设以下标为 i i i 的字符结尾的子串的最长相同前后缀是子串 [ 0 , x ] [0, x] [0,x] [ y , i ] [y, i] [y,i] ,如果这个最长相同前后缀的长度为 j j j ,那么前缀的下一个位置的下标就是 j j j 。如下:

在这里插入图片描述

如果模式串中下标为 i + 1 i+1 i+1 的字符与下标为 j j j 的字符相同,那么以下标为 i + 1 i+1 i+1 的字符结尾的子串的最长相同前后缀就是子串 [ 0 , j ] [0, j] [0,j] [ y , i + 1 ] [y, i+1] [y,i+1] ,其长度为 j + 1 j+1 j+1 ,所以前缀的下一个位置的下标就是 j + 1 j+1 j+1 。如下:

在这里插入图片描述

但如果模式串中下标为 i + 1 i+1 i+1 的字符与下标为 j j j 的字符不相同,此时说明以下标为 i + 1 i+1 i+1 的字符结尾的子串的最长相同前后缀的长度小于 j + 1 j+1 j+1 。如下:

在这里插入图片描述

假设在 ( y , i ] (y, i] (y,i] 这个下标范围内有一个下标 k k k ,使得子串 [ k , i + 1 ] [k, i+1] [k,i+1] 能够成为子串 [ 0 , i + 1 ] [0, i+1] [0,i+1] 的最长相同前后缀中的后缀,即子串 [ 0 , a + 1 ] [0, a+1] [0,a+1] 与子串 [ k , i + 1 ] [k, i+1] [k,i+1] 相同,其中 a + 1 − 0 + 1 = i + 1 − k + 1 a+1-0+1=i+1-k+1 a+10+1=i+1k+1 ,即这两个子串长度相同。如下:

在这里插入图片描述

因为子串 [ 0 , a + 1 ] [0, a+1] [0,a+1] 与子串 [ k , i + 1 ] [k, i+1] [k,i+1] 相同,所以子串 [ 0 , a ] [0, a] [0,a] 与子串 [ k , i ] [k, i] [k,i] 相同,其中 a − 0 + 1 = i − k + 1 a-0+1=i-k+1 a0+1=ik+1 ,即这两个子串的长度相同。如下:

在这里插入图片描述

除此之外,因为子串 [ 0 , x ] [0, x] [0,x] 与子串 [ y , i ] [y, i] [y,i] 相同,所以子串 [ b , x ] [b, x] [b,x] 与子串 [ k , i ] [k, i] [k,i] 相同,其中 x − b + 1 = i − k + 1 x-b+1=i-k+1 xb+1=ik+1 ,即这两个子串的长度相同。如下:

在这里插入图片描述

所以子串 [ 0 , a ] [0, a] [0,a] 与子串 [ b , x ] [b, x] [b,x] 相同,而这两个子串就是子串 [ 0 , x ] [0, x] [0,x] 的一对相同前后缀。因此如果存在上述所说的下标 k k k ,那么子串 [ 0 , i ] [0, i] [0,i] 的最长相同前后缀的前缀 [ 0 , x ] [0, x] [0,x] 就一定存在相同前后缀,此时让指向前缀的指针回退到子串 [ 0 , x ] [0, x] [0,x] 的最长相同前后缀的前缀的下一个位置继续进行匹配即可,而指向后缀的指针不需要回退。

示意图如下:

在这里插入图片描述

KMP算法的前缀表:

  • 在生成前缀表的过程中,指向后缀的指针不需要回退,指向前缀的指针只需要在字符比较失败的时候适当回退即可,不一定会回退到字符串开头。
  • 当第 i + 1 i+1 i+1 个字符(后缀的下一个字符)与第 j j j 个字符(前缀的下一个字符)不相同时,如果子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 存在最长相同前后缀,则直接让指向前缀的指针回退到对应前缀的下一个位置继续进行匹配即可。
  • 当字符比较失败时,如果子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 不存在相同前后缀,则说明在 ( y , i ] (y, i] (y,i] 下标范围内不存在所谓的下标 k k k ,这时让指向前缀的指针回退到模式串的开头即可。

说明一下:

  • 因为在生成前缀表时,是要找到以每个字符为结尾的子串的最长相同前后缀,所以要让上图中的下标 k k k 尽量靠前,也就是要让 [ k , i ] [k, i] [k,i] [ 0 , a ] [0, a] [0,a] [ b , x ] [b, x] [b,x] 这三个子串的长度尽可能长,所以当出现字符比较失败时,需要找到子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 的最长相同前后缀。
  • 当字符比较失败时,说明图中子串 [ y , i + 1 ] [y, i+1] [y,i+1] 无法作为子串 [ 0 , i + 1 ] [0, i+1] [0,i+1] 的相同前后缀,因此下标 k k k 的取值范围是 ( y , i ] (y, i] (y,i] ,所以 [ k , i ] [k, i] [k,i] 的长度必须小于 [ y , i ] [y, i] [y,i] 的长度。
  • 子串 [ 0 , a ] [0, a] [0,a] [ b , x ] [b, x] [b,x] 的长度和 [ k , i ] [k, i] [k,i] 是相同的,子串 [ 0 , x ] [0, x] [0,x] 的长度和 [ y , i ] [y, i] [y,i] 是相同的,因此子串 [ 0 , a ] [0, a] [0,a] [ b , x ] [b, x] [b,x] 的长度就必须小于子串 [ 0 , x ] [0, x] [0,x] 的长度,虽然我们要的是子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 的最长相同前后缀,但是这个前后缀的长度必须小于子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 的长度。( j − 1 j-1 j1 x x x 相同)
  • 仔细想想,你会发现生成前缀表的过程和前面字符串匹配的过程是类似的,生成前缀表时指向前缀的指针和字符串匹配时指向模式串的指针的行为是一样的,当出现字符串匹配失败时,指向模式串的指针会回退,而当出现字符比较失败时,指向前缀的指针会回退。

KMP算法的实现

根据模式串生成前缀表

生成前缀表的逻辑如下:

  • 将以下标为0的字符结尾的子串的最长相同前后缀长度初始化为0,因为在KMP算法中只有单个字符的字符串没有相同前后缀,因此其最长相同前后缀的长度为0。
  • 用指针 i i i 指向后缀的下一个位置,指针 j j j 指向前缀的下一个位置。在计算以下标为 i i i 的字符结尾的子串的最长相同前后缀的长度时,如果下标为 j j j 的字符与下标为 i i i 的字符不相同,那么指针 j j j 回退到子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 的最长相同前后缀的前缀的下一个位置,如果字符仍然与指针 i i i 指向的字符不相同,则指针 j j j 继续回退,直到与指针 i i i 指向的字符相同或回退到字符串开头。
  • 如果下标为 j j j 的字符与下标为 i i i 的字符相同,则指针 j j j 后移,此时指针 j j j 指向的就是子串 [ 0 , i ] [0, i] [0,i] 的最长相同前后缀的前缀的下一个位置,也就是最长相同前后缀的长度。

代码如下:

//根据模式串生成前缀表
vector<int> getNext(const string& needle) {
    
    //j: 指向前缀末尾位置(前缀的长度)
    //i: 指向后缀末尾位置
    int n = needle.size();
    vector<int> next(n);
    next[0] = 0; //以下标为0的字符结尾的字符串的相同前后缀长度为0
    int j = 0; //指向前缀末尾位置
    for (int i = 1; i < n; i++) {
     //i从1开始,next[0]已被初始化
        while (j > 0 && needle[j] != needle[i]) {
     //前后缀末尾不相同,前缀位置向前回退
            j = next[j - 1]; //j回退到以下标为j-1的字符结尾的字符串的最长相同前后缀的前缀的下一个位置
        }
        if (needle[j] == needle[i]) {
     //前后缀末尾相同
            j++; //j后移后的数值就是此时前缀的长度
        }
        next[i] = j; //以下标为i的字符结尾的字符串的相同前后缀的长度(也是前缀的下一个位置的下标)
    }
    return next; //返回前缀表
}

使用前缀表进行字符串匹配

字符串匹配的逻辑如下:

  • 如果模式串为空串,那么模式串在文本串中第一次出现的位置就是第0个位置,否则需要进行字符串匹配,在进行字符串匹配之前,需要先根据模式串生成前缀表。
  • 用指针 i i i 指向文本串中下一个待比较的字符,指针 j j j 指向模式串中下一个待比较的字符。在字符串匹配过程中,如果文本串中下标为 i i i 的字符与模式串中下标为 j j j 的字符不匹配,那么指针 j j j 回退到子串 [ 0 , j − 1 ] [0, j-1] [0,j1] 的最长相同前后缀的前缀的下一个位置,如果字符仍然与指针 i i i 指向的字符不匹配,则指针 j j j 继续回退,直到与指针 i i i 指向的字符相同或回退到模式串开头。
  • 如果模式串中下标为 j j j 的字符与文本串中下标为 i i i 的字符匹配,则指针 j j j 后移,如果指针 j j j 移到了模式串末尾,则匹配完毕,此时文本串中的起始匹配位置即为模式串第一次出现的位置。

代码如下:

int searchNeedle(string haystack, string needle) {
    
    //j: 指向模式串中下一个待比较的字符
    //i: 指向文本串中下一个待比较的字符
    int n = needle.size();
    if (n == 0) //模式串为空串
        return 0;
    vector<int> next = getNext(needle); //根据模式串生成前缀表
    int j = 0; //指向模式串中下一个待比较的字符
    for (int i = 0; i < haystack.size(); i++) {
     //指向文本串中下一个待比较的字符
        while (j > 0 && needle[j] != haystack[i]) {
     //当前字符不匹配,指向模式串的指针向前回退(重新寻找匹配位置)
            j = next[j - 1]; //回退到以下标为j-1的字符结尾的字符串的最长相同前后缀的前缀的下一个位置
        }
        if (needle[j] == haystack[i]) {
     //当前字符匹配
            j++; //指向模式串的指针后移
            if (j == n) //j已经到达模式串末尾,匹配完毕
                return i - n + 1; //返回文本串中的起始匹配下标
        }
    }
    return -1; //匹配失败,文本串中没有出现过模式串
}

KMP算法的时间复杂度

KMP算法的时间复杂度

假设文本串长度为 n n n ,模式串长度为 m m m

  • 对于暴力解法来说,它会依次选取文本串中的每一个字符作为匹配的起始位置,如果当前起始位置匹配失败则尝试下一个起始位置重新从模式串的开头进行匹配,因此暴力解法的时间复杂度是 O ( n × m ) O(n\times m) O(n×m)
  • 对于KMP算法来说,它会先遍历一遍模式串生成前缀表,然后再遍历一遍文本串进行字符串匹配,因此KMP算法的时间复杂度是 O ( n + m ) O(n+m) O(n+m)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chenlong_cxy/article/details/130309661

智能推荐

CentOS操作系统防火墙添加例外端口_centos7域控服务器里防火墙需要例外那些端口-程序员宅基地

文章浏览阅读1w次。CentOS6与CentOS7添加防火墙例外端口的命令不同,需单独来说:(1)CentOS6下添加防火墙例外端口#vim/ets/sysconfig/iptables在 -A INPUT -m state--state NEW -m tcp -p tcp --dport 22 -j ACCEPT一行的后台添加类似的一行命令即可,如 # Firewall configura..._centos7域控服务器里防火墙需要例外那些端口

数据结构|数组为什么这么快?-程序员宅基地

文章浏览阅读2.9k次,点赞8次,收藏10次。我相信在很多地方,大家在进行数据结构的比较的时候,说到数组,第一反应就是—快,但是为什么快呢?数组到底快在哪里呢?不知道大家是否有思考过这个问题,这篇文章,我就讲讲我对数组的一些看法,抛砖引玉,希望大家多多交流!文章目录数组到底哪里快?查找快吗?为什么数组能支持随机访问呢?答案:举例分析:我们要分析的第一个问题是:数组到底哪里快?查找快吗?可能有的同学第一反应利用数组进行查找的话,时间...

基于Matlab图像融合总结_图像融合代码matlab-程序员宅基地

文章浏览阅读273次。图像融合是一种将多幅图像信息合并成一幅新图像的技术,可以获得比单独图像更多的信息。在Matlab中,有多种方法可以实现图像融合,包括像素级融合、变换域融合和深度学习融合等。本文将总结这些方法,并提供相应的源代码。在实际应用中,根据图像融合的具体需求,选择合适的方法进行处理。这些方法可以互相结合,实现更细致、更精确的图像融合效果。希望本文提供的源代码和示例能够对您在Matlab中进行图像融合的工作有所帮助。基于Matlab图像融合总结。_图像融合代码matlab

算法题:平均数为k的最长连续子数组-程序员宅基地

文章浏览阅读1k次。平均数为k的最长连续子数组_平均数为k的最长连续子数组

HMC使用手册_台达hmc07-n511h52使用手册-程序员宅基地

文章浏览阅读1k次。今天偶然间想起了之前发生的一次运行事件,在这次故障中,物理服务器宕机,远程终端无法救急。于是就需要用到HMC(硬件管理控制台)进行底层的操作,之前对于这个工具的使用确实不多,所以今天就特意在网上找了一篇关于HMC使用的手册,这里给大家分享一下,如果大家想要看网页原版,下面是网址:HMC使用手册..._台达hmc07-n511h52使用手册

设置定时任务的几种方式_制定定时任务的方法-程序员宅基地

文章浏览阅读471次。◦ 前言 ◦ 解决方案 普通线程sleep的方式实现定时任务 Timer实现定时任务 ScheduledExecutorService实现定时任务 Handler实现定时任务 AlarmManager实现精确定时操作前言项目中总是会因为各种需求添加各种定时任务,所以就打..._制定定时任务的方法

随便推点

Java 中应用Dijkstra算法求解最短路径_路由最短路径代码java-程序员宅基地

文章浏览阅读476次。Dijkstra算法是一个经典的解决最短路径问题的算法,在路由算法、导航系统等领域都有广泛的应用。它通过逐步选择距离起始节点最近的节点,并更新其邻接节点的最短距离,最终得到起始节点到其他所有节点的最短路径。然后,在一个循环中,每次选择距离最小且未加入最短路径集合的节点,将其加入最短路径集合,并更新其邻接节点的最短路径长度。它遍历所有未加入最短路径集合(shortestPathTreeSet)的节点,查找距离最小且未加入最短路径集合的节点,并返回其索引。数组来追踪起始节点到其他节点的最短路径长度,_路由最短路径代码java

Mybatis-plus解决selectOne查询多个会报错的问题_mybatisplus selectone-程序员宅基地

文章浏览阅读2w次,点赞13次,收藏36次。123_mybatisplus selectone

【android】android12蓝牙框架_android 蓝牙框架-程序员宅基地

文章浏览阅读1.6k次,点赞15次,收藏22次。参考:hl=zh-cnhl=zh-cnhl=zh-cn。_android 蓝牙框架

玩转X-CTR100 l STM32F4 l PS2无线手柄-4WD智能小车-程序员宅基地

文章浏览阅读388次。我造轮子,你造车,创客一起造起来!更多塔克创新资讯【塔克社区 www.xtark.cn 】【塔克博客 www.cnblogs.com/xtark/ 】 前面已介绍X-CTR100控制器解码PS2无线手柄,本文继续前文,使用PS2无线手柄,实现4WD智能小车的控制,实现两种控制模式,方向模式和坦克模式。 例程-PS2无线手柄-4WD智能小车(方向模式) 使用4个..._stm32-4wd智能小车摘要

(深度学习)基于残差卷积——resnet的水稻病害识别_resnet152-程序员宅基地

文章浏览阅读1.6k次,点赞20次,收藏34次。利用残差卷积神经网络Resnet152实现水稻病害识别,附带数据集和预训练模型下载链接,代码框架可套用于其它分类任务。_resnet152

Esp8266 进阶之路34 【外设篇③】乐鑫esp8266 NONOS SDK 3.0编程使用 SPI 驱动基于Max7219芯片的八位数码管,显示日期信息。(附带Demo)_8266时钟代码 数码管-程序员宅基地

文章浏览阅读7k次,点赞5次,收藏28次。本系列博客学习由非官方人员 半颗心脏 潜心所力所写,仅仅做个人技术交流分享,不做任何商业用途。如有不对之处,请留言,本人及时更改。 1、 Esp8266之 搭建开发环境,开始一个“hellow world”串口打印。 2、 Esp8266之 利用GPIO开始使用按钮点亮你的“第一盏灯”。 3、 Esp8266之 利用 "软件定时器 " 定时0.5秒闪烁点亮一盏LED。4 、Es..._8266时钟代码 数码管

推荐文章

热门文章

相关标签