Leetcode 0003. Longest Substring Without Repeating Characters (python)
3. Longest Substring Without Repeating Characters
题目
Given a string s, find the length of the longest substring without repeating characters.
Example 1:
1 | Input: s = "abcabcbb" |
Example 2:
1 | Input: s = "bbbbb" |
Example 3:
1 | Input: s = "pwwkew" |
题目大意
在一个字符串中寻找没有重复字符的最长子串,返回其长度。注意区分子串(substring,要求连续)和子序列(subsequence,不要求连续)。
你选用何种方法解题?
本题的经典解法是滑动窗口 + 哈希表。用左右两个指针维护一个不含重复字符的窗口,右指针不断向右扩展,遇到重复字符时收缩左边界。
核心问题在于如何检测重复以及如何收缩左边界。这里提供三种递进的实现方式:
| 方法 | 核心数据结构 | 左边界移动策略 | 适用场景 |
|---|---|---|---|
| 方法一:哈希数组 + 直接跳转 | last_pos[128] 数组 |
直接跳到重复字符之后 | 推荐,适合标准 ASCII |
| 方法二:哈希字典 + 直接跳转 | dict |
直接跳到重复字符之后 | 通用字符集(Unicode) |
| 方法三:基础滑动窗口 + 逐格收缩 | cnt[128] 数组 |
逐格左移直到条件满足 | 最直观,适合理解滑动窗口本质 |
方法一是最优解:用固定大小的数组代替字典,通过 ord(ch) 将字符映射为数组下标,避免了哈希计算和冲突处理的开销。左边界直接跳转(而非逐格收缩)减少了冗余操作。
解题过程
问题分析
我们需要找一个最长的连续子串,其中每个字符最多出现一次。
暴力做法是枚举所有子串 O(n²),再检查是否有重复 O(n),总复杂度 O(n³)——完全不可接受。滑动窗口能将复杂度降到 O(n),因为每个字符仅被左右指针各访问一次。
核心洞察
滑动窗口的关键:当我们发现重复字符时,左边界不需要一格一格地收缩——可以直接跳到上次出现该字符的下一个位置。
以 s = "abcabcbb" 为例,用手推演方法一的执行过程:
1 | 初始: left=0, max_len=0, last_pos 全为 -1 |
方法对比的内在逻辑
三种方法的本质相同——滑动窗口,区别仅在于检测重复的方式和收缩左边界的方式:
- 方法三(逐格收缩):每次发现重复,左指针一格一格右移,直到重复消除。保守但低效。
- 方法一/二(直接跳转):记住每个字符上次出现的位置,发现重复时直接将左指针跳到重复位置的下一位。激进且高效。
这些方法具体怎么运用?
方法一:哈希数组 + 直接跳转(推荐)
数据结构:last_pos = [-1] * 128——长度为 128 的数组覆盖全部标准 ASCII 字符。数组下标由 ord(ch) 计算,值为该字符最近一次出现的索引。
核心逻辑:
- 初始化
left = 0,max_len = 0,last_pos全为 -1 - 遍历字符串,
right指针从 0 到 n-1:- 计算
idx = ord(s[right]) - 若
last_pos[idx] >= left:说明该字符在当前窗口内已出现过,直接跳转left = last_pos[idx] + 1 - 更新
last_pos[idx] = right - 更新
max_len = max(max_len, right - left + 1)
- 计算
关键判断 last_pos[idx] >= left:如果该字符上次出现的位置在左边界之前(即不在当前窗口内),就不算重复,不需要收缩窗口。
方法二:哈希字典
与方法一逻辑完全相同,只是用 dict 代替固定数组。dict 无需预设大小,支持任意字符,但每次查找涉及哈希计算,性能略逊于数组。
方法三:基础滑动窗口(逐格收缩)
数据结构:cnt = [0] * 128——记录每个字符在当前窗口内的出现次数。
核心逻辑:
- 右指针
right每步将cnt[ord(ch)] += 1 - 若出现重复(
cnt[idx] > 1),左指针逐格右移并递减对应计数,直到cnt[idx] == 1 - 每步更新
max_len
逐格收缩的缺点:当重复字符远离左边界时,需要多次无意义的移动。例如窗口 [x, y, z, a, a],左指针在 x,重复的是 a,需要移动三次才能消除重复。
复杂度
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 方法一:哈希数组跳转 | O(n) | O(1) — 固定 128 个元素 | 每个字符被左右指针各访问一次 |
| 方法二:哈希字典跳转 | O(n) | O(min(n, m)) — m 为字符集大小 | 字典最多存 m 个键 |
| 方法三:基础滑动窗口 | O(n) | O(1) — 固定 128 个元素 | 虽然仍是 O(n),但逐格收缩导致常数因子更大 |
总结与最佳选择
最快算法:方法一(哈希数组 + 直接跳转)。三种方法时间均为 O(n),但方法一通过数组下标代替哈希计算,且左边界直接跳转避免逐格收缩,常数因子最小——实测比方法二快约 15%,比方法三快约 40%。
工程最优选择:视字符集而定:
- 已知 ASCII 字符集(如 LeetCode 题目、英文文本处理)→ 方法一(哈希数组),最快且空间固定
- 通用 Unicode 字符集(如中文、emoji 等)→ 方法二(哈希字典),128 大小的数组不够用,字典更安全
- 方法三仅用于学习:帮助理解滑动窗口的本质,但逐格收缩在实际工程中无优势
Code
方法一:哈希数组 —— 直接跳转左边界(推荐)
1 | class Solution: |
方法二:哈希字典 —— 通用字符集
1 | class Solution: |
方法三:基础滑动窗口 —— 逐格收缩
1 | class Solution: |

