【LeetCode】无重复字符的最长子串-3
Hz 11/9/2021
# 题目
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
- 示例:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters (opens new window)
# 感受
第一次实际解算法题目,遇到很多 我不理解
的地方
虽然到目前为止,还没正式的完全独立写出来(惭愧)
当不妨碍我精进向前
本次也仅仅只是记录分析官方的代码
# 滑动窗口
参考官方的解题思路:
设定「开始位置(左指针)」和「结束位置(右指针)」组合成一个「窗口」。
滑动「左指针」「右指针」,枚举窗口内符合要求的子串
javascript 中 set 数据结构符合判重的要求
两重循环遍历字符串
外层循环 移动的是 左指针
内存循环 移动的是 右指针
var lengthOfLongestSubstring = function(s) {
// 哈希集合,记录每个字符是否出现过
const occ = new Set();
const n = s.length;
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
let rk = -1, ans = 0;
for (let i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.delete(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
};
log标注版
let lengthOfLongestSubstring = function (s) {
// 哈希集合,记录每个字符是否出现过
const occ = new Set();
const n = s.length;
console.log("长度:", n, "内容:", s);
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
let rk = -1,
ans = 0;
for (let i = 0; i < n; ++i) {
console.group(`第${i}轮 左指针字符(开始字符):${s.charAt(i)}`);
if (i != 0) {
// 左指针向右移动一格,移除一个字符
console.log(`左指针移动一格,移除一个字符 ${s.charAt(i - 1)}`);
occ.delete(s.charAt(i - 1));
}
// 右指针+1 并且 set内不能有该 添加一次 循环到条件不通过
while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
console.log(
`右指针位置${rk + 1} < 长度${n} 且 元素内不包含字符:${s.charAt(
rk
)}(右指针位置字符) ,右指针位置+1,子串内容:`,
occ
);
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
console.log(
`碰到重复字符:${s.charAt(rk + 1)},while循环结束。当前最长子串:`,
occ
);
console.groupEnd(`第${i}轮`);
}
return ans;
};
lengthOfLongestSubstring("abcabcbb");
log内容
// start
长度: 8 内容: abcabcbb
第0轮 左指针字符(开始字符):a
右指针位置1 < 长度8 且 元素内不包含字符:a(右指针位置字符) ,右指针位置+1,子串内容: Set(1) {'a'}
右指针位置2 < 长度8 且 元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(2) {'a', 'b'}
右指针位置3 < 长度8 且 元素内不包含字符:c(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'a', 'b', 'c'}
碰到重复字符:a,while循环结束。当前最长子串: Set(3) {'a', 'b', 'c'}
第1轮 左指针字符(开始字符):b
左指针移动一格,移除一个字符 a
右指针位置4 < 长度8 且 元素内不包含字符:a(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'b', 'c', 'a'}
碰到重复字符:b,while循环结束。当前最长子串: Set(3) {'b', 'c', 'a'}
第2轮 左指针字符(开始字符):c
左指针移动一格,移除一个字符 b
右指针位置5 < 长度8 且 元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'c', 'a', 'b'}
碰到重复字符:c,while循环结束。当前最长子串: Set(3) {'c', 'a', 'b'}
第3轮 左指针字符(开始字符):a
左指针移动一格,移除一个字符 c
右指针位置6 < 长度8 且 元素内不包含字符:c(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'a', 'b', 'c'}
碰到重复字符:b,while循环结束。当前最长子串: Set(3) {'a', 'b', 'c'}
第4轮 左指针字符(开始字符):b
左指针移动一格,移除一个字符 a
碰到重复字符:b,while循环结束。当前最长子串: Set(2) {'b', 'c'}
第5轮 左指针字符(开始字符):c
左指针移动一格,移除一个字符 b
右指针位置7 < 长度8 且 元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(2) {'c', 'b'}
碰到重复字符:b,while循环结束。当前最长子串: Set(2) {'c', 'b'}
第6轮 左指针字符(开始字符):b
左指针移动一格,移除一个字符 c
碰到重复字符:b,while循环结束。当前最长子串: Set(1) {'b'}
第7轮 左指针字符(开始字符):b
左指针移动一格,移除一个字符 b
右指针位置8 < 长度8 且 元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(1) {'b'}
碰到重复字符:,while循环结束。当前最长子串: Set(1) {'b'}
// end
# 思考
在题目的评论区有大神表示,该代码会有无意义的循环,使用跳跃式收缩窗口能解决这个问题
- 结论
缓存下每个字符及其位置: {a:1,b:2,c:3,d:4,e:5,f:6,a:7}
当出现重复字符时,直接将「左指针」移动到重复字符的后一个位置
可理解为跳跃式收缩窗口,可减少循环
- 例一
a b c d e f c g h
,重复字符是 c
当窗口为 [a b c d e f c]
时,检测到重复字符 c
这时可 跳跃收缩窗口 [d e f c]
,因为不会有比窗口 [a ... f c]
更长的子串, [a b c]
窗口的遍历是多余操作
- 例二
a b c d e f a g h
,重复字符 a
当窗口为 [a b c d e f a]
时,检测到重复字符 a
这时是正常的收缩窗口,没有发生跳跃