题目描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
分析解答
一眼看到 重复、字符串、数组 这样的关键词,首先应该想到的是 滑动窗口。
然后我们需要保留已经扫描过了子串,所有还需要使用到 哈希表 Map。
代码如下:
/**
* 无重复字符的最长子串
* @param s 原始字符串
* @description
* 双指针 -> 字符串、数组 都可用使用类似的解法
* - right 从头扫描到尾部
* - left 默认指向0,发现right出现了重复的字符时, left直接指向左边重复的下一个元素
* - 保存已经扫描的字符 - 哈希表(Map: { key: value } key 为字符,value 为索引)
* - 注意点:必须判断 map.get(s[right]) >= left 保证不会把滑动窗口之外的,但是是 Map 里的字符考虑在内
*/
function lengthOfLongestSubstring(s: string): number {
let left = 0
const map = new Map()
let length = 0
for (let right = 0; right < s.length; right++) {
if (map.has(s[right]) && map.get(s[right]) >= left) {
left = map.get(s[right]) + 1
}
map.set(s[right], right);
length = Math.max(length, right - left + 1)
}
return length
};
思路拓展
很多人第一次写这道题都会有一个疑问:为什么判断条件里要有 map.get(s[right]) >= left
,为什么不能只判断 map.has(s[right])
?
✅ 正确写法:
if (map.has(s[right]) && map.get(s[right]) >= left) {
left = map.get(s[right]) + 1;
}
🤔 为什么不能只写 map.has(s[right])
?
假设字符串是:
s = "abba"
我们一步步来看:
right | s[right] | map 内容 | left | 说明 |
---|---|---|---|---|
0 | ‘a’ | {‘a’:0} | 0 | 新字符,正常移动 |
1 | ‘b’ | {‘a’:0,‘b’:1} | 0 | 新字符,正常移动 |
2 | ‘b’ | {‘a’:0,‘b’:2} | 0❌ or 2✅ | 重复字符,但关键是:之前的 ‘b’ 位置(1)是不是在当前窗口里? 是的(1 >= left),要移动 left = 1 + 1 = 2 |
3 | ‘a’ | {‘a’:3,‘b’:2} | 2 | ‘a’ 虽然出现在 map 中,但它上次出现的位置 0 小于 left=2,已经不在窗口里,不能再移动 left ❌ |
💡 所以 map.get(s[right]) >= left
的作用是:
确保这个重复字符确实 在当前窗口中,而不是在窗口外。
❌ 错误写法(只有 map.has
)的问题:
if (map.has(s[right])) {
left = map.get(s[right]) + 1;
}
在 s = "abba"
的例子中,处理最后一个 'a'
时,会误以为 'a'
重复了,强行把 left
从 2
移到 1
,导致窗口“回退”,出错。
✅ 总结
必须加 && map.get(s[right]) >= left
,是为了保证 map 中记录的重复字符位置是当前窗口里的,避免误判窗口外的旧字符。