feat: add 19 LeetCode Hot 100 medium problems with detailed solutions
批量生成 19 道 LeetCode Hot 100 Medium 难度题目,每道题包含: - 题目描述和示例 - 多种解题思路(回溯、DP、双指针等) - Go 和 Java 双语解答 - 完整的测试用例 - 复杂度分析 - 进阶问题 - P7 加分项(深度理解、实战扩展、变形题目) 新增题目: 1. 盛最多水的容器 (Container With Most Water) - LeetCode 11 2. 电话号码的字母组合 (Letter Combinations) - LeetCode 17 3. 删除链表的倒数第N个结点 - LeetCode 19 4. 括号生成 - LeetCode 22 5. 最长回文子串 - LeetCode 5 6. 子集 - LeetCode 78 7. 单词搜索 - LeetCode 79 8. 柱状图中最大的矩形 - LeetCode 84 9. 最大正方形 - LeetCode 221 10. 完全平方数 - LeetCode 279 11. 最长连续序列 - LeetCode 128 12. 除自身以外数组的乘积 - LeetCode 238 13. 最小栈 - LeetCode 155 14. 二叉树的中序遍历 - LeetCode 94 15. 二叉树的最大深度 - LeetCode 104 16. 翻转二叉树 - LeetCode 226 17. 对称二叉树 - LeetCode 101 18. 路径总和 - LeetCode 112 19. 从前序与中序遍历序列构造二叉树 - LeetCode 105 所有代码均包含: - 清晰的注释说明 - 完整的可运行测试用例 - 时间和空间复杂度分析 - 优化技巧和变形题目 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
448
16-LeetCode Hot 100/子集.md
Normal file
448
16-LeetCode Hot 100/子集.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 子集 (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` 中的所有元素 **互不相同**
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:回溯法(推荐)
|
||||
|
||||
**核心思想:**对于每个元素,可以选择包含或不包含。使用回溯法生成所有可能的组合。
|
||||
|
||||
**算法步骤:**
|
||||
1. 初始化结果数组和当前子集
|
||||
2. 定义回溯函数 `backtrack(start)`:
|
||||
- 将当前子集加入结果
|
||||
- 从 `start` 开始遍历,依次尝试包含每个元素
|
||||
- 递归调用后撤销选择(回溯)
|
||||
|
||||
### 方法二:迭代法(位掩码)
|
||||
|
||||
**核心思想:**子集可以用二进制表示。对于 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))
|
||||
}
|
||||
```
|
||||
|
||||
### Java 实现(回溯法)
|
||||
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Subsets {
|
||||
|
||||
public List<List<Integer>> subsets(int[] nums) {
|
||||
List<List<Integer>> result = new ArrayList<>();
|
||||
List<Integer> current = new ArrayList<>();
|
||||
backtrack(result, current, nums, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void backtrack(List<List<Integer>> result, List<Integer> current,
|
||||
int[] nums, int start) {
|
||||
// 将当前子集加入结果
|
||||
result.add(new ArrayList<>(current));
|
||||
|
||||
// 从 start 开始尝试包含每个元素
|
||||
for (int i = start; i < nums.length; i++) {
|
||||
// 选择当前元素
|
||||
current.add(nums[i]);
|
||||
// 递归处理下一个元素
|
||||
backtrack(result, current, nums, i + 1);
|
||||
// 撤销选择(回溯)
|
||||
current.remove(current.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
public static void main(String[] args) {
|
||||
Subsets solution = new Subsets();
|
||||
|
||||
// 测试用例1
|
||||
int[] nums1 = {1, 2, 3};
|
||||
System.out.println("输入: [1, 2, 3]");
|
||||
System.out.println("输出: " + solution.subsets(nums1));
|
||||
|
||||
// 测试用例2
|
||||
int[] nums2 = {0};
|
||||
System.out.println("\n输入: [0]");
|
||||
System.out.println("输出: " + solution.subsets(nums2));
|
||||
|
||||
// 测试用例3
|
||||
int[] nums3 = {1, 2};
|
||||
System.out.println("\n输入: [1, 2]");
|
||||
System.out.println("输出: " + solution.subsets(nums3));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Go 实现(迭代法-位掩码)
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
### Java 实现(迭代法-位掩码)
|
||||
|
||||
```java
|
||||
public List<List<Integer>> subsetsBitMask(int[] nums) {
|
||||
int n = nums.length;
|
||||
int total = 1 << n; // 2^n 个子集
|
||||
List<List<Integer>> result = new ArrayList<>();
|
||||
|
||||
for (int mask = 0; mask < total; mask++) {
|
||||
List<Integer> subset = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
// 检查第 i 位是否为 1
|
||||
if ((mask & (1 << i)) != 0) {
|
||||
subset.add(nums[i]);
|
||||
}
|
||||
}
|
||||
result.add(subset);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Go 实现(级联法)
|
||||
|
||||
```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)
|
||||
- 需要存储所有子集
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### 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
|
||||
Reference in New Issue
Block a user