代码随想录算法训练营day09

题目:28. 实现 strStr()、459.重复的子字符串

参考链接:代码随想录

28. 实现 strStr()

思路:KMP算法,这个比较复杂,主要是需要理解一个前缀表,即储存模式串needle的最长相等前后缀,注意前缀不包含末尾,后缀不包含开头。我们的第一步就是根据needle,计算出前缀表,这里可以先不用理解为什么,先记住前缀表的求法。前缀表就是一个长度和needle相同的数组,对于needle的每个字符,都计算其相同的最长前缀和最长后缀的长度,这就是前缀表。前缀表的主要作用是当第一次匹配失败时,可以告诉你从模式串的哪个地方接着匹配。其实计算前缀表的过程,就是模式串自己和自己匹配的过程
例子:

a a b a a f
0 1 0 1 2 0

计算代码如下:不进行减一操作,就是正常的前缀表。减一个人感觉完全多此一举,还不如右移的好。思路:j指向前缀末尾,也表示i之前的最长相等前后缀子串长度,i指向后缀末尾。j初始化为0, next[0]=0 ,那么i就应该从1开始往后遍历,如果在i处与j不匹配,则使用 next[j-1] 往前回退,直到回退 j=0 。如果i和j相等,则直接将最长相等长度即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;
    }
}

然后对于匹配过程,j依旧从0开始遍历模式串,i用于遍历文本串,和求next数组相比,最大的区别就是是文本串的i和模式串的j比较,如果不匹配,依旧根据next[j-1]进行回退,最重要的地方是当j已经遍历到needle末尾的时候,说明完美匹配,直接返回结果。
完整代码:我们只用不减一的方法,可以背下来。

class Solution {
public:
    void getNext(int* next,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) {
        int len=needle.size();
        if(len==0){
            return 0;
        }
        int prefix[len];
        getNext(prefix,needle);//计算前缀表
        int j=0;//还是指向前缀末尾,模式串
        for(int i=0;i<haystack.size();i++){//i遍历文本串
            while(j>0 && haystack[i]!=needle[j]){
                j=prefix[j-1];//不匹配,回退
            }
            if(haystack[i]==needle[j]){//匹配,i和j
                j++;
            }
            if(j==len){//完美匹配
                return i-len+1;
            }
        }
        return -1;
    }
};

时间复杂度O(m+n)。

459.重复的子字符串

思路:一开始只想得到暴力算法,对每一个前缀进行往后遍历,时间复杂度O(n^2),也想不到如何和KMP结合起来。
移动匹配算法,对一个字符串s,如果其由重复相同子串组成,那么s+s中间必定包含了s,问题转换为一个字符串中间出现另一个字符串的问题。我们将2s的头和尾去掉,然后使用库函数。时间复杂度O(n)。find库函数的时间复杂度为O(m+n)。

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        string s1=s+s;//字符串可以直接+,只是和数字的相加需要重载
        s1.erase(s1.begin());
        s1.erase(s1.end()-1);
        if(s1.find(s)!=s.npos){//npos是字符串的一个标识符,如果为这个表示没找到
            return true;//能找到
        }
        return false;
    }
};

KMP法,这个完全想不到,只能先看解析再写代码。KMP最适合干的事,就是查询一个字符串串是不是另一个串的子串对于重复串组成的字符串,其最长相等前缀和后缀不包含的子串就是最小重复子串,如图所示。代码随想录算法训练营day09_第1张图片
数学推导:s为子串,x为最小重复子串,s=n*x。最长相等前后缀长度为mx,n-m=1,只需要判断nx%(n-m)x==0即可说明存在最小重复子串。
我们只需要考虑next[len-1]的值即为s的最长相等前后缀长度m,然后判断即可。时间复杂度O(n)。

class Solution {
public:
    void getNext(int* next,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;//这句当时没背出来,理解为对每个遍历的i的位置求j
        }
    }
    bool repeatedSubstringPattern(string s) {
        int len=s.size();
        int next[len];
        getNext(next,s);
        int m=next[len-1];
        if(m>0 && len%(len-m)==0){
            return true;
        }
        return false;
    }
};

一刷其中原理没有理解清楚,主要是对KMP写法的背诵,尤其是求next前缀表的代码实现。

你可能感兴趣的:(算法)