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

778 lines
17 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.
# 子集 (Subsets)
## 题目描述
给你一个整数数组 `nums`,数组中的元素 **互不相同**。返回该数组所有可能的子集(幂集)。
解集 **不能** 包含重复的子集。你可以按 **任意顺序** 返回解集。
### 示例
**示例 1**
```
输入nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
```
**示例 2**
```
输入nums = [0]
输出:[[],[0]]
```
### 约束条件
- `1 <= nums.length <= 10`
- `-10 <= nums[i] <= 10`
- `nums` 中的所有元素 **互不相同**
## 思路推导
### 暴力解法分析
**第一步:直观思路 - 枚举所有可能的子集**
```python
def subsets_brute(nums):
n = len(nums)
result = []
# 遍历所有可能的子集掩码
for mask in range(1 << n): # 0 到 2^n - 1
subset = []
for i in range(n):
if mask & (1 << i): # 检查第 i 位是否为 1
subset.append(nums[i])
result.append(subset)
return result
```
**时间复杂度分析:**
- 有 2^n 个子集
- 每个子集需要 O(n) 时间构建
- **总时间复杂度O(n × 2^n)**
**问题:**
- 虽然时间复杂度已经是最优的,但位运算不易理解
- 代码可读性较差
- 难以扩展到带约束条件的子集问题
### 优化思考 - 如何更直观地生成子集?
**核心观察:**
1. **子集的本质**:对每个元素,都有"选"或"不选"两种选择
2. **决策树视角**n 个元素构成一个 n 层的决策树
3. **回溯法**:自然地表达这种"选择-撤销"的过程
**为什么用回溯?**
- 更直观地表达选择过程
- 易于剪枝(如有约束条件)
- 可以生成子集的同时进行处理
### 为什么这样思考?
**1. 二叉选择视角**
```
对于 [1,2,3]
[]
/ \
不选1 选1
[] [1]
/ \ / \
不选2 选2 不选2 选2
[] [2] [1] [1,2]
/ \ / \ / \ / \
3 [] 3 [2] ... (继续展开)
叶子节点就是所有子集
```
**2. 回溯法的优势**
```
- 每个节点代表一个决策点
- 自然地表达"选"或"不选"
- 可以在任意时刻处理当前子集
- 易于添加约束条件(如子集和限制)
```
## 解题思路
### 方法一:回溯法(推荐)
**核心思想:**对于每个元素,可以选择包含或不包含。使用回溯法生成所有可能的组合。
**算法步骤:**
1. 初始化结果数组和当前子集
2. 定义回溯函数 `backtrack(start)`
- 将当前子集加入结果
-`start` 开始遍历,依次尝试包含每个元素
- 递归调用后撤销选择(回溯)
### 详细算法流程
**步骤1理解回溯框架**
```python
def backtrack(start):
# 将当前子集加入结果
result.append(current[:])
# 从 start 开始尝试包含每个元素
for i in range(start, len(nums)):
# 选择当前元素
current.append(nums[i])
# 递归处理下一个元素
backtrack(i + 1)
# 撤销选择(回溯)
current.pop()
```
**Q: 为什么从 start 而不是从 0 开始?**
A: 避免重复生成相同的子集。举例:
```
nums = [1, 2]
如果每次都从 0 开始:
- 选 1: current=[1]
- 选 2: current=[1,2] ✓
- 选 1: current=[1,1] ✗ 重复!
如果从 start 开始:
- i=0: 选 1: current=[1]
- i=1: 选 2: current=[1,2] ✓
- i=1: 选 2: current=[2] ✓
```
**步骤2理解为何要加入空集**
```python
result.append(current[:]) # 在循环前就加入
```
**Q: 为什么在循环前就加入结果?**
A: 因为每个中间状态都是一个有效的子集。举例:
```
nums = [1, 2, 3]
执行过程:
1. current=[] → 加入 []
2. 选择 1: current=[1] → 加入 [1]
3. 选择 2: current=[1,2] → 加入 [1,2]
4. 选择 3: current=[1,2,3] → 加入 [1,2,3]
5. 回溯current=[1,2]
6. 回溯current=[1]
7. 选择 3: current=[1,3] → 加入 [1,3]
...
```
**步骤3理解回溯的撤销**
```python
current.append(nums[i]) # 做选择
backtrack(i + 1)
current.pop() # 撤销选择
```
**Q: 为什么必须撤销?**
A: 因为 `current` 是共享的列表,不撤销会影响后续递归。
举例说明撤销的重要性:
```
不撤销的情况:
- 选择 1: current=[1]
- 选择 2: current=[1,2],加入结果
- 回溯(但没有撤销)
- 选择 3: current=[1,2,3] ✗ 应该是 [1,3]
正确撤销:
- 选择 1: current=[1]
- 选择 2: current=[1,2],加入结果
- 回溯并撤销current=[1]
- 选择 3: current=[1,3] ✓
```
### 关键细节说明
**细节1为什么用 current[:] 而不是 current**
```python
# 错误写法
result.append(current) # 添加引用
# 正确写法
result.append(current[:]) # 添加副本
```
**为什么?**
- `current` 是可变列表,后续修改会影响已加入结果的数据
- `current[:]` 创建副本,保证结果不被修改
**细节2为什么循环从 start 开始?**
```python
for i in range(start, len(nums)): # 从 start 开始
current.append(nums[i])
backtrack(i + 1) # 下次从 i+1 开始
current.pop()
```
**为什么?**
- 避免重复:确保子集中的元素按原数组顺序出现
- 例如:[1,2] 会出现,但 [2,1] 不会出现
**细节3如何理解生成所有子集**
```
nums = [1, 2, 3]
回溯树:
[]
/ | \
不选2 选2 选3 (错误理解)
|
选3
正确理解(按顺序):
[]
/ | \
[1] [2] [3]
/ \ |
[1,2] [1,3] [2,3]
|
[1,2,3]
每个节点都是一个有效的子集!
```
### 边界条件分析
**边界1空数组**
```
输入nums = []
输出:[[]]
原因:空集是任何集合的子集
```
**边界2单个元素**
```
输入nums = [1]
输出:[[], [1]]
过程:
- 初始current=[],加入 []
- 选择 1current=[1],加入 [1]
```
**边界3所有元素相同**
```
输入nums = [1, 1, 1]
输出:[[], [1], [1,1], [1,1,1]]
注意:题目说元素互不相同,所以这种情况不会出现
```
### 复杂度分析(详细版)
**时间复杂度:**
```
- 子集数量2^n
- 每个子集的构建O(n)(最坏情况)
- **总时间复杂度O(n × 2^n)**
为什么每个子集是 O(n)
- 虽然子集长度不同,但平均长度是 n/2
- 总元素数 = 0×C(n,0) + 1×C(n,1) + ... + n×C(n,n) = n×2^(n-1)
- 平均每个子集的元素数 = n×2^(n-1) / 2^n = n/2
```
**空间复杂度:**
```
- 递归栈深度O(n)
- 存储结果O(n × 2^n)(所有子集的总元素数)
- **空间复杂度O(n)**(不计结果存储)
### 方法二:迭代法(位掩码)
**核心思想:**子集可以用二进制表示。对于 n 个元素,共有 2^n 个子集。
**算法步骤:**
1. 计算子集总数 `total = 1 << n`
2. 对于每个数字 `i` 从 0 到 `total-1`
- 将 `i` 的二进制表示转换为子集
- 第 `j` 位为 1 表示包含 `nums[j]`
### 方法三:级联法
**核心思想:**对于已有的每个子集,通过添加当前元素生成新的子集。
**算法步骤:**
1. 初始化结果为 `[[]]`
2. 对于每个元素:
- 取出所有已有子集
- 将当前元素添加到每个子集
- 将新子集加入结果
## 代码实现
### Go 实现(回溯法)
```go
package main
import "fmt"
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
// 将当前子集加入结果(需要复制)
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
// 从 start 开始尝试包含每个元素
for i := start; i < len(nums); i++ {
// 选择当前元素
current = append(current, nums[i])
// 递归处理下一个元素
backtrack(i + 1)
// 撤销选择(回溯)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
// 测试用例
func main() {
// 测试用例1
nums1 := []int{1, 2, 3}
fmt.Printf("输入: %v\n", nums1)
fmt.Printf("输出: %v\n", subsets(nums1))
// 测试用例2
nums2 := []int{0}
fmt.Printf("\n输入: %v\n", nums2)
fmt.Printf("输出: %v\n", subsets(nums2))
// 测试用例3
nums3 := []int{1, 2}
fmt.Printf("\n输入: %v\n", nums3)
fmt.Printf("输出: %v\n", subsets(nums3))
}
```
```go
func subsetsBitMask(nums []int) [][]int {
n := len(nums)
total := 1 << n // 2^n 个子集
result := make([][]int, 0, total)
for mask := 0; mask < total; mask++ {
subset := []int{}
for i := 0; i < n; i++ {
// 检查第 i 位是否为 1
if mask&(1<<i) != 0 {
subset = append(subset, nums[i])
}
}
result = append(result, subset)
}
return result
}
```
```go
func subsetsCascade(nums []int) [][]int {
result := [][]int{{}} // 初始化为空集
for _, num := range nums {
// 对于每个已有子集,添加当前元素生成新子集
newSubsets := make([][]int, 0, len(result))
for _, subset := range result {
newSubset := make([]int, len(subset)+1)
copy(newSubset, subset)
newSubset[len(subset)] = num
newSubsets = append(newSubsets, newSubset)
}
result = append(result, newSubsets...)
}
return result
}
```
## 复杂度分析
### 回溯法
- **时间复杂度:** O(n × 2^n)
- 共有 2^n 个子集
- 每个子集的复制需要 O(n) 时间
- **空间复杂度:** O(n)
- 递归栈深度最大为 n
- 不包括存储结果的空间
### 迭代法(位掩码)
- **时间复杂度:** O(n × 2^n)
- 需要生成 2^n 个子集
- 每个子集需要 O(n) 时间构建
- **空间复杂度:** O(1)
- 只使用了常数级别的额外空间(不包括结果)
### 级联法
- **时间复杂度:** O(n × 2^n)
- 每次迭代都会将子集数量翻倍
- 总共需要处理 n 次
- **空间复杂度:** O(n × 2^n)
- 需要存储所有子集
## 执行过程演示
`nums = [1, 2, 3]` 为例:
```
初始状态result=[], current=[], start=0
第1层递归 (start=0):
result=[[]] # 加入空集
循环i 从 0 到 2
i=0, nums[0]=1:
current=[1]
递归 backtrack(1)
├─ result=[[], [1]] # 加入 [1]
├─ i=1, nums[1]=2:
│ current=[1,2]
│ 递归 backtrack(2)
│ ├─ result=[[], [1], [1,2]] # 加入 [1,2]
│ ├─ i=2, nums[2]=3:
│ │ current=[1,2,3]
│ │ 递归 backtrack(3)
│ │ ├─ result=[[], [1], [1,2], [1,2,3]] # 加入 [1,2,3]
│ │ └─ 返回
│ │ current=[1,2] # 撤销 3
│ └─ 返回
│ current=[1] # 撤销 2
├─ i=2, nums[2]=3:
│ current=[1,3]
│ 递归 backtrack(3)
│ ├─ result=[[], [1], [1,2], [1,2,3], [1,3]] # 加入 [1,3]
│ └─ 返回
│ current=[1] # 撤销 3
└─ 返回
current=[] # 撤销 1
i=1, nums[1]=2:
current=[2]
递归 backtrack(2)
├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2]] # 加入 [2]
├─ i=2, nums[2]=3:
│ current=[2,3]
│ 递归 backtrack(3)
│ ├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3]] # 加入 [2,3]
│ └─ 返回
│ current=[2] # 撤销 3
└─ 返回
current=[] # 撤销 2
i=2, nums[2]=3:
current=[3]
递归 backtrack(3)
├─ result=[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]] # 加入 [3]
└─ 返回
current=[] # 撤销 3
最终结果:[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
```
## 常见错误
### 错误1忘记复制 current
**错误写法:**
```go
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
result = append(result, current) // 错误!添加引用
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
**正确写法:**
```go
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
temp := make([]int, len(current))
copy(temp, current) // 复制
result = append(result, temp)
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
**原因:**Go 中切片是引用类型,直接添加会导致所有结果都是同一个切片的引用。
### 错误2循环从 0 开始而不是 start
**错误写法:**
```go
for i := 0; i < len(nums); i++ { // 错误:从 0 开始
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
```
**正确写法:**
```go
for i := start; i < len(nums); i++ { // 正确:从 start 开始
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
```
**原因:**会导致重复生成相同的子集。
### 错误3忘记撤销选择
**错误写法:**
```go
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
// 忘记撤销
}
```
**正确写法:**
```go
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1] // 必须撤销
}
```
**原因:**不撤销会导致后续递归使用错误的 current。
## 进阶问题
### Q1: 如果数组中有重复元素,应该如何处理?
**A:** 需要先排序,然后在回溯时跳过重复元素。
```go
func subsetsWithDup(nums []int) [][]int {
sort.Ints(nums)
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
for i := start; i < len(nums); i++ {
// 跳过重复元素
if i > start && nums[i] == nums[i-1] {
continue
}
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
### Q2: 如果要求子集的大小恰好为 k应该如何修改
**A:** 在回溯时添加终止条件。
```go
func subsetsK(nums []int, k int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
if len(current) == k {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
return
}
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
## P7 加分项
### 1. 深度理解:为什么子集问题适合用回溯法?
**回溯法的本质:**
- 在解空间树中进行深度优先搜索
- 每个节点代表一个决策(包含或不包含当前元素)
- 通过撤销选择(回溯)来探索所有可能
**为什么适合子集问题:**
1. **决策清晰:**每个元素只有两种选择(包含或不包含)
2. **无后效性:**当前选择不影响之前的选择
3. **边界明确:**子集大小从 0 到 n
### 2. 实战扩展:组合与排列
**组合问题:**从 n 个元素中选 k 个,不考虑顺序
**排列问题:**从 n 个元素中选 k 个,考虑顺序
```go
// 组合
func combine(n int, k int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
if len(current) == k {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
return
}
for i := start; i <= n; i++ {
current = append(current, i)
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(1)
return result
}
// 排列
func permute(nums []int) [][]int {
result := [][]int{}
current := []int{}
used := make([]bool, len(nums))
var backtrack func()
backtrack = func() {
if len(current) == len(nums) {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
return
}
for i := 0; i < len(nums); i++ {
if used[i] {
continue
}
current = append(current, nums[i])
used[i] = true
backtrack()
current = current[:len(current)-1]
used[i] = false
}
}
backtrack()
return result
}
```
### 3. 变形题目
#### 变形1子集 II有重复元素
**LeetCode 90:** 给定一个可能包含重复元素的整数数组 nums返回该数组所有可能的子集幂集
```go
func subsetsWithDup(nums []int) [][]int {
sort.Ints(nums)
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
for i := start; i < len(nums); i++ {
if i > start && nums[i] == nums[i-1] {
continue
}
current = append(current, nums[i])
backtrack(i + 1)
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
```
### 4. 相关题目推荐
- LeetCode 78: 子集(本题)
- LeetCode 90: 子集 II
- LeetCode 77: 组合
- LeetCode 46: 全排列
- LeetCode 47: 全排列 II