题目来源
题目描述
class Solution {
public:
int lengthOfLongestSubstring(string s) {
}
};
题目解析
递归
使用递归,初始步长为1,遇到重复字符就拆分成两组,取两组中的最大值,(如abcdbef->abcd,cdbef)
暴力法
通过两个for循环,算出每个字符开头的不含有重复字符的 最长子串 的长度。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ans = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
std::set<char> set;
for (int j = i; j < len; ++j) {
if(set.count(s[j])){
break;
}
set.insert(s[j]);
}
ans = std::max(ans, (int)set.size());
}
return ans;
}
};
队列优化
解法一使用的是暴力法,通过观察,可以发现:其实不需要对每个字符算出以他开头的不重复最长子串,每一个字符的最长子串可以通过上一个字符的最长字段变化出来。
定义一个队列,使用滑动窗口的思想,可以减少很多重复的计算。
和933. 最近的请求次数很像,维护一个队列,每次如果有重复元素就开除原来的。当前,这里只记录队列出现的最大长度:
- 定义两个指针:left指向窗口的最右边,cur指向当前遍历的元素:
- 如果当前元素在队列中出现过:
- 开除一个员工,left++
- 将当前元素加入的队列中,判断需不需要更新最大队列长度
- 如果没有出现过:
- 将当前元素加入的队列中,判断需不需要更新最大队列长度
- 如果当前元素在队列中出现过:
class Solution {
public int lengthOfLongestSubstring(String s) {
int ans = 0;
int right = 0;
Queue<Character> queue = new LinkedList<>();
while (right < s.length()){
if (queue.contains(s.charAt(right))){ //C++的queue没有这个功能
queue.poll(); // 直到这个字符之前的全部出队
}else{
queue.offer(s.charAt(right));
ans = Math.max(ans, queue.size());
right++;
}
}
return ans;
}
}
快慢指针,使用map代替队列维护一个滑动窗口
解法二还有一个比较耗时的地方是判断当前的元素是否在不重复子串的队列中,这个操作是O(N)级别的,比较耗时。判断一个字符是否在一个字符列表中,我们很容易就可以想到使用哈希表来进行优化。
func max(num1, num2 int) int {
if num1 > num2 {
return num1
}
return num2
}
func lengthOfLongestSubstring(s string) int {
//快慢指针:快指针指向最新的下标,慢指针指向不重复的最小下标
L, R, ans := 0, 0, 0
mp := make(map[byte]int)
for R < len(s) {
if idx, ok := mp[s[R]]; ok && idx >= L {
L = idx + 1
}
mp[s[R]] = R
ans = max(ans, R-L+1)
R++
}
return ans
}
ps:也可以使用hash桶来替代map
动态规划
不管是子串问题还是子数组问题,只需要这样考虑:
- 字符串必须以0结尾的时候,左侧最多能推多远,最长无重复 求答案
- 字符串必须以1结尾的时候,左侧最多能推多远,最长无重复 求答案
- …
然后,上面所有答案中的最大值就是我想要的
然后在思考:
- 从左到右求答案
- 当我来到 i i i位置的时候, i − 1 i-1 i−1位置的答案已经知道, i − 2 i-2 i−2位置的答案已经知道…那么以 i i i结尾时的答案能不能由我之前的答案推导而出呢?
怎么思考?通过举个例子。
- 假设当前我们来到了以17位置结尾(字符a)的字符串它应该怎么推才能不重复呢?
- 假设之前的位置都解决了,站在当前位置思考,我们想象它的推要受哪些因素的影响?两个因素
- (1)当前字符(a)上次出现的位置,假设为x
- 此时17位置最多能推送到x+1的位置
- (2)当前
i
i
i位置,前一个
i
−
1
i-1
i−1位置往左推的距离
- 16位置往左推送只能推送到13,那么17位置再怎么推送也不能越过13
- (1)当前字符(a)上次出现的位置,假设为x
- 两个位置综合起来考虑:哪个离当前i更近,就是最多能推送的长度
我们不需要准备整张动态规划表,因为 i i i位置只需要 i − 1 i - 1 i−1位置的答案,并不需要左边所有的那些答案,只需要有限几个变量滚动就可以了:
- 需要记录当前字符最近出现的位置?
- 用一个map,key = 当前字符,value表示当前字符出现的索引位置
- 怎么初始化:不需要初始化,每次有字符来了就直接更新
- (如果重复出现,就覆盖;如果没有就直接更新)
- 当然,我们可以用一个256长度的数组来优化,因为它都是小写字符
- 怎么初始化:全部初始化为-1,表示没有出现过(不能初始化为0,因为0是有效索引)
- 用一个map,key = 当前字符,value表示当前字符出现的索引位置
- 需要一个pre,表示上一次最多能推送到哪个索引位置不重复
- 初始化pre = 0,map[str[0]] = 0;
- 然后我们从i = 1开始遍历
- 需要一个ans,用来维护到当前位置为止,最大无重复字符的最长子串长度
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() < 2){
return s.size();
}
std::vector<char> map(256);
for (int i = 0; i < 256; ++i) {
map[i] = -1;
}
map[s[0]] = 0;
int N = s.size();
int ans = 1;
int pre = 1; //注意,这里是长度,不是索引
// 考虑每一个位置最多往左能推送的长度
for (int i = 1; i < N; ++i) {
// 当前位置最大能推送的长度
// 受两个因素影响
int p1 = i - map[s[i]]; // 最多往左能推送的[长度]
int p2 = pre + 1; // 最多往左能推送的[长度]
int curr = std::min(p1, p2); //长度越小,离i越近
ans = std::max(ans, curr);
pre = curr; //之前长度 = 当前长度
map[s[i]] = i;
}
return ans;
}
};
类似题目
- leetcode:3. 最长子串(子串中每个字符出现次数不可以重复) Longest Substring Without Repeating Characters
- leetcode:395. 最长子串(子串中每个字符次数最少应重复k次) Longest Substring with At Least K Repeating Characters
- leetcode:159.最长子串(子串中所有字符种类数最多为2个) Longest Substring with At Most Two Distinct Characters
- leetcode:340.最长子串(子串中所有字符种类数最多为k个) Longest Substring with At Most K Distinct Characters
- leetcode:992. 最长子数组(子数组中整数种类数正好有k个)Subarrays with K Different Integers
- leetcode:933. 最近的请求次数