LC 459.重复的子字符串

LC459. 重复的子字符串

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

示例 1:

输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。

示例 2:

输入: s = “aba”
输出: false

示例 3:

输入: s = “abcabcabcabc”
输出: true
解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成。)

提示:

  • 1 ≤ s . l e n g t h ≤ 1 0 4 1 \leq s.length \leq 10^4 1s.length104
  • s 由小写英文字母组成

解法一(枚举)

思路分析:

  1. 长度为n的字符串可以由长度为m的子串t重复多次构成,则存在以下关系:

    1. n 一定是m的倍数

    2. 子串t一定是s的前缀

    3. 对于任意 i ∈ [ m , n ) i \in [m, n) i[m,n),有s[i] = s[i-m]

  2. 即字符串s中长度为m的前缀就是t,且在这之后的每一个位置上的字符s[i],都需要与该位置之前的m个字符s[i-m]相等

  3. 且因为字符串s由某个字串构成,即子串至少需要重复一次,即m不会大于n的一半,所以只需要在 [ 1 , n 2 ] [1, \frac{n}{2}] [1,2n]范围内遍历即可

实现代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int n = s.length();
        for (int i = 1; i <= n/2; ++i) {    // i指子串长度
            if (n % i == 0) {
                boolean flag = true;
                for (int j = i; j < n; ++ j) {    // 遍历判断字符串s是否由长度为i的子串构成
                    if (s.charAt(j) != s.charAt(j-i)) {
                        flag = false;
                        break;
                    }
                }
                if (flag) return true;    // 出现一个满足条件的子串 则返回true
            }
        }
        return false;
    }
}

提交结果如下:

解答成功:
    执行耗时:9 ms,击败了78.70% 的Java用户
    内存消耗:43.5 MB,击败了5.02% 的Java用户

复杂度分析:

  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n),遍历寻找符合条件的子串长度需要时间复杂度为 O ( m ) O(m) O(m),判断子串是否符合需要时间复杂度为 O ( n ) O(n) O(n)

  • 空间复杂度: O ( 1 ) O(1) O(1),使用了常量空间

解法二(字符串匹配+Java API)

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

思路分析:

  1. 若字符串s可以写成 s ′ s ′ s ′ ⋯ s ′ s ′ s's's'\cdots s's' sssss形式,即由若干个重复子串构成,即将第一个子串 s ′ s' s,移动到字符串的末尾重新组成一个字符串,且有新组成的字符串依然还是等于字符串s

  2. 同理,把两个字符串s联合组到一起,并移除第一个元素和最后一个元素,那么得到的字符串一定包含s,即s是联合组成的新字符串的一个子串

  3. s由重复子串构成,则s有第二点提到的性质,这证明了充分性,而解题需要证明必要性,即s有第二点的性质,则s由重复子串构成

  4. 如何证明必要性;即有字符串t = s+s,且st中的起始位置不为0或n,那么s就满足题目要求

  5. 详细证明如:详细证明

实现代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        return (s+s).indexOf(s, 1) != s.length();
    }
}

提交结果如下:

解答成功:
    执行耗时:82 ms,击败了38.79% 的Java用户
    内存消耗:43.8 MB,击败了5.04% 的Java用户

复杂度分析:

  • 语言自带字符串查找函数,不具体分析

解法三(字符串匹配+KMP算法)

思路分析:

  1. 对于解法二中,查询字符串函数可以使用KMP算法来实现

  2. 且next数组对应于 字符串s的前缀表右移一步

实现代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        return kmp(s+s, s);
    }
    private boolean kmp(String s, String t) {
        int sLen = s.length();    // 字符串s的长度
        int tLen = t.length();    // 字符串t的长度
        int[] next = new int[tLen];        // 创建next数组
        // 初始化next数组
        int j = -1;
        next[0] = j;
        for (int i = 1; i < tLen; ++i) {
            while (j >= 0 && t.charAt(i) != t.charAt(j+1)) {
                j = next[j];
            }
            if (t.charAt(i) == t.charAt(j+1))
                ++ j;
            next[i] = j;
        }
        // 判断字符串t是否在字符串s中
        j = -1;    // 与初始化next数组时对应
        for (int i = 1; i < sLen-1; ++i) {    // 只能判断子串t是否在字符串s[1:n-1)中
            while (j >= 0 && s.charAt(i) != t.charAt(j+1)) {
                j = next[j];
            }
            if (s.charAt(i) == t.charAt(j+1)) {
                ++ j;
            }
            if (j == tLen-1) {
                return true;
            }
        }
        return false;
    }
}

提交结果如下:

解答成功:
    执行耗时:15 ms,击败了49.46% 的Java用户
    内存消耗:43.9 MB,击败了5.04% 的Java用户

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),遍历字符串s和组成的字符串t

  • 空间复杂度: O ( n ) O(n) O(n),next数组花费空间复杂度为 O ( n ) O(n) O(n)

解法四(优化解法三)

思路分析:

  1. 综合解法三和解法二的思路,可以发现 字符串由长度为i的前缀重复 n i \frac{n}{i} in次构成,即设i为最小的起始位置,即gcd(n,i) = i

  2. 且对于数组next[n-1]表示s具有长度为next[n-1]的相同的前缀和后缀,那么对于满足条件的字符串一定有next[n-1] = n-ii = n-next[n-1]

  3. 即满足条件则nn-next[n-1]的倍数

实现代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int n = s.length();    // 字符串s的长度
        int[] next = new int[n];    // next数组对应s的前缀表 右移
        int j = -1;
        next[0] = j;
        for (int i = 1; i < n; ++i) {
            while (j >= 0 && s.charAt(i) != s.charAt(j+1)) {
                j = next[j];
            }
            if (s.charAt(i) == s.charAt(j+1)) {
                ++ j;
            }
            next[i] = j;
        }
        // 因为next数组对应s的前缀表右移一位
        // 所以next[n-1]表示的是s具有长度为next[n-1]的相同前后缀的值-1
        // 所以计算时字符串s相同前后缀的长度为 next[n-1]+1
        // 所以判断 n 与 n-next[n-1]-1
        // 且需要注意next[n-1]+1 应该大于等于0 保证字符串s具有相同的前后缀
        return next[n-1] >= 0 && n % (n - next[n - 1] - 1) == 0;
    }
}

提交结果如下:

解答成功:
    执行耗时:8 ms,击败了83.28% 的Java用户
    内存消耗:44 MB,击败了5.04% 的Java用户

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),只需遍历一遍字符串s

  • 空间复杂度: O ( n ) O(n) O(n),需要使用辅助数组next

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值