代码随想录算法训练营第九天| Leetcode 28.找到字符串第一个匹配项的下标、459.重复的子字符串

一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。

                                                                                                                             ——KMP

目录

Leetcode 28.找到字符串第一个匹配项的下标

Leetcode 459.重复的子字符串


Leetcode 28.找到字符串第一个匹配项的下标

题目链接:Leetcode 28.找到字符串第一个匹配项的下标

题目描述:给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。简单来说就是实现库函数strstr()。

思路:首先想到的肯定是两层循环暴力匹配,只要找不到就重新匹配。不过暴力匹配的时间复杂度是O(n^2),有没有一种方法可以利用已经匹配过的信息来减少匹配次数,进而降低时间复杂度呢?这种方法就是著名的KMP算法。KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

KMP算法的关键在于理解next数组(前缀表)的含义:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

next数组存储内容的作用为:如果当前字符匹配失败,我需要回退到下标为几的位置重新匹配。举个例子,next[ i ]=j 代表的含义是如果第i个字符匹配失败,下一次匹配要从下标为j的位置重新匹配,直到字符匹配成功或下标退到为0。

说了这么多,接下来就该思考如何求next数组。构造next数组其实就是计算needle前缀表的过程。

求解next数组步骤如下:

(1)初始化。其中,长度为0的最大相同前缀后缀为0(也就意味着当回退到第一个字符时,即使再回退,也要回到0,其实就相当于不变),由于i=0的情况考虑过了,因此i从1开始。

(2)处理前后缀不相同的情况。(当前长度字符串前后缀不相等时,就看比当前字符串少一个字符的前缀和,类似于一种递归思想)

(3)处理前后缀相同的情况。

代码如下:

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

以上代码不仅仅只有这一种写法,不过大同小异,具体可以看这篇文章:代码随想录

匹配过程和求解next数组过程类似。

由于要返回haystack下标,因此匹配成功后返回值为i-needle。size()+1

代码如下:

for (int i = 0, j = 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;

最后贴上整体代码:(前缀表不减1)

class Solution {
public:
    void getNext(int* next, const string& s) {
        next[0] = 0;
        for (int i = 1, j = 0; 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);//求解next数组过程
        //匹配过程
        for (int i = 0, j = 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;
    }
};
  • 时间复杂度: O(n + m)
  • 空间复杂度: O(m)

代码是写完了,那时间复杂度是怎么求出来的呢?无论在预处理过程还是查询过程中,虽然匹配失败时,指针会不断地根据 next 数组向左回退,看似时间复杂度会很高。但考虑匹配成功时,指针会向右移动一个位置,这一部分对应的时间复杂度为 O(n+m)。又因为向左移动的次数不会超过向右移动的次数,因此总时间复杂度仍然为 O(n+m)。

Leetcode 459.重复的子字符串

题目链接:Leetcode 459.重复的子字符串

题目描述:给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

思路:Kmp的经典应用就是在一个串中找另一个串,同样,对于在一个串中找字串也是合适的。

第一种方法:KMP

这道题求next数组的方法和上面那道题一样Leetcode 28.找到字符串第一个匹配项的下标,就不多解释了,如果看了上面的讲解还是不太理解可以看这里:代码随想录

接下来思考一个问题:最长相同前后缀和重复子串的关系又有什么关系呢?

先回顾一下前、后缀的定义:

  • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
  • 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

因此我们可以得出一个结论:在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。那这个结论是怎么求出来的呢?

代码随想录算法训练营第九天| Leetcode 28.找到字符串第一个匹配项的下标、459.重复的子字符串_第1张图片

步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。

步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。

步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。

步骤四:循环往复。

所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。

注:推导过程及图片来源于《代码随想录》

代码如下:(KMP)

class Solution {
public:
    void getNext(int* next, const string& s) {
        next[0] = 0;
        for (int i = 1, j = 0; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    bool repeatedSubstringPattern(string s) {
        if (s.size() == 0) {
            return false;
        }
        int next[s.size()];
        getNext(next, s);
        int len = s.size();
        //如果len % (len - (next[len - 1] + 1)) == 0 ,
        //则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,
        //说明该字符串有重复的子字符串。
        if (next[len - 1] != 0 && len % (len - next[len - 1]) == 0) {
            return true;
        }
        return false;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

第二种方法:字符串匹配

这个思路可以学习一下:我们将两个 s 连在一起,并移除第一个和最后一个字符。如果 s 是该字符串(s+s)的子串,那么 s 就满足题目要求。至于证明过程,459. 重复的子字符串题解区有严谨的数学证明。

代码如下:(字符串匹配)

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        return (s+s).find(s,1)!=s.size();
    }
};

总结:KMP算法的关键在于理解next数组的作用,以及next数组元素的值代表的含义。

最后,如果文章有错误,请在评论区或私信指出,让我们共同进步!

你可能感兴趣的:(算法,leetcode,c++)