LeetCode 第 3 题「无重复字符的最长子串」
字数 2337 2025-10-25 17:33:22
好的,我们这次来详细学习 LeetCode 第 3 题「无重复字符的最长子串」。
1. 题目描述
题目:给定一个字符串 s,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以长度为 3。
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以长度为 1。
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以长度为 3。
注意:答案必须是子串,"pwke" 是一个子序列,不是子串。
注意:子串是连续的,子序列可以不连续。
2. 题意理解
我们要找的是 连续的一段字符,并且里面不能有重复的字符。
比如 "abcabcbb":
- 从下标 0 开始:
"a"→"ab"→"abc"都无重复,长度 3。 - 如果继续加
'a',就重复了,所以不能直接延长,需要调整起点。
关键:当发现重复字符时,我们要跳过重复字符出现的位置,重新计算无重复区间。
3. 思路演进
3.1 暴力法(用于理解问题)
枚举所有子串(起点 i,终点 j),检查是否无重复字符。
检查重复可以用哈希集合。
时间复杂度:O(n³) 或 O(n²)(取决于检查重复的方式),会超时。
3.2 滑动窗口(双指针)思路
我们维护一个窗口 [left, right],表示当前考察的子串。
- 用哈希集合
windowSet存储窗口内的字符(为了 O(1) 判断重复)。 right指针逐个向右移动,将字符加入窗口:- 如果
s[right]不在集合中,说明无重复,更新最大长度。 - 如果
s[right]已在集合中,说明出现重复,需要移动left指针,从集合中移除s[left],left++,直到重复字符被移出窗口为止,然后才把s[right]加入集合。
- 如果
示例 s = "abcabcbb":
- 初始:
left=0, right=0, 集合{}, maxLen=0 - right=0:
'a'不在集合 → 加入,maxLen=1 - right=1:
'b'不在集合 → 加入,maxLen=2 - right=2:
'c'不在集合 → 加入,maxLen=3 - right=3:
'a'在集合中(此时集合{a,b,c})→ 移动 left:- 移除 s[0]='a',left=1,集合
{b,c} - 现在
'a'不在集合了,加入'a',maxLen 仍为 3
- 移除 s[0]='a',left=1,集合
- 依此类推。
这样每个字符最多进入和离开集合一次,时间复杂度 O(n)。
3.3 优化:直接跳转 left
上面在发现重复时,left 是一个个移动的,其实可以一次跳到重复字符的下一个位置。
方法:用哈希映射 lastOccurrence 记录每个字符 最近一次出现的下标。
- 当
s[right]在lastOccurrence中且其索引 ≥left(即在当前窗口内重复),则直接把left设为lastOccurrence[s[right]] + 1。 - 更新
lastOccurrence[s[right]] = right。
这样避免了一步步移动 left。
4. 详细步骤(优化后滑动窗口)
- 初始化:
left = 0maxLen = 0- 哈希表
map:字符 → 最后出现的位置
- 遍历
right从 0 到 n-1:- 如果当前字符
ch = s[right]在map中,并且map[ch] >= left(在当前窗口内重复):- 将
left更新为map[ch] + 1(跳过重复字符上次出现的位置)
- 将
- 更新
maxLen = max(maxLen, right - left + 1) - 更新
map[ch] = right
- 如果当前字符
- 返回
maxLen
5. 示例推演(s = "abcabcbb")
| right | ch | map 初始(空) | left | 动作 | maxLen |
|---|---|---|---|---|---|
| 0 | a | 存 a:0 | 0 | 无重复,max=1 | 1 |
| 1 | b | 存 b:1 | 0 | 无重复,max=2 | 2 |
| 2 | c | 存 c:2 | 0 | 无重复,max=3 | 3 |
| 3 | a | a 在 map 中 index=0 ≥ left=0 → 重复 | left=0+1=1 | 更新 a:3,max=3? right-left+1=3-1+1=3 | 3 |
| 4 | b | b 在 map 中 index=1 ≥ left=1 → 重复 | left=1+1=2 | 更新 b:4,长度=4-2+1=3 | 3 |
| 5 | c | c 在 map 中 index=2 ≥ left=2 → 重复 | left=2+1=3 | 更新 c:5,长度=5-3+1=3 | 3 |
| 6 | b | b 在 map 中 index=4 ≥ left=3 → 重复 | left=4+1=5 | 更新 b:6,长度=6-5+1=2 | 3 |
| 7 | b | b 在 map 中 index=6 ≥ left=5 → 重复 | left=6+1=7 | 更新 b:7,长度=7-7+1=1 | 3 |
最终结果:3。
6. 代码实现(Python)
def lengthOfLongestSubstring(s: str) -> int:
char_map = {} # 存储字符最后出现的索引
left = 0
max_len = 0
for right, ch in enumerate(s):
if ch in char_map and char_map[ch] >= left:
left = char_map[ch] + 1
max_len = max(max_len, right - left + 1)
char_map[ch] = right
return max_len
7. 复杂度分析
- 时间复杂度:O(n),n 为字符串长度,每个字符遍历一次。
- 空间复杂度:O(min(m, n)),m 为字符集大小(ASCII 最多 128),哈希表存储字符索引。
这样一步步从暴力法到滑动窗口,再到优化跳转,应该能让你彻底理解这个经典题目了。需要我再解释某一部分吗?