LeetCode28 KMP算法实例

题目描述:

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。

示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2

示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1

这道题我在自己做的时候,使用了“自以为是”的“双指针”方法,等到浅显的了解了下KMP算法后,发现我的所谓双指针算法只是表面上的,实际上还是里外两次遍历,具有很高的时间复杂度。这里直接上原来的代码,就不多做解释了。

int strStr(string haystack, string needle) {                                                                                                                                                           

        int slow = 0, fast = 0;
        int len = needle.length();

        if (needle.length() > haystack.length()) return -1;

        while (slow <= haystack.length() - len)
        {
            if (haystack[slow] != needle[0])
            {
                slow++;
                fast++;
            }
            else {
                int count = 0;
                int f = fast;
                for (fast; fast < f + len; fast++)
                {
                    if (haystack[fast] == needle[count])
                        count++;
                }
                if (count == len)
                    return slow;

                slow++; //匹配不成功,slow向前走一位
                fast = slow;//fast跟上,重新开始while循环
            }
        }

        return -1;
    }

可以发现,在我上面自己写的代码中,每当匹配不成功的时候,我选择了从haystack字符串中下一个位置的字符开始判断是否为needle的首字符。也就是,在这个算法里,上次失败的匹配对于这次的匹配来说,除了让slow++(也就是从失败的下一位重新开始找),并没有其它的帮助。换个意思说,本次的匹配并没有吸收上一次匹配的经验,一切都是从头开始。俗话说“失败乃成功之母”,那么如何让“失败”更加有意义呢?让我们带着这样的疑问进入KMP算法的学习。

KMP算法

这段我是跟着代码随想录(Carol)和木子喵neko学的。可能是因为这是卡哥早期视频,讲解的思维比较跳跃,看了好几遍没看懂,最后在b站上发现了个可爱的白毛vtuber才略懂了一二(才不是因为喜欢白毛)。所以以下多多少少会有二人的影子,请见谅。

该算法是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。KMP正是分别取他们三人的姓氏首字母命名的。

KMP算法要解决的问题,就是如这个题目所说的要在haystack(也叫文本串)中寻找needle(也叫模式串)。其经典思想就是:当出现字符串不匹配时,可以记录一部分已经匹配的内容,避免从头开始匹配

在讲解大致思路前,我们先看几个概念。

前后缀

对于一个字符串而言,任何一个连续的、包括首字符但不包括尾字符的子字符串称为该字符串的一个前缀。同理,任何一个连续的、包括尾字符但不包括首字符的子字符串称为该字符串的一个后缀

例如,对于字符串 s="aabaaf",其前缀字符串集合为{a,aa,aab,aaba,aabaa};其后缀字符串集合为{f,af,aaf,baaf,abaaf}。

最长公共前后缀

最长公共前后缀,代码随想录卡哥把它称为最长相等前后缀,意思就是在前后缀中完全相同的子字符串。很明显,对于上述例子而言,其最长公共前后缀为aa,其最大长度为2。

前缀表(重点)

所谓前缀表,就是记录模板串从needle[0]开始的所有子串的最长公共前后缀。对于例子s="aabaaf",其所有符合条件的子串为{a,aa,aab,aaba,aabaa,aabaaf},则其对应的前缀表为{0,1,0,1,2,0}。注意:因为前缀表第0位所对应的子串一定只有一个字符,所以所有前缀表的第0位一定为0。

那么,前缀表的作用是什么呢?

还记得kmp的核心思想吗?前缀表正是用来记录已经匹配的内容。

举个例子:对于模板串aabaaf和文本串aabaabaaf,之后都用这个了,懒得一遍一遍写了。一开始当然是和暴力解法一样从头开始匹配,但是当匹配到b和f的时候,发生冲突了,不匹配了,如果按照暴力解法,我们会把模板串向后移动一位,然后重新开始匹配,就此循环。而kmp算法在发生冲突的时候,会选择向后回溯,回溯到哪呢?这就用到我们提前做好的前缀表了。

LeetCode28 KMP算法实例_第1张图片

存储每个子字符串的最长公共前后缀的意义就在于,一旦发生冲突,kmp算法会去想:呀,发生冲突了,但是我不想重新开始,让我看看已经匹配正确的子字符串的屁股有没有能让我当做脑袋用的?省得我重新开始找了。这个屁股和脑袋,就是子字符串的后缀与前缀(子字符串的前缀其实也是模板串的一个前缀)(这个子字符串其实是位于文本串中的),而发生冲突的位置的前一位所对应的前缀表里面正好就是已经匹配正确的子字符串的最长公共前后缀的长度,而长度是从1开始,位置是从0开始,这样我们就可以知道在下一次循环中模板串应该开始的位置。如下图:

LeetCode28 KMP算法实例_第2张图片

我这说得不太清楚,推荐去看我说的那两位up

KMP算法实操

求next数组(重点)

所谓next数组,其实就是记录当匹配失败时指针应该回溯的位置。虽然子字符串是在文本串而非模板串上,但是本质还是一样的,next数组就是needle模板串的前缀表。

next数组有很多种存储方式,但归根结底还是+1-1习惯的问题,我就直接用前缀表当next数组啦。

在getNext(int*next,string needle)函数里,我需要定义两个指针i、j,其分别代表子字符串后缀的末位和前缀的末位。先进行初始化:

int j = 0;//前缀当然从0位置开始

next[0] = 0;//前缀表第一个元素必定为0

for(int i = 1; i < needle.size(); i++)//后缀是不能包含首字母的

{ //这里之后再填东西 }

在每个for循环里,我就要开始看看这次子字符串的前后缀是否相同了,如果不相同,说明遇见冲突了,在kmp里面遇见冲突的方法是让j回溯到前一个字符串所对应的前缀表中的位置,即j=next[j-1,]然后重新开始匹配,请注意,整个不相同情况也是套在一个while(j>0&&s[i]!=s[j])循环里的,因为回溯一次后之后的匹配不一定,或者说很小概率能够完全重合,仅仅回溯一次是不够的,要么回到0处,要么找到匹配进入相同情况的算法;如果相同,则j++;到最后,让next[i] = j;具体代码如下:

void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
                j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }

经此,我们便得到了依据模板串needle所创造的前缀表——next;

寻找匹配字符串

在得到next数组后,我们终于可以开始进行寻找匹配字符串的操作了。在文本串中开始捣鼓,和暴力算法一样用个i的for循环,然后用while比啊比,如果是相同的,j就继续加,如果不同了就开始回溯,直到相同。最后再把j跟needle的length比一下,如果符合条件就返回j,如果不符合就return-1,成了。这里比较简单,上代码。

 int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
        int next[needle.size()];
        getNext(next, needle);
        int j = 0;
        for (int i = 0; i < haystack.size(); i++) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }

整体代码如下:(卡哥的)

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

这道题算是初见KMP,大受震撼,这篇文章同样也是我在csdn的第一篇文章,值得纪念。

你可能感兴趣的:(LeetCode)