按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
733 lines
16 KiB
Markdown
733 lines
16 KiB
Markdown
# 无重复字符的最长子串 (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 为字符集大小
|
||
- ASCII:O(128) = O(1)
|
||
- Unicode:O(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))
|