每天一道LeetCode-----KMP算法查找子串,重新实现strStr()函数

Implement strStr()

原题链接Implement strStr()

这里写图片描述
子串查找,方法很多,可以用string内置的接口find解决,这里主要复习一下kmp算法


kmp算法常用于字符串匹配,相比于传统方式一个一个查找,当遇到不匹配时从头开始的土方法,kmp可以有效减少比较次数,遇到不匹配时,不需要从头开始

判断abababc中是否存在ababc子串,直观上是存在的
abababc为源字符串sourceababc为目标字符串target
传统方式

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
a   b   a   b   c           target
0   1   2   3   4           target下标

依次比较source[0]target[0]source[1]target[1]source[4]target[4]
当比较到source[4]target[4]时,发现字符a和字符c不相等,此时需要从头第二个字符开始

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
    a   b   a   b   c       target
    0   1   2   3   4       target下标

比较source[1]target[0]时就不相等,此时需要从第三个字符开始

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
        a   b   a   b   c   target
        0   1   2   3   4   target下标

依次比较source[2]target[0]source[3]target[1]source[6]target[4]
发现终于相等了,说明有子串存在,但是每次失配后都从下一个字符开始从头查找,每次都需要将target所有字符都比较一次,效率堪忧啊…


kmp算法

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
a   b   a   b   c           target
0   1   2   3   4           target下标

依次比较source[0]target[0]source[1]target[1]source[4]target[4]
当比较source[4]target[4]时不相等,于是…

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
        a   b   a   b   c   target
        0   1   2   3   4   target下标

直接比较source[4]target[2],发现相等,继续比较source[5]target[3]source[6]target[4],发现存在子串
在比较时,发现target的前两个字符ab在第二次开始时根本没有比较,是直接从第三个字符开始的。

直观的看,当比较source[4]target[4]时,source[0 : 3]肯定和target[0 : 3]相等,也就是说比较a和c时前面4个字符abab是匹配的,又因为target[0 : 1]target[2 : 3]相等,那么和target[2 : 3]匹配的source[2 : 3]肯定也和target[0 : 1]匹配,那么就可以直接将target后移两位,让source[2 : 3]匹配target[0 : 1],从target[2]开始比较,也就直接让source[4]target[2]比较


所以,在失配时移动target而非source,这就需要对target进行预处理以便在失配时直到该移动多少,kmp将预处理后的数据存放在一个prefix数组中,这里成为前缀数组,首先理解什么是字符串中每个字符的前缀
假设当前字符为target[j],存在i < j满足target[0 : i] == target[j - i : j],此时target[0 : i]为字符target[j]的前缀,而i + 1为target[j]的前缀个数
比如说ababaca字符串

target[0] == 'a',第一个字符,前缀个数是0,前缀为空
target[1] == 'b',没有一个i满足target[0 : i] == target[1 - i : 1],前缀个数是0,前缀为空
target[2] == 'a',target[0 : 0] == target[1: 1],所以i为0,此时前缀个数为1,前缀为"a"
target[3] == 'b',target[0 : 1] == target[2 : 3],所以i为0,此时前缀个数为2,前缀为"ab"
target[4] == 'a',target[0 : 2] == target[2 : 4],所以i为2,此时前缀个数为3,前缀为"aba"
target[5] == 'c',没有一个i满足target[0 : i] == target[5 - i : 5],此时前缀个数为0,前缀为空
target[6] == 'a',target[0 : 0] == target[6 : 6],所以i为0,此时前缀个数为1,前缀为"a"

在对目标字符串预处理时,本质就是计算每个字符的前缀个数,前缀的意思在于,若target[i]的前缀个数是n,那么有target[0 : n - 1] == target[i - n + 1, i],也就是说如果target[i+1]souce[j]比较时失配,可以直接将target[0 : n - 1]移动到target[i - n + 1, i]的位置,而下次开始直接从target[n]source[j]开始比较。
对于最开始的那个例子,abababc为源字符串sourceababc为目标字符串target ,匹配之前需要对target进行预处理,得到每个字符对应的前缀个数

0   1   2   3   4   下标
a   b   a   b   c   target
0   0   1   2   0   前缀个数

接着开始依次比较

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source
a   b   a   b   c           target
0   1   2   3   4           target下标
0   0   1   2   0           前缀个数

当比较到失配时,这里source[4] != target[4]导致失配,找到target[4 - 1]target[3]的前缀个数2,也就是说target[0 : 1] == target[2 : 3],所以可以直接将target[0 : 1]移动到target[2 : 3]的位置

0   1   2   3   4   5   6   source下标
a   b   a   b   a   b   c   source

a   b   a   b   c           target
0   1   2   3   4           target下标

        a   b   a   b   c   target
        0   1   2   3   4   target下标
        0   0   1   2   0   前缀个数

下面接着比较target[2]source[4]即可,这样,失配时一次性移动了2步,而不需要每次都只移动1步,当字符串很大时,target重复字符过多,前缀个数比较大时,效果更加明显
代码如下

class Solution {
public:
    int strStr(string haystack, string needle) {
        if(needle.size() == 0)
            return 0;

        std::vector<int> prefix(needle.size(), 0);
        handlePrefix(prefix, needle);
        /* 
         * 和生成prefix的过程很像
         * 这里lp记录着needle当前遍历到的位置
         * 如果不相等,就改变lp,把needle[lp-1]的前缀个数的那么多元素后移到lp前面
         */
        int lp = 0;
        for(int i = 0; i < haystack.size(); ++i)
        {
            while(lp > 0 && haystack[i] != needle[lp])
                lp = prefix[lp - 1];
            if(haystack[i] == needle[lp])
                ++lp;
            if(lp == needle.size())
                return i - lp + 1;
        }
        return -1;
    }
private:
    void handlePrefix(std::vector<int>& prefix, string needle)
    {
        /*
         * lp
         * 记录着上一个字符needle[i-1]的前缀个数。
         * 假设needle[i-1]的前缀个数是n,那么needle[0, n-1] == needle[i-1-n : i-1]
         * 那么对于needle[i]而言,因为needle[i-1]的前缀个数是n,到达needle[0, n-1],
         * 那么,首先它的前缀个数的很可能是n+1,所以直接比较needle[i]和needle[lp]
         * lp是个数,对应下标刚好是第lp + 1个字符
         * 如果不相等,就需要减小前缀个数,根据needle[lp-1]的前缀个数重复进行
         */
        int lp = 0;
        /* i记录着当前遍历的目标字符串的位置 */
        for(int i = 1; i < needle.size(); ++i)
        {
            while(lp > 0 && needle[i] != needle[lp])
                lp = prefix[lp - 1];
            if(needle[i] == needle[lp])
                ++lp;
            prefix[i] = lp;
        }
    }
};

你可能感兴趣的:(LeetCode,leetcode)