LeetCode 3 无重复字符的最长子串:python3 题解
目录
1. 题目含义解析
目标:在一个字符串中,找到一段连续的字符,这段字符里没有任何重复的字母,且这段字符的长度要尽可能长。
关键点:
- 子串 (Substring):必须是连续的。例如
"pwwkew"中,"wke"是子串,但"pwke"不是(因为跳过了中间的w)。 - 无重复:窗口内的字符不能出现两次。
- 返回长度:不需要返回具体的子串,只需要返回它的长度。
示例分析:
- 输入
"abcabcbb"- 无重复子串有
"abc","bca","cab"等。 - 最长的是
"abc",长度为 3。
- 无重复子串有
- 输入
"bbbbb"- 任何包含两个
b的串都有重复。 - 最长只能是
"b",长度为 1。
- 任何包含两个
2. 核心解法:滑动窗口 + 哈希表 (最优解)
这是最推荐的方法,时间复杂度为 \(O(n)\),效率最高。
2.1 思路图解
想象你有一个可以伸缩的窗口(框),框住了字符串的一部分。
我们有两个指针:
left:窗口的左边界。right:窗口的右边界。
操作流程:
right指针不断向右移动,把新字符纳入窗口。- 每次纳入新字符前,检查这个字符是否已经在窗口内出现过。
- 如果没有重复:窗口扩大,更新最大长度。
- 如果有重复:说明当前窗口不合法了。我们需要缩小窗口,将
left指针向右移动,直到把那个重复的旧字符移出窗口为止。
为了快速判断字符是否重复,以及知道重复字符在哪里,我们使用一个哈希表(字典)来记录每个字符最后一次出现的位置。
2.2 关键逻辑细节
当 right 遇到一个已经在字典里存在的字符 char 时,left 应该移动到哪里?
- 假设
char上次出现在索引old_index。 - 为了排除这个重复,
left必须移动到old_index + 1。 - 注意:
left只能向右移动,不能向左回退。所以新的left应该是max(当前 left, old_index + 1)。- 例子:字符串
"abba"。当处理第二个'b'时,left移到了'b'后面。当处理第二个'a'时,虽然'a'第一次出现在索引 0,但此时left已经在索引 2 了,我们不能把left退回到 1,否则窗口里会包含重复的'b'。
- 例子:字符串
2.3 代码实现 (Python 3)
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 哈希表,用来存储字符最后一次出现的索引位置
# key: 字符,value: 该字符在字符串中的索引
char_index_map = {}
# left 指针表示滑动窗口的左边界(包含)
left = 0
# max_length 用来记录找到的最长子串长度
max_length = 0
# right 指针表示滑动窗口的右边界,遍历字符串
for right, char in enumerate(s):
# 如果当前字符已经在哈希表中,说明遇到了重复字符
if char in char_index_map:
# 更新 left 指针
# 1. char_index_map[char] + 1: 跳过上次出现该字符的位置
# 2. left: 保证 left 指针只向右移动,不会回退
# 取两者较大值,确保窗口内没有重复字符
left = max(left, char_index_map[char] + 1)
# 更新当前字符的最新索引位置
char_index_map[char] = right
# 计算当前窗口长度 (right - left + 1) 并更新最大长度
# 即使刚遇到过重复,更新 left 后,当前窗口也是合法的
current_length = right - left + 1
max_length = max(max_length, current_length)
return max_length
2.4 代码执行流程演示
以 s = "abcabcbb" 为例:
| 步骤 | right | char | char_index_map (更新前) | left 变化逻辑 | left (更新后) | 当前窗口 | 长度 | max_length |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 'a' | {} | 不在 map 中 | 0 | "a" | 1 | 1 |
| 2 | 1 | 'b' | 不在 map 中 | 0 | "ab" | 2 | 2 | |
| 3 | 2 | 'c' | 不在 map 中 | 0 | "abc" | 3 | 3 | |
| 4 | 3 | 'a' | 'a' 在 map (idx 0) -> left=max(0, 0+1) | 1 | "bca" | 3 | 3 | |
| 5 | 4 | 'b' | 'b' 在 map (idx 1) -> left=max(1, 1+1) | 2 | "cab" | 3 | 3 | |
| 6 | 5 | 'c' | 'c' 在 map (idx 2) -> left=max(2, 2+1) | 3 | "abc" | 3 | 3 | |
| 7 | 6 | 'b' | 'b' 在 map (idx 4) -> left=max(3, 4+1) | 5 | "cb" | 2 | 3 | |
| 8 | 7 | 'b' | 'b' 在 map (idx 6) -> left=max(5, 6+1) | 7 | "b" | 1 | 3 |
最终返回 3。
3. 备选解法:滑动窗口 + 集合 (Set)
这种方法逻辑更直观,但代码稍微多一点点。它不记录索引,而是记录窗口内存在的字符。
思路:
- 使用一个
set存储当前窗口内的字符。 right向右移动。- 如果
s[right]在set里,说明重复了。此时不断移动left,并从set中移除s[left],直到s[right]不在set里为止。 - 将
s[right]加入set,更新最大长度。
优缺点:
- 优点:逻辑非常符合“滑动窗口”的直观定义(右边进,左边出)。
- 缺点:
left指针可能需要一步步移动,虽然总时间复杂度依然是 \(O(n)\),但在某些极端情况下(如大量重复字符),内部while循环执行次数较多。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 使用集合存储当前窗口中出现的字符
window_set = set()
left = 0
max_length = 0
for right in range(len(s)):
# 如果右边界字符已在窗口中,不断移动左边界直到移除该字符
while s[right] in window_set:
window_set.remove(s[left])
left += 1
# 将新字符加入窗口
window_set.add(s[right])
# 更新最大长度
max_length = max(max_length, right - left + 1)
return max_length
4. 不推荐解法:暴力枚举 (Brute Force)
为了让你理解为什么上面的方法好,我们看看暴力法。
思路:
检查所有的子串(双重循环),对每个子串判断是否有重复字符。
代码逻辑:
# 伪代码示意,不建议提交
max_len = 0
for i in range(len(s)):
for j in range(i + 1, len(s) + 1):
substring = s[i:j]
if len(substring) == len(set(substring)): # 判断无重复
max_len = max(max_len, len(substring))
为什么不行?
- 时间复杂度:\(O(n^3)\) 或 \(O(n^2)\)。
- 原因:题目中
s.length最大为 \(50,000\)。\(50000^2 = 2,500,000,000\) (25 亿) 次运算,这会导致超时 (Time Limit Exceeded)。 - 结论:在面试或实际工程中,对于 \(10^4\) 级别的数据,必须使用 \(O(n)\) 或 \(O(n \log n)\) 的算法。
5. 复杂度分析
针对最优解(滑动窗口 + 哈希表):
- 时间复杂度: \(O(n)\)
- 其中 \(n\) 是字符串的长度。
right指针遍历了整个字符串一次。left指针虽然会移动,但最多也只移动 \(n\) 次(不会回退)。- 哈希表的查找和插入操作平均为 \(O(1)\)。
- 空间复杂度: \(O(min(n, \Sigma))\)
- 其中 \(\Sigma\) 是字符集的大小。
- 哈希表存储字符索引。最坏情况下,字符串没有重复字符,哈希表存储 \(n\) 个字符。
- 如果字符集有限(例如只有 ASCII 128 个字符),则空间复杂度为 \(O(128) = O(1)\)。
6. 总结与面试建议
- 首选方案:在面试中,直接提出 滑动窗口 + 哈希表 的方案。它既高效又展示了你对数据结构的理解。
- 边界处理:代码中不需要特殊处理空字符串,因为
for循环不会执行,max_length初始为 0,直接返回 0,逻辑自洽。 - 易错点:
- 忘记
left = max(left, ...)中的max,导致left回退。 - 窗口长度计算错误,应该是
right - left + 1。 - 混淆子串(连续)和子序列(不连续)。
- 忘记
希望这份题解能帮你彻底搞懂这道题!如果有任何不清楚的地方,欢迎随时提问。
# 最终提交用的完整代码块
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 哈希表,记录字符最后一次出现的索引
char_index_map = {}
# 滑动窗口左边界
left = 0
# 记录最大长度
max_length = 0
# 遍历字符串,right 为右边界索引,char 为当前字符
for right, char in enumerate(s):
# 如果字符在 map 中且其索引在当前窗口内 (>= left)
# 实际上只需判断在 map 中,因为 left 更新逻辑会处理窗口范围
if char in char_index_map:
# 核心:左边界直接跳到重复字符的下一位
# 使用 max 防止左边界回退(例如 "abba" 的情况)
left = max(left, char_index_map[char] + 1)
# 更新字符的最新索引
char_index_map[char] = right
# 计算当前合法窗口长度,并更新全局最大值
max_length = max(max_length, right - left + 1)
return max_length

浙公网安备 33010602011771号