今天无聊的时候刷了一道leetcode的题目,给定字符串,查找最长无重复字符串,具体题目信息如下:
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
1 思路解析
本题是一道考察字符串匹配的题目,找出其中不含重复字符的字符串的最大长度,所使用的解题思路是滑动窗口的方法来求解
1.1 什么是滑动窗口?
就是从字符串srring中,取出一段连续的子串,滑动窗口就是给定原始字符串S,给定子串的起始位置start和结束位置end,win = S[start, end] 就是窗口,win的值随着start和end的变化不断的变化,因此,称为滑动窗口。因此,滑动窗口虽然是一个数组,由于原串S始终不变,所以实际上只需要定义下面两个整数就可以来表示滑动窗口了:
···
// 滑动窗口模板
int start
int end
···
这样有什么好处呢?可以充分利用空间,降低空间复杂度,避免无效的读写运算。
1.2 滑动窗口求解无重复子串的最大长度
思路:
设定一个全局变量来记录最大长度 maxLen = 0
初始化一个滑动窗口,start = 0, end = 0
开始for循环遍历S,设当前位置为 curId,每次移动一位
if S[curId] 在滑动窗口中不存在,
则将curId加入到滑动窗口中,即 end = curId 或者 end++
else
从滑动窗口中找到与curId对应的字符相等的最后一个字符的位置 lastId,将lastId 之前的所有字符从滑动窗口中删除,即 start = lastId +1
则将curId加入到滑动窗口中,即 end = curId 或者 end++
更新 maxLen = max( maxLen, end - start + 1)
2 实现代码
2.1 白银解法:
执行用时: 12 ms
内存消耗: 6.8 MB
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = 0;
int start = 0, end = 0; //定义滑动窗口
for (int i = 0; i < s.size(); i++) { //遍历所有的字符
for (int j = start; j < end; j++) { // 遍历滑动窗口
if (s.at(j) == s.at(i)) {
start = j + 1; // 如果当前字符和滑动窗口中的字符相同,则删除滑动窗口中前面的字符
}
}
end++;
len = (len < end - start) ? (end - start) : len; // 更新最大长度
}
return len;
}
};
上面是根据滑动窗口的思路,写出来的,耗时12ms,做完之后,开始思考,是否还可以优化?
for (int j = start; j < end; j++) {
if (s.at(j) == s.at(i)) {
start = j + 1;
}
}
上面的代码总每个字符都需要全部遍历滑动窗口
我们的目的是找到最后一个与当前字符相同的位置,然后更新start的值,何不倒序遍历滑动窗口?
for (int j = end - 1; j >= start; j--) {
if (s.at(j) == s.at(i)) {
start = j + 1;
break; // 满足条件就直接跳出for循环,减少不必要的计算
}
}
2.2 黄金求解
上面虽然优化之后有一点点的性能改进,但是,还不是最好的。
有一个问题,每次更新滑动窗口的时候,只需要遍历滑动窗口来更新start,是否可以避免掉这种遍历?
从算法的优化角度来考虑,最简单的优化思路如下
1 牺牲空间换时间
2 牺牲时间换空间
尝试使用空间来换时间,从解题思路可以看到,我们只需要找到当前字符在滑动窗口中的位置,是否可以用一个固定的数组来记录每个字符上次出现的位置,这样就可以不用遍历滑动窗口了。
由于字符串中的每个字符对应一个ASCII码,将字符串的ASCII码值作为下标,上次出现的下标作为数组的值,因此,设置一个记录状态的数组 int state[128].
因此,上面的for循环查找位置则变成了 判断 state[s[end]] >= start,实现代码如下:
执行用时: 4 ms
内存消耗: 6.7 MB
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = 0, start = 0, end = 0;
int state[128];
memset(state, -1, sizeof(state));
for (end = 0; end < s.size(); end++) {
if (state[s[end]] >= start) {
start = state[s[end]] + 1;
}
state[s[end]] = end;
len = (len < end - start + 1) ? (end - start + 1) : len;
}
return len;
}
};