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

733 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 无重复字符的最长子串 (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" 是一个子序列,不是子串。
```
## 思路推导
### 暴力解法分析
**最直观的思路**:枚举所有可能的子串,检查是否有重复字符。
```python
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] 是否在窗口内
```python
# 维护一个窗口 [left, right]
# 每次向右扩展 right
# 如果 s[right] 在窗口内重复,移动 left
```
**为什么这样思考?**
- 窗口内的子串保证无重复
- 只需要向右移动,不需要回溯
- 每个字符最多被访问 2 次(进入和离开窗口)
**优化后的思路**
```python
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)
### 优化思考 - 第二步:哈希表优化
**问题**:如何快速判断字符是否在窗口内?
**关键优化**:用哈希表记录字符最后出现的位置
```python
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用数组代替哈希表
```python
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初始化数据结构**
```python
char_index = {} # 字符 → 最后出现的位置
left = 0 # 窗口左边界
max_len = 0 # 最长长度
```
**作用**
- `char_index`:快速判断重复
- `left`:标记当前窗口的起点
- `max_len`:记录结果
**步骤2遍历字符串**
```python
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**
- 必须先判断重复,再更新位置
- 如果先更新,会覆盖旧位置
- 错误示例:
```python
char_index[char] = right # 错误!先更新了
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1 # 永远成立
```
**步骤3返回结果**
```python
return max_len
```
### 关键细节说明
**细节1为什么用 `enumerate` 而不是 `range`**
```python
# 推荐写法:同时获取索引和字符
for right, char in enumerate(s):
# ...
# 不推荐:需要额外索引
for i in range(len(s)):
char = s[i]
# ...
```
**细节2为什么窗口长度是 `right - left + 1`**
```python
# 示例s = "abc"
left = 0, right = 2
窗口长度 = 2 - 0 + 1 = 3
索引:[0, 1, 2]
# 为什么 +1
# 索引从 0 开始,需要 +1 才是实际长度
```
**细节3为什么 `char_index[char] >= left` 而不是 `> left`**
```python
# 示例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为什么需要两个条件判断**
```python
# 条件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哈希表推荐
```go
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
```go
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忘记判断字符是否在窗口内
❌ **错误代码**
```go
if idx, ok := charIndex[char]; ok {
left = idx + 1 // 错误!可能在窗口外
}
```
✅ **正确代码**
```go
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 的时机错误
❌ **错误代码**
```go
for right, char := range s {
charIndex[char] = right // 错误!先更新了
if idx, ok := charIndex[char]; ok && idx >= left {
left = idx + 1 // 永远成立
}
}
```
✅ **正确代码**
```go
for right, char := range s {
if idx, ok := charIndex[char]; ok && idx >= left {
left = idx + 1 // 先判断
}
charIndex[char] = right // 再更新
}
```
**原因**
- 先更新会覆盖旧位置
- 导致判断永远成立
---
### 错误3窗口长度计算错误
❌ **错误代码**
```go
max_len = max(max_len, right - left) // 错误!少了 +1
```
✅ **正确代码**
```go
max_len = max(max_len, right - left + 1) // 正确
```
**原因**
- 索引从 0 开始
- 长度 = right - left + 1
- 示例:[0, 2] 长度为 3不是 2
---
## 进阶问题
### Q1: 如何返回最长子串本身?
```go
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: 如果字符集有限(如只有小写字母),如何优化?
**优化**:使用数组代替哈希表
```go
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. 至多包含两个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/)
2. [340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/)
---
## 总结
**核心要点**
1. **滑动窗口**:动态调整窗口边界
2. **哈希表**:记录字符位置,快速判断重复
3. **双指针**left 和 right 指针协同移动
**易错点**
- 忘记判断重复字符是否在窗口内(`idx >= left`
- 更新 left 的时机
- 数组越界(使用数组代替哈希表时)
**最优解法**:滑动窗口 + 哈希表,时间 O(n),空间 O(min(m, n))