一、题目描述
给你一个字符串 s
和一个整数 k
,请你找出 s
中的最长子串, 要求该子串中的每一字符出现次数都不少于 k
。返回这一子串的长度。
如果不存在这样的子字符串,则返回 0。
示例 1:
输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
示例 2:
输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。
提示:
1 <= s.length <= 10^4
s
仅由小写英文字母组成1 <= k <= 10^5
二、解题思路
这个问题可以使用分治法的思想来解决。首先,我们可以遍历字符串,统计每个字符的出现次数。然后,找出那些出现次数少于k的字符,因为这些字符不能出现在结果子串中。我们将字符串根据这些字符分割成多个子串,然后递归地在每个子串中寻找满足条件的最长子串。
具体步骤如下:
- 统计字符串中每个字符的出现次数。
- 找出所有出现次数少于k的字符。
- 根据这些字符将原字符串分割成多个子串。
- 对每个子串递归地执行上述步骤,直到找到满足条件的最长子串。
三、具体代码
java
class Solution {
public int longestSubstring(String s, int k) {
// 如果字符串为空或者长度小于k,直接返回0
if (s == null || s.length() < k) return 0;
// 统计每个字符的出现次数
int[] counts = new int[26];
for (char c : s.toCharArray()) {
counts[c - 'a']++;
}
// 遍历每个字符,找出出现次数少于k的字符
for (int i = 0; i < 26; i++) {
if (counts[i] > 0 && counts[i] < k) {
// 找到出现次数少于k的字符,分割字符串
int res = 0;
for (String str : s.split(String.valueOf((char)(i + 'a')))) {
// 递归处理每个子串
res = Math.max(res, longestSubstring(str, k));
}
return res;
}
}
// 如果所有字符的出现次数都不少于k,返回原字符串长度
return s.length();
}
}
这段代码首先统计了字符串中每个字符的出现次数,然后检查哪些字符的出现次数少于k。对于每个这样的字符,它将字符串分割成多个子串,并对每个子串递归地调用longestSubstring
函数。最终,它会返回找到的最长子串的长度。如果字符串中的所有字符都至少出现了k次,那么它会直接返回字符串的长度。
四、时间复杂度和空间复杂度
1. 时间复杂度
-
统计每个字符的出现次数:这个步骤需要遍历字符串一次,所以时间复杂度是O(n),其中n是字符串的长度。
-
遍历字符统计数组:这个步骤需要遍历一个固定大小(26)的数组,所以时间复杂度是O(1)。
-
分割字符串:在最坏的情况下,如果字符串中的每个字符都至少出现了一次,那么我们需要分割n个子串(每个字符对应一个子串),每个子串的分割操作是O(n),所以这部分的时间复杂度是O(n^2)。
-
递归处理子串:对于每个子串,我们都需要调用
longestSubstring
函数。在最坏的情况下,每次递归调用都会将字符串分割成两个部分,所以递归的深度是O(log n)。每个递归调用的处理时间是O(n),所以递归部分的总时间复杂度是O(n log n)。
综合以上步骤,我们可以得出总的时间复杂度是O(n^2) + O(n log n),由于O(n^2)是主导项,所以总的时间复杂度是O(n^2)。
2. 空间复杂度
-
字符统计数组:这个数组的大小是固定的,为26,所以空间复杂度是O(1)。
-
递归栈空间:在最坏的情况下,递归的深度是O(log n),所以递归栈的空间复杂度是O(log n)。
-
分割字符串:在分割字符串时,会创建新的字符串对象,这些对象的空间复杂度是O(n)。
综合以上步骤,我们可以得出总的空间复杂度是O(n) + O(log n),由于O(n)是主导项,所以总的空间复杂度是O(n)。
五、总结知识点
-
基础编程概念:
if
语句:用于条件判断。for
循环:用于遍历集合或重复执行代码块。return
语句:用于从方法中返回值。
-
字符串操作:
toCharArray()
:将字符串转换为字符数组。split(String regex)
:根据给定的正则表达式分割字符串,并返回分割后的字符串数组。
-
数组:
- 声明和初始化数组:
int[] counts = new int[26];
。 - 访问数组元素:
counts[c - 'a']++;
。
- 声明和初始化数组:
-
字符与ASCII码:
- 字符与整数之间的转换:通过
'a'
来计算字符在数组中的索引。
- 字符与整数之间的转换:通过
-
递归:
- 方法自身调用自身:
longestSubstring(str, k)
。
- 方法自身调用自身:
-
字符串分割与递归结合:
- 使用
split
方法将字符串分割成多个子串,并对每个子串递归调用相同的逻辑。
- 使用
-
数学运算:
Math.max(int a, int b)
:返回两个整数中的最大值。
-
算法设计:
- 分治法:将大问题分解为小问题,递归地解决小问题,然后合并结果。
-
边界条件处理:
- 检查字符串是否为空或者长度是否小于k,并返回0。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。