跳到主要内容

3. 无重复字符的最长子串

题目描述

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

示例

示例 1:

输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。
注意:答案必须是子串,"pwke" 是子序列,不是子串。

解题思路

第一步:理解问题本质

这是一个典型的滑动窗口问题:

  • 需要找到一个连续的子串
  • 子串内不能有重复字符
  • 求满足条件的子串的最大长度

第二步:暴力解法

思路:枚举所有子串,检查是否有重复字符,记录最大长度。

class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
n = len(s)
max_len = 0

for i in range(n):
seen = set()
for j in range(i, n):
if s[j] in seen:
break
seen.add(s[j])
max_len = max(max_len, j - i + 1)

return max_len

缺点:时间复杂度 O(n²),效率太低。

第三步:优化解法 —— 滑动窗口

核心洞察

  • 使用左右两个指针表示窗口的边界
  • 右指针不断向右扩展窗口
  • 当遇到重复字符时,收缩左指针直到窗口内无重复
  • 使用哈希表记录字符位置,快速判断重复

算法步骤

  1. left:窗口左边界,i:窗口右边界(当前遍历位置)
  2. 哈希表记录每个字符最后一次出现的索引
  3. 遍历字符串:
    • 如果当前字符未出现过,窗口直接扩展
    • 如果出现过,收缩左边界到重复字符的下一个位置
    • 更新当前字符的位置
    • 更新最大长度

为什么正确

  • 哈希表记录字符最后出现的位置,可以在 O(1) 时间内判断重复
  • 左边界只能右移不能左移,保证滑动窗口的正确性
  • 每个字符最多被访问两次(被右指针和左指针各一次)

完整代码实现

class Solution:
"""
无重复字符的最长子串 - 滑动窗口

核心思想:
用滑动窗口维护一个不包含重复字符的子串,窗口右边界不断右移扩展,
遇到重复字符时收缩左边界,保证窗口内无重复字符。

为什么用哈希表?
哈希表记录每个字符最后一次出现的位置,可以在 O(1) 时间内判断字符是否重复,
并快速确定左边界应该移动到哪里。

滑动窗口的关键:
1. left:窗口左边界
2. i:窗口右边界(当前遍历位置)
3. hash_dict:记录字符最后一次出现的索引

时间复杂度:O(n),每个字符只访问一次
空间复杂度:O(min(m,n)),m 是字符集大小
"""

def lengthOfLongestSubstring(self, s: str) -> int:
hash_dict = {} # 记录字符最后一次出现的索引
n = len(s)
left, ans, result = 0, 0, 0 # left:左边界, ans:当前窗口长度, result:最大长度

for i in range(0, n):
if hash_dict.get(s[i], -1) == -1:
# 字符未出现过,窗口直接扩展
ans += 1
else:
# 字符出现过,需要收缩左边界
# 注意:left 只能右移不能左移(取 max 保证)
if left < hash_dict[s[i]]:
left = hash_dict[s[i]]
ans = i - left # 重新计算当前窗口长度

hash_dict[s[i]] = i # 更新字符位置
result = max(ans, result) # 更新最大长度

return result

示例推演

s = "abcabcbb" 为例:

is[i]hash_dictleftansresult说明
0'a'{}011首次出现,窗口扩展
1'b'{'a':0}022首次出现,窗口扩展
2'c'{'a':0,'b':1}033首次出现,窗口扩展
3'a'{'a':0,'b':1,'c':2}0→123'a'重复,left移到1
4'b'{'a':3,'b':1,'c':2}1→223'b'重复,left移到2
5'c'{'a':3,'b':4,'c':2}2→323'c'重复,left移到3
6'b'{'a':3,'b':4,'c':5}3→513'b'重复,left移到5
7'b'{'a':3,'b':6,'c':5}5→703'b'重复,left移到7

结果:3(最长子串为 "abc")

再以一个特殊例子 s = "abba" 为例:

is[i]hash_dictleft说明
0'a'{}0-
1'b'{'a':0}0-
2'b'{'a':0,'b':1}0→2'b'重复,left移到2
3'a'{'a':0,'b':2}2hash_dict['a']=0 < left=2,不更新left

关键点:当 hash_dict[s[i]] < left 时,说明这个字符虽然之前出现过,但已经在窗口外了,不需要收缩左边界。这就是为什么代码中要判断 if left < hash_dict[s[i]]


复杂度分析

解法时间复杂度空间复杂度说明
暴力O(n²)O(min(m,n))枚举所有子串
滑动窗口(最优)O(n)O(min(m,n))m是字符集大小

易错点总结

1. 左边界更新要取 max

# 错误写法
left = hash_dict[s[i]]

# 正确写法
if left < hash_dict[s[i]]:
left = hash_dict[s[i]]

原因:左边界只能右移,如果重复字符在窗口左边界的左边,不需要更新左边界。

2. 窗口长度的计算

ans = i - left  # 不是 i - left + 1

原因:left 是重复字符的索引,新的窗口应该从重复字符的下一个位置开始,所以是 i - left

3. 先更新结果还是先更新哈希表?

先更新哈希表,再计算结果。因为当前字符已经算入窗口。


扩展思考

1. 如何返回最长子串本身?

记录最大长度时,同时记录起始位置,最后截取子串。

2. 如果最多允许 k 个重复字符?

使用一个计数器记录窗口内每个字符的出现次数,当不同字符的数量超过 k 时收缩窗口。

3. 滑动窗口的通用模板

def sliding_window(s):
left = 0
window = {}
for right in range(len(s)):
# 扩大窗口,加入 s[right]
window[s[right]] = window.get(s[right], 0) + 1

# 收缩窗口的条件
while 需要收缩:
# 移除 s[left]
window[s[left]] -= 1
left += 1

# 更新结果
update_result()

相关题目

加载评论中...