拆解并实现KMP算法

KMP算法的作用和优势?

KMP算法是高效字符串匹配算法,用于在主串中快速查找子串位置。其核心思想是通过预处理子串,得到子串的前缀表,在某个位置发现匹配错误时,避免重头匹配,而是根据已匹配的部分子串,找到可以对应回子串头部的最长长度位置开始重新匹配。

举个例子:主串aabaabaaf,子串aabaaf,在主串找到完整子串出现的位置

在暴力解法中,使用双循环来匹配:你的指针一从主串下标0开始一个个同子串的指针二比较,匹配到下标5发现b≠f,主串的指针一又灰溜溜从下标1开始,子串回退到下标0,重新逐一比较...

而在KMP算法中,我们不需要将主串的指针一回退,而是只将子串的指针二回退到一个合适的位置,在这个位置,保留了当前子串已匹配的部分 aabaa 的最长相等前后缀即 aa,意味着在我们之前匹配的部分的尾部(即后缀)和头部(即前缀)有重合的部分aa,那我们将指针二指到下标2:b,继续与指针一的下标5:b比较即可。

如何理解前缀、后缀、最长相等前后缀

根据上文,我们知道在KMP中,无需回退主串的匹配指针,移动的只有子串的匹配指针,所以我们寻找前后缀的目标字符串永远是子串,并且是子串已匹配的部分。

前缀定义:字符串中包含 字母而不包含尾字母的全部子序列

后缀定义:字符串中包含 字母而不包含首字母的全部子序列

上面我们已经顺利匹配好了 aabaa,对它寻找前后缀的过程如下:

  • 前缀: a, aa, aab, aaba (-->方向截取)
  • 后缀: a, aa, baa, abaa (<--方向截取)

那么它们到底代表了什么呢?笔者可以给出最形象的解释是:

匹配完整子串的过程中,可以将子串拆成 '已匹配的头部' 和 '待匹配的尾部' 两部分

后缀列表,就是在已完成的匹配头部中从后往前组合,列出可选择使用的全部新头部

而前缀列表代表着完整子串从前往后拆解出的全部头部可能性

只有头部严格匹配成功并尽可能长,我们才可以将子串指针回退,并尽量减少下次重新匹配尾部的工作量,这就是为什么我们要寻找最长相等前后缀

建立前缀表及使用(理论)

在本题中,我们就是通过前缀表来记录各个情况下的最长相等前后缀,来指引我们遇到不匹配的情况时,我们将子串的匹配指针回退到哪里是最节省工作量的。

前缀表用数组来存放,我们将其命名为prefix。

我们先来捋捋对于上文给出的例子的前缀表建立过程:

题目:主串aabaabaaf,子串aabaaf,在主串找到完整子串出现的位置

想要强调:建立前缀表其实是根据子串独立进行的运算过程!!与主串是没有关系的,只要是查询同一条子串,总是用一样的前缀表!

那么根据上文提到的匹配完整子串的过程中,可以将子串拆成 '已匹配的头部' 和 '待匹配的尾部' 两部分,下面将'已匹配的头部'记作s。来看子串aabaaf的前缀表建立过程:

  • 当s为a:根据前缀、后缀定义发现前后缀都为空,故最长相等前后缀为0,记录prefix[0]=0
  • 当s为aa:前缀列表为[a],后缀列表为[a],最长相等前后缀为1,prefix[1]=1
  • 当s为aab:前缀列表为[a,aa],后缀列表为[b,ab],最长相等前后缀为0,prefix[2]=0
  • 当s为aaba:前缀列表为[a,aa,aab],后缀列表为[a,ba,aba],最长相等前后缀为1,prefix[3]=1
  • 当s为aabaa:前缀列表为[a,aa,aab,aaba],后缀列表为[a,aa,baa,abaa],最长相等前后缀为2,prefix[4]=2
  • 当s为aabaaf:前缀列表为[a,aa,aab,aaba,aabaa],后缀列表为[f,af,aaf,baaf,abaaf],最长相等前后缀为0,prefix[5]=0

如上,得到prefix=[0,1,0,1,2,0]

得到了前缀表,怎么用?

再次回忆一下前缀表里的数值代表什么:prefix[i]代表着在已匹配好前i+1个字符的情况下,重新匹配应回到的下标是prefix[i]。比如子串aabaaf,prefix[4]代表我们已匹配前5个字符aabaa,但在匹配到f时错误,此时我们需要将指针回退到prefix[4]=2,即从下标2:字符b再次与主串比较

注意,上面说回退的下标为prefix[i],因为最长相等前后缀代表的是长度,而我们应该从已匹配好的长度的后一位开始重新比较,因此正好不用做额外的+1处理,直接使用prefix[i]作为下标就好

代码实现建立前缀表(JS)

首先确认一下我们在找前缀表的理论梳理中,需要明确的变量有:子串、代表前缀表的数组、前缀、后缀,在代码实现中,我们也要捋清这几个变量间的逻辑。上代码:

JavaScript 复制代码
const getPrefix = (needle) => { //传入子串
        let prefix = []; //建立needle的前缀表
        let j = 0; //前缀末尾位置,兼当前已匹配的最长长度!
        prefix[0] = j; //初始化,已匹配部分只有一位时,前后缀都是空,所以长度为0

        for (let i = 1; i < needle.length; ++i) { //后缀末尾位置,兼当前需要处理的字符串末尾位置!因为后缀不包含首字母,所以从下标1开始
            while (j > 0 && needle[i] !== needle[j]) //注意这里是while!!处理前后缀的末尾不一样(即不匹配)的情况,j>0是为了防止j-1越界
                j = prefix[j - 1]; //j除了代表前缀末尾位置,也代表着当前已匹配的最长长度,既然出现了不匹配,那我们利用前缀表的属性,再次让前缀的位置回退,直到匹配成功/前缀不能再缩短为止
            if (needle[i] === needle[j]) //匹配成功
                j++; //已匹配的最长长度+1,也代表前缀末尾向后移一位,尝试下次匹配更长的后缀
            prefix[i] = j; //前i+1个字符的最长相等前后缀已找到,存入prefix
        }

        return prefix; //返回前缀表
    }

完整KMP代码实现

JavaScript 复制代码
/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var kmp = function (haystack, needle) {
    if (needle.length === 0)
        return 0;

    const getPrefix = (needle) => {
        let prefix = [];
        let j = 0; 
        prefix[0] = j; 

        for (let i = 1; i < needle.length; ++i) { 
            while (j > 0 && needle[i] !== needle[j]) /
                j = prefix[j - 1]; 
            if (needle[i] === needle[j]) 
                j++; 
            prefix[i] = j; 
        }

        return prefix; 
    }

    let prefix = getPrefix(needle);
    let j = 0; //可活动的子串匹配指针
    for (let i = 0; i < haystack.length; ++i) { //一直从前往后移动的主串匹配指针
        while (j > 0 && haystack[i] !== needle[j])
            j = prefix[j - 1];
        if (haystack[i] === needle[j])
            j++;
        if (j === needle.length) //已匹配长度达到子串长度,代表已完成
            return (i - needle.length + 1); //返回匹配到的首字母下标
    }

    return -1;
};

参考文献

代码随想录:实现strStr()

相关推荐
刚入坑的新人编程8 分钟前
暑期算法训练.3
c++·算法
平哥努力学习ing20 分钟前
C语言内存函数
c语言·开发语言·算法
H_HX_xL_L28 分钟前
数据结构的算法分析与线性表<1>
数据结构·算法
xienda29 分钟前
数据结构排序算法总结(C语言实现)
数据结构·算法·排序算法
科大饭桶29 分钟前
数据结构自学Day8: 堆的排序以及TopK问题
数据结构·c++·算法·leetcode·二叉树·c
minji...30 分钟前
数据结构 栈(2)--栈的实现
开发语言·数据结构·c++·算法·链表
zh_xuan34 分钟前
c++ 模板元编程
开发语言·c++·算法
木子.李3471 小时前
记录Leetcode中的报错问题
算法·leetcode·职场和发展
方方土3331 小时前
题解:CF1829H Don‘t Blame Me
数据结构·算法·图论
达文汐1 小时前
【中等】题解力扣22:括号生成
java·算法·leetcode·深度优先