KMP算法(我的理解)

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

下面先直接给出KMP的算法流程:
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,

  1. 如果j = -1,或者str[i] == p[j](即当前字符匹配成功),都令i++,j++,继续匹配下一个字符;
  2. 如果j != -1,且str[i] != p[j](即当前字符匹配失败),则i不变,令j = next[j],可以理解为当匹配失败时,模式串p并不是从头开始匹配,而是从next[j]位置继续匹配;

与暴力匹配相比,KMP的不同之处就在于j的回溯位置,j不是傻乎乎的回溯到初始位置,因而大大提高了算法效率。这之中发挥巨大作用的就是next[]数组,从上面的分析中我们已经知道,next[]数组存在的意义就是当匹配失败时,我们能确定的知道j应该回溯到什么位置。那么为什么next[]会有这么强大的功能呢?

这就要从字符串的前缀和后缀说起:

  1. 先解释定义:
    例如字符串hello,它的前缀集合为{h, he, hel, hell},后缀集合为{ello, llo, lo, 0},要注意的是,字符串本身并不是自己的后缀。
  2. PMT
    例如,对于aba,它的前缀集合为{a, ab},后缀 集合为{ba, a}。两个集合的交集为{a},那么PMT为1。再比如,对于字符串ababa,它的前缀集合为{a, ab, aba, abab},它的后缀集合为{baba, aba, ba, a}, 两个集合的交集为{a, aba},那么PMT为3。

这里解释一下:PMT是一个被称为部分匹配表(Partial Match Table)的数组,PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度(这里的字字符串是指从初始位置到当前位置这一字串)。

下面我们举例说明如何求PMT,例如有一字符串abababca

char a b a b a b c a
index 0 1 2 3 4 5 6 7
pmt 0 0 1 2 3 4 0 1
举例说一下具体求值过程:例如`pmt[2] = 1`,当前子串为`aba`,则前缀集合与后缀集合的交集中最长元素是`a`,所以`pmt[2] = 1`,再例如`pmt[5] = 4`,当前子串为`ababab`,则前缀集合与后缀集合的交集中最长元素是`abab`,所以`pmt[5] = 4`。

接下来就是我们的重头戏了,如何求next[]数组?
当我们匹配到c时,如下图所示:
在这里插入图片描述
此时匹配失败,但是abababca,即前6个字符是已经匹配成功的,对应的子串是ababab,前面已经知道pmt[5] = 4,即存在前缀ababab,存在后缀ababab,这里最重要的一点就是c前面的4个字符abababc也必定匹配成功,那么如果将模式串向右移动两个位置,即将原来后缀的位置替换成前缀这4个字符,如图(b)所示,此时前面4个字符一定匹配成功,j回溯到这个位置,继续匹配,从图中可以看出next[6] = 4(c对应的index为6);

说了半天,好像并没说如何求next[]数组,但是真正的理解了的话,可以知道next[]pmt[]是有直接关系的,next[j] = pmt[j - 1]

下面以表格的形式给出具体next[]数组的值:

char a b a b a b c a
index 0 1 2 3 4 5 6 7
pmt 0 0 1 2 3 4 0 1
next -1 0 0 1 2 3 4 0

接下来给出求next[]数组的代码,这里跳过了求pmt的环节:

void InitNextArr(char str[], int next[]){
    int i = 0, j = -1;
    next[0] = -1;
    while(i < strlen(str)){
        if(j == -1 || str[i] == str[j]){
            i++;
            j++;
            next[i] = j;
        }
        else{
            j = next[j];
        }
    }
}

现在给出匹配函数:

int KMP(char str[], char p[], int next[]){
    int i = 0, j = 0;
    while(i < strlen(str) && j < strlen(p)){
        if(j == -1 || str[i] == p[j]){
            i++;
            j++;
        }
        else
            j = next[j];	//当匹配失败时,j回溯
    }
    if(j == strlen(p))
        return i - j;
    else
        return -1;
}

完整代码如下:

#include <iostream>
#include <stdio.h>
#include <cstring>
using namespace std;

void InitNextArr(char str[], int next[]){
    int i = 0, j = -1;
    next[0] = -1;
    while(i < strlen(str)){
        if(j == -1 || str[i] == str[j]){
            i++;
            j++;
            next[i] = j;
        }
        else{
            j = next[j];
        }
    }
}

int KMP(char str[], char p[], int next[]){
    int i = 0, j = 0;
    while(i < strlen(str) && j < strlen(p)){
        if(j == -1 || str[i] == p[j]){
            i++;
            j++;
        }
        else
            j = next[j];
    }
    if(j == strlen(p))
        return i - j;
    else
        return -1;
}

int main(){
    char ch1[] = "abcdabcde", ch2[] = "abcde";
    int next[8];
    InitNextArr(ch2, next);
    printf("%d\n", KMP(ch1, ch2, next));
}

发布了37 篇原创文章 · 获赞 30 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40941722/article/details/96481266