问题简介
📌 题目描述 :
给定一个字符串 s,请你找出其中不含有重复字符的 最长子串 的长度。
示例说明
✅ 示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
✅ 示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
✅ 示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
✅ 示例 4:
输入: s = ""
输出: 0
解题思路
💡 核心思想 :使用滑动窗口(Sliding Window) + 哈希表(记录字符最近出现位置)
✅ 方法一:滑动窗口 + 哈希表(推荐)
- 使用两个指针
left和right表示当前窗口的左右边界。 - 用一个哈希表
map记录每个字符最近一次出现的下标。 - 遍历字符串,
right指针不断右移:- 如果
s[right]已在当前窗口中出现过(即map中存在且其索引 ≥left),说明出现重复。 - 此时将
left移动到重复字符的下一个位置 (即map[s[right]] + 1)。
- 如果
- 更新
map[s[right]] = right。 - 每次更新最大长度:
maxLen = max(maxLen, right - left + 1)。
❗注意:不能简单地将
left设为map[s[right]] + 1而不判断是否在当前窗口内。例如"abba",当right=3(第二个'a')时,map['a']=0,但此时left=2(因为前面'bb'已经移动过),所以'a'不在当前窗口[2,2]中,不应移动left。
✅ 方法二:暴力法(不推荐,仅用于理解)
- 枚举所有子串,对每个子串检查是否有重复字符。
- 时间复杂度 O(n³),会超时。
✅ 方法三:优化的滑动窗口(使用数组代替哈希表)
- 若字符集有限(如 ASCII 128 字符),可用
int[128]数组代替HashMap,初始化为-1。 - 逻辑同方法一,但效率更高(常数时间更优)。
代码实现
tabs
:::tab Java
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
Map<Character, Integer> map = new HashMap<>();
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果字符已存在且在当前窗口内
if (map.containsKey(c) && map.get(c) >= left) {
left = map.get(c) + 1;
}
map.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
:::
:::tab Go
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 {
return 0
}
m := make(map[byte]int)
left, maxLen := 0, 0
for right := 0; right < len(s); right++ {
c := s[right]
if idx, ok := m[c]; ok && idx >= left {
left = idx + 1
}
m[c] = right
if right-left+1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
:::
💡 提示 :Go 中
map的ok返回值用于判断 key 是否存在。
示例演示(以 "pwwkew" 为例)
| right | char | map 状态(char→index) | left | 当前窗口 | maxLen |
|---|---|---|---|---|---|
| 0 | 'p' | {'p':0} | 0 | "p" | 1 |
| 1 | 'w' | {'p':0, 'w':1} | 0 | "pw" | 2 |
| 2 | 'w' | {'p':0, 'w':2} | 2 | "w" | 2 |
| 3 | 'k' | {'p':0, 'w':2, 'k':3} | 2 | "wk" | 2 |
| 4 | 'e' | ... | 2 | "wke" | 3 |
| 5 | 'w' | ... → 'w':5 | 3 | "kew" | 3 |
✅ 最终结果:3
答案有效性证明
- 正确性 :滑动窗口始终维护一个无重复字符 的子串。
- 当遇到重复字符时,
left跳到其上次出现位置之后,确保窗口内无重复。 - 所有可能的无重复子串都会被窗口覆盖(因为
right遍历所有位置)。
- 当遇到重复字符时,
- 最优性 :每次扩展
right时都尝试更新最大长度,不会遗漏更优解。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 滑动窗口 + HashMap | O(n) | O(min(m, n)) | m 是字符集大小(如 ASCII 128),n 是字符串长度 |
| 暴力法 | O(n³) | O(1) | 枚举所有子串 + 检查重复 |
| 滑动窗口 + 数组 | O(n) | O(m) | 更快的常数因子,适用于已知字符集 |
✅ 实际应用中,方法一(滑动窗口 + 哈希表) 是最优解。
问题总结
- 🔑 关键技巧:滑动窗口 + 哈希表记录字符位置。
- ⚠️ 易错点 :
- 忘记判断重复字符是否在当前窗口内(如
"abba")。 - 错误地将
left直接设为map[c] + 1而不判断map[c] >= left。
- 忘记判断重复字符是否在当前窗口内(如
- 💡 扩展思考 :
- 若求最长无重复子串本身(而非长度),只需额外记录起始位置和长度。
- 若字符集很大(如 Unicode),仍可用
HashMap,空间复杂度为 O(k),k 为实际出现的不同字符数。
✅ 掌握此题,就掌握了滑动窗口类问题的核心思想!
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions