首先有两种方法,一种是分治的方法。另一种是滑动窗口。
1. 分治方法
分治: 这个题目可以使用分治的原因就在于如果一个字符出现次数小于k次,那么最终结果字符串肯定不包含这个字符,比如abcaa,k=2情况下,b和c出现的次数都小于2,最终结果是aa字符串长度为2,不包含bc两个字符。
步骤:
- 字符串s整体统计词频。
- 遍历词频表,如果某个字符ch词频大于0,小于k,那么说明这个字符ch需要被剔除。
- ch它两边的字符才有可能是结果。比如abcaa,k=2,b的词频是1,大于0,小于k=2.b两边分别是a和caa,结果只能在这两个字符串中寻找。同理c也是,c两边分别是ab和aa。因此可以分别对于b进行一次分割,b两边的字符串去做相同的操作找到最大的字符。
- 递归求解相同子问题:对于c字符两边的字符串也去做递归操作。
- longestSubstring(abcaa, 2)
- 对于按照b切分的左右字符串进行询问最大子串:longestSubstring(a, 2) longestSubstring(caa, 2)
- 0, longestSubstring("", 2) ,longestSubstring(aa, 2)
- 最终返回2
- 对于c切分的左右字符串进行询问最大子串:longestSubstring(ab, 2) longestSubstring(aa, 2)
- 0, 2
- 最终2
- 对于按照b切分的左右字符串进行询问最大子串:longestSubstring(a, 2) longestSubstring(caa, 2)
分治这个方法就用于将父类问题转换为规模更小的子类问题进行解决,本质和递归很相似。
时间复杂度这个我也不是很懂,Leetcode评论区说是这个答案,不太明白为什么最多执行26次。目前还在争论,我个人觉得这个方法不是最优解,递归的层数和重复子问题过多。
时间复杂度:O(N * 26 * 26),因为函数最多执行 26 次,for循环遍历一次是26个字符,循环里面对 s 分割时间复杂度是O(N)。超过了 84.40% 的提交。
空间复杂度:O(26 * 26),函数执行 26 次,每次开辟 26 个字符的set空间。
推荐阅读以及分治方法参考自: leetcode-cn.com/problems/lo…
class Solution {
// 函数定义为给定的字符串s,至少有 K 个重复字符的最长子串
// 函数意义和题目相同
public int longestSubstring(String s, int k) {
// base case
if (s.length() < k) {
return 0;
}
int[] freqMap = new int[26];
// 统计词频
for (int i = 0; i < s.length(); i++) {
freqMap[s.charAt(i) - 'a']++;
}
int res = 0;
// 对于 0 < 出现次数 < k的字符进行切割,该字符左右字符串继续递归
// 比如b是小于2次的,aba -> a, a继续递归
for (int i = 0; i < freqMap.length; i++) {
char ch = (char)(i + 'a');
if (freqMap[ch - 'a'] > 0 && freqMap[ch - 'a'] < k) {
for (String str : s.split(String.valueOf(ch))) {
res = Math.max(res,longestSubstring(str, k));
}
// 只要出现一个小于k的,那么说明结果必然不是s.length
// 最终结果应该来源于小于k字符的左右两边的字符串结果中最大的。
// abaa, k = 2 -> 必然res = 2最终,因此这时候直接返回。
return res;
}
}
// 如果上面的if没有进去,说明所有字符串的词频都是大于k的,返回当前字符串长度。
return s.length();
}
}
2. 滑动窗口
2.1 思路过程:
滑动窗口官方题解看的头疼,能说人话不说人话。 一开始就一直想一次性扫描一边这个string算出答案,abcdde,考虑这个例子,右指针无法确定当前应不应该停下来,因为后面仍旧有可能满足答案,所以右指针不得已要走完一遍。那右指针现在就stuck 在最后位置了,显然不可能回来了。左指针移动到第一个d的位置,窗口内dde显然也是不满足k=2的情况的。这个问题的根源在于我们根本不知道后面出现的字符是不是可能能满足前面出现的字母。
2.2 步骤:
因此,我们对于字母类型的数量进行枚举,一开始我们默认只有一种类型,比如aaab, k = 2,结果就应该是aaa。但是b我们也要扫描。为了解决这个问题,我们应该在解决完一种类型后舍弃该类型的内容,aaa,left指针是0,right指针是2. 右指针右移到b,也就是index=4位置,导致现在窗口内的字符类型的数量超过1,那么我们现在要让左指针移动直到又满足窗口内字符类型的数量不超过1. left指针移动到b的位置也就可以满足条件。 右指针移动到结尾后,我们需要默认现在窗口只允许容纳2种类型,因此继续上面步骤。发现没有解。其实这个时候函数就可以break了,这个可以通过全局变量来标记总共type,字母类型的数量枚举到给的字符串s的总类型就可以。
下面是来自评论区seven的思路,他的思路比官方题解更加概括,适合看完官方题解的看一下:leetcode-cn.com/problems/lo…
大佬,没想到可以递归,不错,又学到了一种思路。这题第一反应是滑动窗口,但是不能直接滑窗,不好搞,窗口守不住,结合分治,分26情况,子问题可以滑窗。
4个月前刷过这道题,当时用Go做的,滑动窗口。今天重新用Java打卡。
总体思路:
因为题目中明确指出字符均为小写字母,利用这个条件依次尝试:
如果必须是1、2、3...26种字符时,达标(每个字符次数>=k)的子串最长是多少
26种情况,每种情况收集1个答案,最终的答案就是26种的max。
固定字符种类后,子问题可以滑动窗口:O(N)
总的时间复杂度,可以认为是O(N)
Java,1ms,80%
2.3 代码
class Solution {
public int longestSubstring(String s, int k) {
// 枚举最长子串可能的所有字符类型的最多数目
int len = s.length();
int res = 0;
for (int maxType = 1; maxType <= 26; maxType++) {
int left = 0;
int right = 0;
int totalType = 0;
int[] freqMap = new int[26];
int lessKCount = 0;
while (right < len) {
if (freqMap[s.charAt(right) - 'a'] == 0) {
lessKCount++;
totalType++;
}
freqMap[s.charAt(right) - 'a']++;
if (freqMap[s.charAt(right) - 'a'] == k) {
lessKCount--;
}
while (totalType > maxType) {
freqMap[s.charAt(left) - 'a']--;
// 很巧明利用k-1这个条件来控制lessKCount是否需要++,然后统一在词频为0的时候--
if (freqMap[s.charAt(left) - 'a'] == k - 1) {
lessKCount++;
}
if (freqMap[s.charAt(left) - 'a'] == 0) {
totalType--;
lessKCount--;
}
left++;
}
if (lessKCount == 0) {
res = Math.max(res, right - left + 1);
}
right++;
}
}
return res;
}
}