代码随想录刷题-字符串-重复的子字符串

重复的子字符串

本节对应代码随想录中:代码随想录,讲解视频:字符串这么玩,可有点难度! | LeetCode:459.重复的子字符串_哔哩哔哩_bilibili

习题

题目链接:459. 重复的子字符串 - 力扣(LeetCode)

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

示例 1:
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。

暴力解法

比较直观的解法就是构建所有可能的子串,然后去不断的和剩余的字符串匹配看看是否可以由当前子串重复组成。

字串的长度最多为字符串的一半长度,并且字符串的长度要是子串长度的倍数,才可能满足条件。

构建好子串后,我的解法是不断用 substr 截取剩下的子串长度的字符,判断和子串是否相等。不过 LeetCode 官方题解的判断效率更高点。具体来说,用 i 指针遍历字符串 s,不断得到新的长度的子串。然后使用指针 j 从 i 指针开始向后遍历,不断比较 s[j] 和 s[j - i],这句话的意思是比较当前字符和上一个周期的对应位置的字符是否相等。例如字符串 abc abc,即当 i=3子串为 abc 时比较 s[3]和 s[0]、s[4]和 s[1]、s[5]和 s[2]。也就是说不断比较当前周期的每个元素是否等于上个周期的每个元素。这样省去了剩余周期构建字符串的过程

class Solution {
    
    
   public:
    bool repeatedSubstringPattern(string s) {
    
    
        int n = s.size();
        for (int i = 1; i  <= n/2; ++i) {
    
    
            // 只有子串能被s整除才可能满足条件
            if (n % i == 0) {
    
    
                bool match = true;
                // abc abc abc
                for (int j = i; j < n; ++j) {
    
    
                    // 不断比较当前字符和上个周期的对应位置的字符是否相等
                    if (s[j] != s[j - i]) {
    
    
                        match = false;
                        break;
                    }
                }
                if (match) {
    
    
                    return true;
                }
            }
        }
        return false;
    }
};
  • 时间复杂度:O( n 2 n^2 n2)。有两个嵌套的for循环。最外层循环从1到n/2进行迭代,而内层循环遍历给定字符串的长度n。因此,总的时间复杂度为 O( n 2 n^2 n2
  • 空间复杂度:O( 1 1 1)。未使用任何额外的存储空间。它只是在原始字符串 s 上执行常量级别的操作。因此,空间复杂度为 O(1)

字符串匹配

对于可以由重复子串组成的字符串,如 abc abc,可以由 abc 组成。也就是字符串 s 的前半部分和后半部分相等,都为 abc。那我们拼接两个字符串 s 得到 abc abc | abc abc,这样第一个 s 的后半部分和第二个 s 的前半部分又能够构成一个 s。那我们从第一个字符的位置开始查找 s,如果找到的 s 的起始位置不是第二个 s 的起始位置,说明两个 s 中间可以构成一个 s。即 bc abc | abc abc 查找 abc abc 的起始位置不是在第二个 s 的起始位置说明可以由重复子串构成。

注意在这里只证明了可以由重复子串组成的字符串有这样的性质,但没证明有这样性质的字符串一定可以由重复子串组成,关于这点证明可以去看 LeetCode 官方题解方法三之后的「正确性证明」部分

class Solution {
    
    
public:
    bool repeatedSubstringPattern(string s) {
    
    
        return (s + s).find(s, 1) != s.size();
    }
};
  • 时间复杂度:O( m + n m+n m+n)。find函数通常使用KMP算法,在大多数情况下,它的时间复杂度为O(n),其中n是搜索字符串的长度。在最坏的情况下,当模式字符串与搜索字符串中的某个前缀匹配时,该算法的时间复杂度为O(m+n),其中m是模式字符串的长度。这通常发生在搜索字符串很长,而模式字符串很短且由相同字符组成的情况下
  • 空间复杂度:O( n n n)。创建了一个新的字符串对象,其长度等于给定字符串的长度的二倍

kmp 解法

这道题最优的解法还是要用到 kmp。我们知道 kmp 的 next 数组存的是公共前后缀的长度,如 abababab 的最长相等前缀和最长相等后缀为 ababab,而这两个的差的部分就是最小重复子串。

简单的给个证明:如下图,由于前缀和后缀是相等的,那么1=2,而2=3,因此1=3。同理3=4,而4=5,因此3=5。综上,1=3=5即字符串可由 ab 重复组成。

在这里插入图片描述

根据上面的推导,我们求出 next 数组,next 数组的最后一个值就是最长相等前后缀的长度,字符串的长度减去这个长度得到差值,如果这个字符串的长度是这个差值的倍数,返回 true,如果 next 数组最后一个元素为0即没有最长相等前后缀或者差值不能被整除,返回 false

// 3种情况
abcabd // next[5]=0,返回false
abcab  // next[4]=2,5-2=3无法被5整除,返回false
abcabc // next[5]=3,6-3=3能被整除,返回true

代码如下,和上题 kmp 的解法类似,只不过是得到 next 数组后判断上面的3种情况

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) {
    
    
        int next[s.size()];
        getNext(next, s);
        int len = s.size();
        // 关键语句,对应上面所讲的3种情况
        if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
    
    
            return true;
        }
        return false;
    }
};
  • 时间复杂度:O( n n n)。getNext 函数的时间复杂度为 O(n),repeatedSubstringPattern 函数的时间复杂度主要来自于 getNext 函数,因此总的时间复杂度为 O(n)
  • 空间复杂度:O( n n n)。使用了一个大小为n的数组next来存储前缀与后缀匹配的最大长度,因此空间复杂度为O(n)

猜你喜欢

转载自blog.csdn.net/zss192/article/details/129973633