KMP算法到底在干什么

在写这篇文章之前参考了两篇文章,觉得写得很好,尤其是阮一峰写的KMP算法
KMP算法的关键是它的next数组,利用next数组能够高效地确定在当前失配的情况下,应当将模式串移动多少位才能够避免不必要的匹配。

不必要的匹配
如图,如果当前目标串与模式串在D处发生失配,传统方法是从模式串的开头位置重新移动,直到开头位置能够找到匹配的字符然后重新开始下一个匹配流程。但我们注意到在D发生失配之前的AB是能够被开头的AB正常匹配,那中间那些测试就是冗余的做法。
《KMP算法到底在干什么》

《KMP算法到底在干什么》

一个基本原理
让算法知道在失配字符之前的最长前缀匹配信息,同时利用这个信息跳过已经被比较过的位置,继续后移模式串,以此来提高匹配效率。下面详细说明。

过程模拟
首先确定一个规则:把模式串移动到最新位置后,在失配位置之前的字符都需要与目标串中的字符匹配
还是用刚才那张图,观察在失配字符D之前有最长两个字符AB能够与模式串的前缀相匹配,那么当前的移动位数应当是4。这样移动的理由是我可以保证忽略尽量多的无用匹配,而同时保证不丢失应该做匹配测试的所有可能位置。我们可以得出一个移动位数的通式:

=

比如这个例子就是:

4=62

《KMP算法到底在干什么》

next数组在干嘛?
上面说的做法,其实就是把模式串分割成前缀后缀来考虑,比如模式串为: ABCDABD ,我们可以分成以下几种情况

前缀后缀前缀后缀最长匹配
ABCDABD0
ABCDABD0
ABCDABD0
ABCDABD2
ABCDABD0
ABCDABD0

之前提到过一个next数组,这个数组是和模式串相关的,我们利用它存储到当前字符位置时,前缀后缀的最长匹配字符数,比如以上模式串的next数组表示为:

PatternABCDABD
next0000120

那么上面提到的位移公式就可以借助next数组来实现了。

KMP如何借助next数组移位
道理其实很简单,如果目标串和模式串的字符匹配,那么就同时移动两者的下标;如果不能匹配,就使用next数组来获得移动的数目。但编程方法实现的话,next数组我们需要再修改一下,这样就能够直接获得当前失配位置应当对应的新的模式串字符下标(因为我们关注的是在失配字符之前有几个匹配的字符)。

PatternABCDABD
next-1000012

关于next数组的第一个位置为-1的问题,之后再将,KMP的搜索代码如下:

void kmp(const string& match, const string& pattern) {
    int posP = -1, posM = -1, lenM = match.length(), lenP = pattern.length();
    while (posP < lenP && posM < lenM) {
        if (posP == -1 || pattern[posP] == match[posM]) {
            posP++;
            posM++;
        } else {
            posP = nextArr[posP];
        }
    }
}

如何求next数组
从上面的代码你应该已经看出next数组记录的其实也就是前后缀失配的第一个字符的下标(也就是前后缀最长匹配情况下最后一个字符下标+1),所以为了吻合这一原则我们让next的第一个元素值为-1。所以怎么求呢?
粗略的想想,好像应该是有两个变量分别记录模式串前缀下标i和后缀下标j,每次移动j,通过j把模式串分成前缀和后缀,然后测试它们的匹配情况更新next数组。但这样其实没有必要,因为在这个过程中,next数组记录的信息没有被使用到反而还重复地被刷新,复杂度还成了 O(n2) 。我们其实可以充分利用已经知道的next数组值,来更新下一次需要和前缀匹配的后缀的位置:

if (i == -1 || pattern[j] == pattern[i]) {
    i++;
    j++;
    next[j] = i;
} else {
    i = next[i];  // update i with next[i]
}

一个问题
因为使用next数组来找到失配处的新模式串字符,但从上面生成next数组的代码来看,如果新的下标对应的新模式串字符其实和原来的适配字符是一样的话,那此次移动也是无效的,比如这个例子:
《KMP算法到底在干什么》

《KMP算法到底在干什么》
所以在得到新下标时,不急着更新next数组,而是进一步考虑新的字符是不是和之前的一样,如果一样,那么进一步调用当前前缀i的next值来更新next[j]

void produceNext(int* nextArr, const string& pattern) {
    int i = -1, j = 0, end = pattern.length() - 1;
    *(nextArr) = -1;
    while (j < end) {
        if (i == -1 || pattern[j] == pattern[i]) {
            i++;
            j++;
            // consider the next character: pattern[i] ?= pattern[j]
            if (pattern[i] != pattern[j]) *(nextArr + j) = i;
            else *(nextArr + j) = *(nextArr + i);
        } else {
            i = *(nextArr + i);
        }
    }
}

完整的实现

#include <iostream>
#include <cstdio>

using namespace std;

void produceNext(int* nextArr, const string& pattern) {
    int i = -1, j = 0, end = pattern.length() - 1;
    *(nextArr) = -1;
    while (j < end) {
        if (i == -1 || pattern[j] == pattern[i]) {
            i++;
            j++;
            // consider the next character: pattern[i] ?= pattern[j]
            if (pattern[i] != pattern[j]) *(nextArr + j) = i;
            else *(nextArr + j) = *(nextArr + i);
        } else {
            i = *(nextArr + i);
        }
    }
}

void kmp(const string& match, const string& pattern) {
    int lenP = pattern.length();
    int* nextArr = new int[lenP];

    produceNext(nextArr, pattern);

    int posP = -1, posM = -1, lenM = match.length();

    while (posP < lend && posM < lenM) {
        if (posP == -1 || pattern[posP] == match[posM]) {
            posP++;
            posM++;
        } else {
            posP = nextArr[posP];
        }
    }

    if (posP != lens) printf("I cannot find a substring of MATCH!\n");
    else printf("I've found a substring start from: %d.\n", posM - lens);

    delete[] nextArr;
}

int main() {
    string a, b;
    puts("Please input MATCH and PATTERN string.");
    cin >> a >> b;
    cout << "YOUR INPUT: " << a << ", " << b << endl;
    kmp(a, b);
    return 0;
}

时间复杂度
生成next: O(m) ,KMP同时遍历一遍: O(n) ,总的复杂度为: O(n+m)

引用
1. 字符串匹配的KMP算法
2. 从头到尾理解KMP

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/kornberg_fresnel/article/details/78002188
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞