395——至少有K个重复字符的最长子串
残疾人康复训练
题目:
给你一个字符串 s
和一个整数 k
,请你找出 s
中的最长子串, 要求该子串中的每一字符出现次数都不少于 k
。返回这一子串的长度。
解题尝试
最开始的时候一头雾水(因为我是残疾人)
求子串的问题,在我看来,最难的地方就是时间复杂度
因为随着字符串长度的增加,全部字串的数量是呈指数级别上升的,因此所有基于枚举所有字串的暴力算法必定会超时
因为太久没有写这种题目,用的又是陌生的C++,我决定看一下评论区
在看完评论区后,我写出了第一版的代码
class Solution
{
public:
int longestSubstring(string s, int k)
{
/*
方法一:使用分治的方法
求最长子串,即求每一个满足条件的子串的长度,然后返回最大的子串
其中,每一个子串又可以当作主串进行分解,直至分解出空串,结束
*/
// 若为空串,返回
if (s.length() == 0)
return 0;
// 接下来记录当前字符串的字符出现的次数
vector<int> count(26, 0); // 26个小写字母
for (int i = 0; i < s.length(); i ++)
count[s[i] - 'a'] ++;
// 然后判断当前的字符串是否为满足条件的字符串
int pos = 0;
while (pos < s.length() && count[s[pos] - 'a'] >= k)
pos ++;
// pos == 0,说明整个字符串没有一个字符出现次数大于等于k
// pos == s.length,说明完全满足条件
if (pos == s.length() || pos == 0)
return pos;
// 若并不是满足条件的字符串,但是有一部分字符出现次数满足条件
// 说明有可能它的子串能满足条件
// 将整个字符串按照pos为分界线拆分成两部分
int left = longestSubstring(s.substr(0, pos), k);
int right = longestSubstring(s.substr(pos), k);
return max(left, right);
}
};
整个算法思想最大的错误在于这里:
// pos == 0,说明整个字符串没有一个字符出现次数大于等于k
// pos == s.length,说明完全满足条件
if (pos == s.length() || pos == 0)
return pos;
即使pos==0
,整个字符串也不一定完全不满足条件
原谅我,我只是一个残疾人,这是我的第一次康复训练,难免会写沙比代码
经过修改和五六次本地debug后,得出的版本如下:
class Solution
{
public:
int longestSubstring(string s, int k)
{
/*
使用分治的方法进行求解
一个字符串有这几种情况:
1. 所有字符出现的次数都小于k,返回0,即不存在这种子串
2. 所有字符出现的次数都大于等于k,返回字符串的长度,即最长子串为自身
3. 一部分字符出现次数大于等于k,另一部分出现的次数小于k,即有可能存在这种子串
第三种情况需要对字符串进行拆分处理,关键的难点在于如何对字符串进行拆分
*/
// 第一步,对字符串的所有字符出现的次数进行统计, 时间复杂度:O(n)
vector<int> count(26, 0); // 26个小写英文字母
for (char c : s)
count[c - 'a'] ++;
// 第二步,判断这个字符串属于哪一种情况,时间复杂度:O(1)
bool allSatisfy = true, allNotSatisfy = true;
for (auto item : count)
if (item >= k)
allNotSatisfy = false;
else if (item != 0 && item < k)
allSatisfy = false;
if (allNotSatisfy)
return 0;
else if (allSatisfy)
return s.length();
// 第三步,对该字符串进行拆分处理,时间复杂度最大为O(n)
int pos = 0;
while (pos < s.length() && count[s[pos] - 'a'] < k)
pos ++;
int start = pos; // 忽略开头那些不符合条件的字符--含有不符合条件的字符的子串必定不可能满足
while (pos < s.length() && count[s[pos] - 'a'] >= k)
pos ++;
int left = longestSubstring(s.substr(start, pos), k);
while (pos < s.length() && count[s[pos] - 'a'] < k)
pos ++;
int right = longestSubstring(s.substr(pos), k);
return max(left, right);
}
};
运行时间0ms
思路
首先对这个问题进行分析,给我一个字符串,我能得到的结果只有三种:
- 完全不符合条件
- 全部符合条件
- 一半符合一半不符合
完全符合和完全不符合很容易判断:完全符合就返回当前的字符串的长度,完全不符合就返回0
这里的关键是一半符合一半不符合要怎么进行处理,要怎么样才能把它分成完全符合和完全不符合的情况
对于第三种情况的字符串,其内的字符必定存在一部分是满足条件的,另一部分是不满足条件的
对于任何一个字符串,只要存在不满足条件的字符,那么它一定不是完全满足条件的
所以,我们要以不满足条件的字符为分隔符,寻找最长的子串的长度
例如
"bbaaacbd" 3
这个测试样例,其中c和d是不满足条件的字符,我们将其用#
代替,则是求这两个子串
"bbaaa"
"b"
的最长字串的长度
其中,第二个子串明显不符合
第一个子串中的b变成了不满足条件的字符,那么我们将b作为分隔符,再次进行分割,只剩下aaa
这个子串,而这个子串符合第二种情况,因此返回其长度,即3
实际操作的时候,受我懒惰的限制,将整个字符串分成左右两部分,分别求其最大长度
分享别人的做法
- python之不用脑子法
class Solution(object):
def longestSubstring(self, s, k):
if not s:
return 0
for c in set(s):
if s.count(c) < k:
return max(self.longestSubstring(t, k) for t in s.split(c))
return len(s)
果然python够直接,适合我这类残疾人使用
但是我还是不喜欢python,除非什么时候它能跑得和C语言一样快
- C++之我是抄它的法
class Solution {
public:
int longestSubstring(string s, int k) {
if (k <= 1) return s.size();
if (s.empty() || s.size() < k) return 0;
vector<int> hash(128, 0);
for (char c : s) ++hash[c];
int i = 0;
while (i < s.size() && hash[s[i]] >= k) ++i;
if (i == s.size()) return s.size();
int l = longestSubstring(s.substr(0, i), k);
while (i < s.size() && hash[s[i]] < k) ++i;
int r = longestSubstring(s.substr(i), k);
return max(l, r);
}
};