【转载】如何更好地理解和掌握 KMP 算法?

2022年 9月 16日 32点热度 0人点赞 0条评论

file

如何更好地理解和掌握 KMP 算法? - 海纳的回答 - 知乎
https://www.zhihu.com/question/21923021/answer/281346746

原作者写的不错, 但是代码有几处问题, 下面这个是我修改之后的, 在 leetcode 上跑没问题.

28. 找出字符串中第一个匹配项的下标

class Solution {
public:

    vector<int> getNext(const string &needle) {
        int n = needle.length();
        vector<int> next(n, 0);
        next[0] = -1;
        int i = 0, j = -1;
        while(i < n - 1) {    // 这里 i < n - 1, 否则导致越界
            if (j == -1 || needle[i] == needle[j]) {
                ++i;
                ++j;
                next[i] = j;
            } else {
                j = next[j];
            }
        }
        return next;
    }

    int strStr(string haystack, string needle) {

        int i = 0, j = 0;
        vector<int> next = getNext(needle);
        // 由于 j 可能为 -1, 所以长度比较的时候要转为 int
        while(i < (int) haystack.length() && j < (int) needle.length()) {
            if (j == -1 || haystack[i] == needle[j]) {
                ++i;
                ++j;
            } else {
                j = next[j];
            }
        }

        return j == needle.length() ? i - j : -1;
    }
};

下面是正文.

有些算法, 适合从它产生的动机, 如何设计与解决问题这样正向地去介绍. 但 KMP 算法真的不适合这样去学. 最好的办法是先搞清楚它所用的数据结构是什么, 再搞清楚怎么用, 最后为什么的问题就会有恍然大悟的感觉. 我试着从这个思路再介绍一下. 大家只需要记住一点, PMT 是什么东西. 然后自己临时推这个算法也是能推出来的, 完全不需要死记硬背.

KMP 算法的核心, 是一个被称为部分匹配表 (Partial Match Table) 的数组. 我觉得理解 KMP 的最大障碍就是很多人在看了很多关于 KMP 的文章之后, 仍然搞不懂 PMT 中的值代表了什么意思. 这里我们抛开所有的枝枝蔓蔓, 先来解释一下这个数据到底是什么.

对于字符串 abababca, 它的 PMT 如下表所示:

file

就像例子中所示的, 如果待匹配的模式字符串有 8 个字符, 那么 PMT 就会有 8 个值.

我先解释一下字符串的前缀和后缀. 如果字符串 A 和 B, 存在 A=BS, 其中 S 是任意的非空字符串, 那就称 B 为 A 的前缀. 例如, Harry的前缀包括 {"H", "Ha", "Har", "Harr"}, 我们把所有前缀组成的集合, 称为字符串的前缀集合. 同样可以定义后缀 A=SB, 其中 S 是任意的非空字符串, 那就称 B 为 A 的后缀, 例如, Potter 的后缀包括 {"otter", "tter", "ter", "er", "r"}, 然后把所有后缀组成的集合, 称为字符串的后缀集合. 要注意的是, 字符串本身并不是自己的后缀.

有了这个定义, 就可以说明 PMT 中的值的意义了. PMT 中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度. 例如, 对于 aba, 它的前缀集合为 {"a", "ab"}, 后缀集合为 {"ba", "a"}. 两个集合的交集为 {"a"}, 那么长度最长的元素就是字符串 a 了, 长度为 1, 所以对于 aba 而言, 它在 PMT 表中对应的值就是 1. 再比如, 对于字符串 ababa, 它的前缀集合为 {"a", "ab", "aba", "abab"}, 它的后缀集合为 {"baba", "aba", "ba", "a"}, 两个集合的交集为 {"a", "aba"}, 其中最长的元素为 aba, 长度为 3.

好了, 解释清楚这个表是什么之后, 我们再来看如何使用这个表来加速字符串的查找, 以及这样用的道理是什么. 如图 1.12 所示, 要在主字符串 ababababca 中查找模式字符串 abababca. 如果在 j 处字符不匹配, 那么由于前边所说的模式字符串 PMT 的性质, 主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的. 这是因为主字符串在 i 位失配, 也就意味着主字符串从 i−j 到 i 这一段是与模式字符串的 0 到 j 这一段是完全相同的. 而我们上面也解释了, 模式字符串从 0 到 j−1 , 在这个例子中就是 ababab, 其前缀集合与后缀集合的交集的最长元素为 abab, 长度为 4. 所以就可以断言, 主字符串中 i 指针之前的 4 位一定与模式字符串的第 0 位至第 4 位是相同的, 即长度为 4 的后缀与前缀相同. 这样一来, 我们就可以将这些字符段的比较省略掉. 具体的做法是, 保持 i 指针不动, 然后将 j 指针指向模式字符串的 PMT[j −1] 位即可.

简言之, 以图中的例子来说, 在 i 处失配, 那么主字符串和模式字符串的前边 6 位就是相同的. 又因为模式字符串的前 6 位, 它的前 4 位前缀和后 4 位后缀是相同的, 所以我们推知主字符串 i 之前的 4 位和模式字符串开头的 4 位是相同的. 就是图中的灰色部分. 那这部分就不用再比较了.

file

有了上面的思路, 我们就可以使用 PMT 加速字符串的查找了. 我们看到如果是在 j 位失配, 那么影响 j 指针回溯的位置的其实是第 j −1 位的 PMT 值, 所以为了编程的方便, 我们不直接使用 PMT 数组, 而是将 PMT 数组向后偏移一位. 我们把新得到的这个数组称为 next 数组. 下面给出根据 next 数组进行字符串匹配加速的字符串匹配程序. 其中要注意的一个技巧是, 在把 PMT 进行向右偏移时, 第 0 位的值, 我们将其设成了 -1, 这只是为了编程的方便, 并没有其他的意义. 在本节的例子中, next 数组如下表所示.

file

具体的程序如下所示:

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

好了, 讲到这里, 其实 KMP 算法的主体就已经讲解完了. 你会发现, 其实 KMP 算法的动机是很简单的, 解决的方案也很简单. 远没有很多教材和算法书里所讲的那么乱七八糟, 只要搞明白了 PMT 的意义, 其实整个算法都迎刃而解.

现在, 我们再看一下如何编程快速求得 next 数组. 其实, 求 next 数组的过程完全可以看成字符串匹配的过程, 即以模式字符串为主字符串, 以模式字符串的前缀为目标字符串, 一旦字符串匹配成功, 那么当前的 next 值就是匹配成功的字符串的长度.

具体来说, 就是从模式字符串的第一位 (注意, 不包括第 0 位) 开始对自身进行匹配运算. 在任一位置, 能匹配的最长长度就是当前位置的 next 值. 如下图所示.

file

file

file

file

file
求 next 数组值的程序如下所示:

void getNext(char * p, int * next) {
    next[0] = -1;
    int i = 0, j = -1;

    while (i < (int)strlen(p)) {
        if (j == -1 || p[i] == p[j]) {
            ++i;
            ++j;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

至此, KMP 算法就全部介绍完了.

file

本文来自:https://blog.duhbb.com

本文链接地址:【转载】如何更好地理解和掌握 KMP 算法?,英雄不问来路,转载请注明出处,谢谢。

有话想说:那就赶紧去给我留言吧。

rainbow

这个人很懒,什么都没留下

文章评论