LeetCode 3 无重复字符的最长子串:python3 题解



1. 题目含义解析

目标:在一个字符串中,找到一段连续的字符,这段字符里没有任何重复的字母,且这段字符的长度要尽可能长。

关键点

  1. 子串 (Substring):必须是连续的。例如 "pwwkew" 中,"wke" 是子串,但 "pwke" 不是(因为跳过了中间的 w)。
  2. 无重复:窗口内的字符不能出现两次。
  3. 返回长度:不需要返回具体的子串,只需要返回它的长度。

示例分析

  • 输入 "abcabcbb"
    • 无重复子串有 "abc", "bca", "cab" 等。
    • 最长的是 "abc",长度为 3
  • 输入 "bbbbb"
    • 任何包含两个 b 的串都有重复。
    • 最长只能是 "b",长度为 1

2. 核心解法:滑动窗口 + 哈希表 (最优解)

这是最推荐的方法,时间复杂度为 \(O(n)\),效率最高。

2.1 思路图解

想象你有一个可以伸缩的窗口(框),框住了字符串的一部分。
我们有两个指针:

  • left:窗口的左边界。
  • right:窗口的右边界。

操作流程

  1. right 指针不断向右移动,把新字符纳入窗口。
  2. 每次纳入新字符前,检查这个字符是否已经在窗口内出现过。
  3. 如果没有重复:窗口扩大,更新最大长度。
  4. 如果有重复:说明当前窗口不合法了。我们需要缩小窗口,将 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)

这种方法逻辑更直观,但代码稍微多一点点。它不记录索引,而是记录窗口内存在的字符。

思路

  1. 使用一个 set 存储当前窗口内的字符。
  2. right 向右移动。
  3. 如果 s[right]set 里,说明重复了。此时不断移动 left,并从 set 中移除 s[left],直到 s[right] 不在 set 里为止。
  4. 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. 总结与面试建议

  1. 首选方案:在面试中,直接提出 滑动窗口 + 哈希表 的方案。它既高效又展示了你对数据结构的理解。
  2. 边界处理:代码中不需要特殊处理空字符串,因为 for 循环不会执行,max_length 初始为 0,直接返回 0,逻辑自洽。
  3. 易错点
    • 忘记 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


posted @ 2026-03-03 16:49  MoonOut  阅读(3)  评论(0)    收藏  举报