KMP算法总结

KMP算法

今天做了LeetCode上一道题,提到了KMP算法,就去了解了一下,理解起来有点困难。可以看下面我贴出来的两篇文章,我的文章只是自己总结一下。(可以先看这两篇文章,关于前缀表那块我写的不太好)

KMP算法(一)(对比暴力匹配算法)

KMP算法(研究总结,字符串)

KMP算法的作用是在A串中查找是否有B串的算法,时间复杂度为O(n+m)。

原理是利用上次匹配的信息,这次匹配并不是从上一次A串中开始匹配的下一个位置开始,而是直接从上一次匹配的不匹配位置开始,且不是从头匹配B串,而是匹配B串中还未匹配的后续部分串。

例如A=aabaabaafa,B=aabaaf,从头开始匹配,当匹配到A中第二个b时发现该子串与B不相等,如果是暴力求解,这个是否我们就又从A中的第二个a开始与B从头匹配,但我们观察可以发现,A中不匹配的第二个b前面是aa,这个aa与B的开头相等,所以我们可以直接用第二个b去匹配B中的aa后面的b,这样就跳过了A中的一些元素,降低了时间复杂度。(这样做是完备的,因为如果在这种情况下如果从第二个元素开始能匹配,那么从第一个元素也可以匹配,可以自己举个例子看看,主要第一个不匹配的元素前都是匹配的元素,如果错位能匹配的话,说明这些元素都相同,也不用太关注这句)

我们要做的是当出现不匹配时,继续从这个不匹配的元素开始比较,关键就在于怎么知道从B中哪个元素开始匹配。

我们要跳过B中前面一些串,说明不匹配元素前面部分串与B中后面部分相同,且不匹配元素前面部分串与B中前面部分串相同,即B中后面部分与B中前面部分串相同,我们要找的就是前后相同的有多长,由此得出该从B的哪里匹配。

插叙一个知识:这里就引入最长公共前后缀,前缀是指以第一个元素开头,不包含最后一个元素的串,如aabb的前缀是a,aa,aab;后缀是指不以第一个元素开头,以最后一个元素结尾的串。如aabb的后缀的abb,bb,b。最长公共前后缀是指前缀与后缀相等的串中最长的前缀与后缀。如aaa,前缀有a,aa,后缀有aa,a,最长公共前后缀就是aa。

我们继续回到A与B匹配的话题,前面说过我们现在要解决的问题是知道该从B的哪里匹配起,而引入最长公共前后缀就是为了解决这个问题,还是以A=aabaabaafa,B=aabaaf距离,当第一次不匹配后,B中现在不匹配的是f,于是我们可以去看除去f前面串的最长公共前后缀的长度,即aabaa的长度,为2,所以这个时候我们可以从B中第2位置(位置从0开始)开始匹配,为什么?因为f前面的aabaa最长公共前后缀长度为2,说明aabaa后面部分与前面部分最长相等的部分为2,而后面部分又与A中不匹配字符(这里是第二个b)前面的部分串相同,即A中不匹配字符前面串与B中前面部分最长相等的长度为2,所以下一次匹配从A中不匹配的字符与B中第2位置开始。

OK,总结一下到目前为止我们得到的结论,查找A串中是否有与B串相等的子串,我们可以用KMP算法,KMP算法原理是A中这一次匹配并不是从上一次匹配的下一个位置开始,也不是从头匹配B串,而是从A中不匹配的字符开始,同时B串中是从不匹配字符前面串的最大前后缀和位置开始。之所以能这样做是因为A中不匹配字符的前面部分串与B中不匹配字符前面部分串相同,而B中不匹配字符前面部分串又与B中开头的部分串相同,最长的相同的串就是最长公共前后缀。

其实KMP算法主要原理就是上面那段话了,现在就一个问题了,那就是求出B中每个串的最长前后缀长度以方便匹配的时候用。我们一般把存储B中每个串的最长前后缀长度的数组叫做前缀表或next表。

next表的构造首先是初始化,当串只有一个元素最长公共前后缀长度肯定为0,然后再是求后面。怎么求后面的串对应的长度呢,我们还是以B=aabaaf举例子,(现在规定最长公共前后缀长度叫做前缀值),以i的值代表当前求的位置(i从0开始),例如i现在为3,指向第三个a,那怎么求aaba的最长公共长度,利用前面一个串的信息,前面一个串的最长公共长度我们已经求出来了,我们可以直接把当前元素添加到前一个串的最长公共后缀,然后看现在这个后缀是否与之前的最长公共前缀加其后的一个元素相同,即直接比较当前元素和之前一个串的最长公共前缀的其后一个元素,而之前一个串的最长公共前缀的其后一个元素位置就是 i 前一个元素对应的前缀值。 i 现在为3,前一个串是aab,b对应前缀值为0,所以a就与位置为0的元素比较。

若相同,那最好,则当前元素对应前缀值为前一个元素对应前缀值加1.若不相同,则找当前比较的元素前一个元素对应的前缀值对应位置的元素比较,有点绕口,例如 i 当前为5,指向 f ,前一个元素a对应前缀值为2,所以 f 与第2个元素(从0开始)b比较,不相同,而b前一个元素a前缀值为1,那就与位置为1的a比较,不相同,继续重复这个过程直至边界。为什么不相同的时候可以通过这样比较来判断前缀值呢,说起来有点复杂,既然前一个元素对应的最长后缀加上当前元素不行,那就去找第二长的后缀加上当前元素,第二长的后缀在哪里呢,就是当前比较元素的前一个元素对应的前缀值。为什么,当前比较元素前一个前缀值代表以当前比较元素结尾的串最长公共前后缀长度,即该前缀值对应的前面部分的串与后面部分的串相同,而后面部分的串又与以当前元素结尾的部分串相同,所以我们直接比较当前元素与该前缀值对应位置的元素就能求出当前元素的前缀值,有点麻烦,我也说不太清楚。

而在具体实现中我们常常把每个元素对应的前缀值减1,这样做是为了更好判断边界情况和使得前缀值对应位置就是相应最长前缀结尾的位置。例如aabaaf,第一个 a 前缀值现在为-1,说明a这个串对应最长前缀以位置-1的元素结尾,就是没有这个前缀,可以不用再继续比较,而要比较的时候我们用前缀值加1对应的元素比较。综上,求next表的代码如下:

void getNext(int *next, const string &s) {
    // 初始化,第一个元素前缀值为-1,当前要比较的元素位置为1(从0开始)
    next[0] = -1;
    // j表示当前计算元素的前一个元素对应的前缀值
    int j = -1;
    // 循环依次求解
    for (int i = 1; i < s.length(); i++) {
        // 开始比较直至相等或j 变为 -1说明只有和第一个元素比较了
        while (j >= 0 && s[i] != s[j + 1]) {
            j = next[j];
        }
        // 如果比较结果相等,则在当前前缀值基础上加1
        if (s[i] == s[j + 1]) {
            j++;
        }
        next[i] = j;
    }
}

这里我们具体以一道题来作为例子。

28. 实现 strStr()

在这里插入图片描述

class Solution {
public:
    void getNext(int *next, const string& s) {
        // 第一个元素前缀值初始化为-1
        next[0] = -1;
        // j代表当前比较元素的位置减1
        int j = -1;
        // 从第一个元素开始求(序号从0开始)
        for (int i = 1; i < s.length(); i++) {
            // 如果比较到第一个元素或者比较到相等了就停止,否则一直寻找要比较的元素
            while (j >= 0 && s[i] != s[j + 1]) {
                j = next[j];
            }
            // 如果相等j加1作为前缀值
            if (s[i] == s[j + 1]) {
                j++;
            }
            next[i] = j;
        }
    }

    int strStr(string haystack, string needle) {
        int lenH = haystack.length(), lenN = needle.length();
        int *next = new int [lenN];
        getNext(next, needle);
        // j代表needle串中要比较元素的前一个
        int j = -1;
        for (int i = 0; i < lenH; i++) {
            // 比较找到要在needle串中比较的位置
            while (j >= 0 && haystack[i] != needle[j + 1])
                // 我们每次更新j使用的是前一个元素对应的前缀值,即next[j]
                j = next[j];
            // 如果相等则两个串比较的位置都加1
            if (haystack[i] == needle[j + 1])
                j++;
            // 如果needle串比较完毕则可以成功返回
            if (j + 1 == lenN)
                return (i - lenN + 1); 
        }
        return -1;
    }
};

差不多KMP算法就这样吧,虽然感觉原理还是有点模模糊糊的,但是能写出来了。

再增加一个KMP算法的例子

459.重复的子字符串

在这里插入图片描述直接看代码吧:

/**
* 解题思路:如果该字符串是由子串重复多次构成的,那么该串减去最长公共前缀后得到的串一定就是那个子串,
* 而由于该字符串由子串构成,那么该字符串的长度一定就是子串的倍数,所以可以用KMP算法得到最长公共前缀长度
* 再得到子串长度,判断是否为倍数。
*/

class Solution {
public:
    void getNext(int *next, const string &s) {
        next[0] = -1;
        int j = -1;
        for (int i = 1; i < s.size(); i++) {
            while (j >= 0 && s[i] != s[j + 1])
                j = next[j];
            if (s[i] == s[j + 1])
                j++;
            next[i] = j;
        }
    }

    bool repeatedSubstringPattern(string s) {
        int len = s.size();
        int next[len];
        getNext(next, s);
        if (next[len - 1] != -1 && len % (len - next[len - 1] - 1) == 0)
            return true;
        return false; 
    }
};

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值