Files
interview/16-LeetCode Hot 100/无重复字符的最长子串.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

16 KiB
Raw Blame History

无重复字符的最长子串 (Longest Substring Without Repeating Characters)

LeetCode 3. Medium

题目描述

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

示例 1

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

示例 2

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

示例 3

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

思路推导

暴力解法分析

最直观的思路:枚举所有可能的子串,检查是否有重复字符。

def lengthOfLongestSubstring(s):
    max_len = 0
    n = len(s)

    for i in range(n):
        for j in range(i+1, n+1):
            substring = s[i:j]
            if len(set(substring)) == len(substring):  # 无重复
                max_len = max(max_len, j-i)

    return max_len

时间复杂度O(n³)

  • 外层循环O(n) 枚举起始位置
  • 内层循环O(n) 枚举结束位置
  • 检查重复O(n) 创建集合
  • 总计O(n) × O(n) × O(n) = O(n³)

空间复杂度O(min(m, n))m 为字符集大小

问题分析

  1. 效率太低n=10⁵ 时n³ 不可接受
  2. 重复计算:很多子串被多次检查
  3. 无法利用已知信息

优化思考 - 第一步:滑动窗口

观察:如果 s[i:j] 无重复,检查 s[j] 是否在窗口内

# 维护一个窗口 [left, right]
# 每次向右扩展 right
# 如果 s[right] 在窗口内重复,移动 left

为什么这样思考?

  • 窗口内的子串保证无重复
  • 只需要向右移动,不需要回溯
  • 每个字符最多被访问 2 次(进入和离开窗口)

优化后的思路

left = 0
max_len = 0
for right in range(len(s)):
    # 如果 s[right] 在窗口内,移动 left
    while s[right] in s[left:right]:
        left += 1
    max_len = max(max_len, right - left + 1)

时间复杂度O(n²)

  • 仍然有重复检查:s[right] in s[left:right] 是 O(n)

优化思考 - 第二步:哈希表优化

问题:如何快速判断字符是否在窗口内?

关键优化:用哈希表记录字符最后出现的位置

char_index = {}  # 字符 → 最后出现的位置
left = 0
max_len = 0

for right, char in enumerate(s):
    # 如果字符在窗口内,移动 left
    if char in char_index and char_index[char] >= left:
        left = char_index[char] + 1

    char_index[char] = right
    max_len = max(max_len, right - left + 1)

为什么这样思考?

  • 哈希表查找O(1)
  • 直接定位到重复字符的位置
  • left 可以跳跃式移动,不用逐个移动

时间复杂度O(n)

  • 每个字符只处理一次
  • 哈希表操作O(1)

优化思考 - 第三步:数组代替哈希表

进一步优化:如果字符集有限(如 ASCII用数组代替哈希表

char_index = [-1] * 128  # ASCII 字符集
left = 0
max_len = 0

for right, char in enumerate(s):
    char = ord(char)  # 转换为 ASCII 码
    if char_index[char] >= left:
        left = char_index[char] + 1

    char_index[char] = right
    max_len = max(max_len, right - left + 1)

优势

  • 数组访问比哈希表更快
  • 空间局部性更好cache 友好)
  • 适合字符集有限的情况

解题思路

核心思想

滑动窗口 + 哈希表:维护动态窗口,用哈希表记录字符位置。

为什么这样思考?

  1. 滑动窗口的原理

    • 窗口 [left, right] 内保证无重复字符
    • 右边界不断扩展
    • 左边界根据重复情况调整
  2. 哈希表的作用

    • 记录每个字符最后出现的位置
    • 快速判断重复字符是否在窗口内
    • 支持 O(1) 时间复杂度的查找和更新
  3. 关键判断

    • char_index[char] >= left:字符在窗口内
    • char_index[char] < left:字符在窗口外(已失效)

详细算法流程

步骤1初始化数据结构

char_index = {}  # 字符 → 最后出现的位置
left = 0        # 窗口左边界
max_len = 0     # 最长长度

作用

  • char_index:快速判断重复
  • left:标记当前窗口的起点
  • max_len:记录结果

步骤2遍历字符串

for right, char in enumerate(s):
    # 检查字符是否在窗口内
    if char in char_index and char_index[char] >= left:
        # 重复字符在窗口内,移动 left
        left = char_index[char] + 1

    # 更新字符位置
    char_index[char] = right

    # 更新最大长度
    max_len = max(max_len, right - left + 1)

关键点详解

  1. 为什么判断 char_index[char] >= left

    • 只关心重复字符是否在当前窗口内
    • 如果在窗口外,可以忽略
    • 示例:
      s = "a b c a"
      left = 0, right = 3
      char_index['a'] = 0 >= left → 重复left = 1
      
      s = "a b c a b c"
      left = 1, right = 5
      char_index['b'] = 1 >= left → 重复left = 2
      
      s = "a b c a b"
      left = 1, right = 4
      char_index['a'] = 0 < left → 不在窗口内,不移动
      
  2. 为什么 left = char_index[char] + 1

    • 跳过重复字符,包括重复字符本身
    • 新窗口从重复字符的下一位开始
    • 示例:
      s = "a b c a"
           0 1 2 3
      right = 3, char = 'a'
      char_index['a'] = 0
      left = 0 + 1 = 1
      新窗口:[1, 3] = "bca"
      
  3. 为什么先更新 left再更新 char_index

    • 必须先判断重复,再更新位置
    • 如果先更新,会覆盖旧位置
    • 错误示例:
      char_index[char] = right  # 错误!先更新了
      if char in char_index and char_index[char] >= left:
          left = char_index[char] + 1  # 永远成立
      

步骤3返回结果

return max_len

关键细节说明

细节1为什么用 enumerate 而不是 range

# 推荐写法:同时获取索引和字符
for right, char in enumerate(s):
    # ...

# 不推荐:需要额外索引
for i in range(len(s)):
    char = s[i]
    # ...

细节2为什么窗口长度是 right - left + 1

# 示例s = "abc"
left = 0, right = 2
窗口长度 = 2 - 0 + 1 = 3
索引[0, 1, 2]

# 为什么 +1
# 索引从 0 开始,需要 +1 才是实际长度

细节3为什么 char_index[char] >= left 而不是 > left

# 示例s = "abca"
left = 0, right = 3, char = 'a'
char_index['a'] = 0

# 如果用 > left
if char_index[char] > left:  # 0 > 0 → False
    # 不会移动 left错误

# 正确:用 >= left
if char_index[char] >= left:  # 0 >= 0 → True
    left = 1  # 正确!

细节4为什么需要两个条件判断

# 条件1字符是否出现过
if char in char_index:

# 条件2字符是否在窗口内
and char_index[char] >= left:

# 为什么都需要?
# 示例s = "abcabcbb"
# right = 3, char = 'a'
# char_index['a'] = 0 < left(1) → 不在窗口内
# 虽然出现过,但不在窗口内,可以保留

边界条件分析

边界1空字符串

输入s = ""
输出0
处理循环不执行max_len = 0

边界2全部相同字符

输入s = "bbbbb"
过程:
  right=0: char='b', left=0, max_len=1
  right=1: char='b', 重复, left=1, max_len=1
  right=2: char='b', 重复, left=2, max_len=1
  right=3: char='b', 重复, left=3, max_len=1
  right=4: char='b', 重复, left=4, max_len=1
输出1

边界3全部不同字符

输入s = "abcde"
过程:
  right=0: char='a', left=0, max_len=1
  right=1: char='b', left=0, max_len=2
  right=2: char='c', left=0, max_len=3
  right=3: char='d', left=0, max_len=4
  right=4: char='e', left=0, max_len=5
输出5

边界4重复字符在窗口外

输入s = "abca"
过程:
  right=0: char='a', left=0, max_len=1
  right=1: char='b', left=0, max_len=2
  right=2: char='c', left=0, max_len=3
  right=3: char='a', char_index['a']=0 < left=0? → False
           实际0 >= 0 → True, left=1, max_len=3

输入s = "abcabcbb"
过程:
  right=3: char='a', char_index['a']=0 >= left=0 → left=1
  right=4: char='b', char_index['b']=1 >= left=1 → left=2
  right=5: char='c', char_index['c']=2 >= left=2 → left=3
  right=6: char='b', char_index['b']=4 >= left=3 → left=5
  right=7: char='b', char_index['b']=6 >= left=5 → left=7
输出3

复杂度分析(详细版)

时间复杂度

- 外层循环O(n),遍历字符串
- 哈希表操作O(1),查找和更新
- 总计O(n)

为什么是 O(n)
- 每个字符最多被访问 2 次(进入和离开窗口)
- left 指针最多移动 n 次
- right 指针最多移动 n 次
- 总操作次数 = 2n = O(n)

空间复杂度

- 哈希表O(min(m, n))m 为字符集大小
  - ASCIIO(128) = O(1)
  - UnicodeO(n)
- 指针变量O(1)
- 总计O(min(m, n))

图解过程

字符串: "abcabcbb"

步骤1: [a]bcabcbb
       left=0, right=0, max_len=1
       char_index = {'a': 0}

步骤2: [a,b]cabcbb
       left=0, right=1, max_len=2
       char_index = {'a': 0, 'b': 1}

步骤3: [a,b,c]abcbb
       left=0, right=2, max_len=3
       char_index = {'a': 0, 'b': 1, 'c': 2}

步骤4: a[b,c,a]bcbb  (发现重复left移动)
       left=1, right=3, max_len=3
       char_index = {'a': 3, 'b': 1, 'c': 2}

步骤5: ab[c,a,b]cbb  (发现重复left移动)
       left=2, right=4, max_len=3
       char_index = {'a': 3, 'b': 4, 'c': 2}

步骤6: abc[a,b,c]bb  (发现重复left移动)
       left=3, right=5, max_len=3
       char_index = {'a': 3, 'b': 4, 'c': 5}

步骤7: abca[b,c,b]b  (发现重复left移动)
       left=5, right=6, max_len=3
       char_index = {'a': 3, 'b': 6, 'c': 5}

步骤8: abcab[c,b,b]  (发现重复left移动)
       left=7, right=7, max_len=3
       char_index = {'a': 3, 'b': 7, 'c': 5}

结果: max_len = 3

代码实现

方法1哈希表推荐

func lengthOfLongestSubstring(s string) int {
    // 记录字符最后出现的位置
    charIndex := make(map[rune]int)
    maxLength := 0
    left := 0

    for right, char := range s {
        // 如果字符已存在且在窗口内,移动左边界
        if idx, ok := charIndex[char]; ok && idx >= left {
            left = idx + 1
        }

        // 更新字符位置
        charIndex[char] = right

        // 更新最大长度
        if right - left + 1 > maxLength {
            maxLength = right - left + 1
        }
    }

    return maxLength
}

方法2数组优化ASCII

func lengthOfLongestSubstring(s string) int {
    // 使用数组代替哈希表,适用于 ASCII 字符集
    charIndex := [128]int{} // ASCII 字符集
    for i := range charIndex {
        charIndex[i] = -1
    }

    maxLength := 0
    left := 0

    for right := 0; right < len(s); right++ {
        char := s[right]

        // 如果字符已存在且在窗口内,移动左边界
        if charIndex[char] >= left {
            left = charIndex[char] + 1
        }

        // 更新字符位置
        charIndex[char] = right

        // 更新最大长度
        if right - left + 1 > maxLength {
            maxLength = right - left + 1
        }
    }

    return maxLength
}

执行过程演示

输入s = "abcabcbb"

初始化charIndex = {}, left = 0, max_len = 0

right=0, char='a':
  charIndex['a'] 不存在
  charIndex = {'a': 0}
  max_len = max(0, 0-0+1) = 1

right=1, char='b':
  charIndex['b'] 不存在
  charIndex = {'a': 0, 'b': 1}
  max_len = max(1, 1-0+1) = 2

right=2, char='c':
  charIndex['c'] 不存在
  charIndex = {'a': 0, 'b': 1, 'c': 2}
  max_len = max(2, 2-0+1) = 3

right=3, char='a':
  charIndex['a'] = 0 >= left(0) → 重复
  left = 0 + 1 = 1
  charIndex = {'a': 3, 'b': 1, 'c': 2}
  max_len = max(3, 3-1+1) = 3

right=4, char='b':
  charIndex['b'] = 1 >= left(1) → 重复
  left = 1 + 1 = 2
  charIndex = {'a': 3, 'b': 4, 'c': 2}
  max_len = max(3, 4-2+1) = 3

right=5, char='c':
  charIndex['c'] = 2 >= left(2) → 重复
  left = 2 + 1 = 3
  charIndex = {'a': 3, 'b': 4, 'c': 5}
  max_len = max(3, 5-3+1) = 3

right=6, char='b':
  charIndex['b'] = 4 >= left(3) → 重复
  left = 4 + 1 = 5
  charIndex = {'a': 3, 'b': 6, 'c': 5}
  max_len = max(3, 6-5+1) = 3

right=7, char='b':
  charIndex['b'] = 6 >= left(5) → 重复
  left = 6 + 1 = 7
  charIndex = {'a': 3, 'b': 7, 'c': 5}
  max_len = max(3, 7-7+1) = 3

结果max_len = 3

常见错误

错误1忘记判断字符是否在窗口内

错误代码

if idx, ok := charIndex[char]; ok {
    left = idx + 1  // 错误!可能在窗口外
}

正确代码

if idx, ok := charIndex[char]; ok && idx >= left {
    left = idx + 1  // 正确!只在窗口内时移动
}

原因

  • 示例s = "abcabcbb"
  • right=3, char='a', charIndex['a']=0, left=1
  • 0 < 1不在窗口内不应该移动 left

错误2更新 char_index 的时机错误

错误代码

for right, char := range s {
    charIndex[char] = right  // 错误!先更新了

    if idx, ok := charIndex[char]; ok && idx >= left {
        left = idx + 1  // 永远成立
    }
}

正确代码

for right, char := range s {
    if idx, ok := charIndex[char]; ok && idx >= left {
        left = idx + 1  // 先判断
    }

    charIndex[char] = right  // 再更新
}

原因

  • 先更新会覆盖旧位置
  • 导致判断永远成立

错误3窗口长度计算错误

错误代码

max_len = max(max_len, right - left)  // 错误!少了 +1

正确代码

max_len = max(max_len, right - left + 1)  // 正确

原因

  • 索引从 0 开始
  • 长度 = right - left + 1
  • 示例:[0, 2] 长度为 3不是 2

进阶问题

Q1: 如何返回最长子串本身?

func longestSubstring(s string) string {
    charIndex := make(map[rune]int)
    maxLength := 0
    left := 0
    start := 0 // 记录起始位置

    for right, char := range s {
        if idx, ok := charIndex[char]; ok && idx >= left {
            left = idx + 1
        }
        charIndex[char] = right

        if right - left + 1 > maxLength {
            maxLength = right - left + 1
            start = left
        }
    }

    return s[start : start+maxLength]
}

关键点

  • 记录最长子串的起始位置
  • 在更新 max_len 时同时更新 start

Q2: 如果字符集有限(如只有小写字母),如何优化?

优化:使用数组代替哈希表

func lengthOfLongestSubstring(s string) int {
    charIndex := [128]int{} // ASCII 字符集
    for i := range charIndex {
        charIndex[i] = -1
    }

    maxLength := 0
    left := 0

    for right := 0; right < len(s); right++ {
        char := s[right]
        if charIndex[char] >= left {
            left = charIndex[char] + 1
        }
        charIndex[char] = right
        maxLength = max(maxLength, right-left+1)
    }

    return maxLength
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

优势

  • 数组访问比哈希表更快
  • 空间局部性更好
  • 适合字符集有限的情况

P7 加分项

深度理解

  • 滑动窗口:维护动态窗口,左边界根据重复字符调整
  • 哈希表优化:数组 vs HashMap时间/空间权衡
  • 边界处理:重复字符在窗口外的情况

实战扩展

  • 流式数据:处理超大字符串或流式输入
  • 多线程:分段计算后合并
  • 业务场景:日志去重、用户行为分析

变形题目

  1. 159. 至多包含两个不同字符的最长子串
  2. 340. 至多包含 K 个不同字符的最长子串

总结

核心要点

  1. 滑动窗口:动态调整窗口边界
  2. 哈希表:记录字符位置,快速判断重复
  3. 双指针left 和 right 指针协同移动

易错点

  • 忘记判断重复字符是否在窗口内(idx >= left
  • 更新 left 的时机
  • 数组越界(使用数组代替哈希表时)

最优解法:滑动窗口 + 哈希表,时间 O(n),空间 O(min(m, n))