力扣打卡Day09 KMP!!!!

突然也是似曾相识,发现两年前学过,不过也忘得差不多了。

我觉得KMP的关键在于next数组的计算,另外就是在图上画文本串、模式串、next数组真的能很快程度上帮助理解。

按“死办法”过一遍,并且关注一些需要注意的点,然后就能很快记住对应的逻辑了,然后就跟着题解过了一遍,就可以自己写出来了。

void getNext(int* next, const string& s) {
    int j=0;
    next[0]=0;//将数组的第一个数设置为0
    for(int i=1;i<s.size();i++) {//从第二个字符开始看
        while(j>0&&s[j]!=s[i]) j=next[j-1]; //j指向的不是第一个字符(j要保证大于0,因为下面有取j-1作为数组下标的操作),并且i和j指向的字符不相等
        if(s[j]==s[i]) j++; //相等时对应的数加一
        next[i]=j; //赋值
    }
}

我把做KMP题简单分为以下步骤:

  1. 写出getNext()函数:这是针对Needle模式串和Next数组的,遍历Needle模式串数组,每次遍历都给next赋值j,使用i、j指针分别指向Needle和Next,next[0]预先设置好为0,接着从Needle的第二个数开始,将s[i]和s[j]进行比较,如果相同则j++,如果不同则让j回到上一个尚且相等的字符对应的next的值,在此位置再开始比较。j不仅是指针索引,而且要赋值给next的对应位置的值。
  2. 写主函数:在主函数里定义next,使用getNext()函数得到next数组,再根据题目的具体需求来做题。

 28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

以及知道了getNext()的逻辑,下面主要看题目与主函数需求,这道题要求在haystack字符串中找出needle字符串的第一个匹配项。

在getNext()里,主要针对了Needle模式串和Next数组。

在strStr()里,我们需要结合haystack、needle和next。同样是使用到了i和j两个指针,对haystack目标文本串进行遍历,然后比较haystack与needle的不同之处。

整体来看,其实这部分函数与getNext()高度相似了,但是这里有一点要注意是,haystack要从开头就开始比较,即i=0。然后还在for循环里加了条j是否等于模式串长度的判断,这里的意义是,如果模式串都对上了,那么j就会遍历到最后,存在对应的匹配项,我们这时候只要返回对应的下标就可以啦。

如果循环结束,也没有匹配项,直接返回-1就行。

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j=0;
        next[0]=0;//将数组的第一个数设置为0
        for(int i=1;i<s.size();i++) {//从第二个字符开始看
            while(j>0&&s[j]!=s[i]) j=next[j-1]; //j指向的不是第一个字符(j要保证大于0,因为下面有取j-1作为数组下标的操作),并且i和j指向的字符不相等
            if(s[j]==s[i]) j++; //相等时对应的数加一
            next[i]=j; //赋值
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) return 0;

        
        vector<int> next(needle.size());
        getNext(&next[0], 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() ) { //当能把模式串都对上,j成功遍历到最后,说明是存在对应的字符串匹配的
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

当然是也能用暴力解法,虽然我一开始连暴力解法也没有想明白。

用到了i、j、k三个指针,其中i用于遍历haystack的每一位,j用于在循环内遍历以i开头、长度为模式串needle大小的字符串,k用于循环内遍历needle的每一位。

class Solution {
public:
    int strStr(string haystack, string needle) {
        int n = haystack.size(),m=needle.size();
        for(int i=0;i<=n-m;i++) {
            int j=i,k=0;
            while(k<m && haystack[j]==needle[k]) {
                j++;
                k++;
            }
            if(k==m) return i;
        }
        return -1;
    }
};

459. 重复的子字符串 - 力扣(LeetCode)

回文串思想,在用于处理字符串的前缀和后缀的时候好像是比较吃香,在前面的右旋字符串里面也用到了。55. 右旋字符串(第八期模拟笔试) (kamacoder.com)

把两份字符串拼在一起,如果除了开头结尾还出现了同样的字符串的时候,就说明原字符串是由重复子串组成的,妙啊妙啊。注意要掐头去尾,这里用到了erase()函数,erase()函数的时间复杂度是O(n)。另外还在这里学到了当String的find()函数没有找到对应字符串的时候,返回的是string::npos,应该是noposition的缩写。

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        string t = s + s; // 只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
        t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾,防止find到原来的字符串
        if (t.find(s) != string::npos) return true; // 找不到时返回string::npos
        return false;
    }
};

另外就是KMP方法,计算next数组的方法是一样的,但是主函数方法还是想不到,可能就是这种题是要记下来的。

主函数里的if判断的那个取余是精髓,力扣官方题解里说很容易用反证法证出来。

对于不满足题目要求的字符串,nnn 一定不是 n−fail[n−1]−1n - \textit{fail}[n-1] - 1n−fail[n−1]−1 的倍数。

作者:力扣官方题解
链接:https://leetcode.cn/problems/repeated-substring-pattern/solutions/386481/zhong-fu-de-zi-zi-fu-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在代码随想录里说 

 假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。

因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)

所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。

next 数组记录的就是最长相同前后缀 字符串:KMP算法精讲 (opens new window)这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。

最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:字符串:KMP算法精讲 (opens new window))

数组长度为:len。

如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。

数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。

强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法

class Solution {
public:
    void getNext (int* next, const string& s){
        next[0] = 0;
        int j = 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;
        }
    }
    bool repeatedSubstringPattern (string s) {
        if (s.size() == 0) {
            return false;
        }
        int next[s.size()];
        getNext(next, s);
        int len = s.size();
        if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
            return true;
        }
        return false;
    }
};

今天还复习了一下字符串和双指针法,回顾了一下之前的知识,还学到了新知识。

  1. 原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。
  2. erase()函数的时间复杂度是O(n)
  3. 通过两个指针在一个for循环下完成两个for循环的工作。对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。

    同样的道理,五数之和,n数之和都是在这个基础上累加。

好累,接着补下一天的内容了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值