Compare commits
7 Commits
bc0ed186c8
...
4247e0700d
| Author | SHA1 | Date | |
|---|---|---|---|
| 4247e0700d | |||
| 58b7491868 | |||
| 15dbd75004 | |||
| 184f388a45 | |||
| f138f9649d | |||
| e75e4778b1 | |||
| dcd3e136ec |
48
.obsidian/workspace.json
vendored
48
.obsidian/workspace.json
vendored
@@ -196,41 +196,41 @@
|
||||
},
|
||||
"active": "fcbc762a80282002",
|
||||
"lastOpenFiles": [
|
||||
"16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md",
|
||||
"16-LeetCode Hot 100/路径总和.md",
|
||||
"16-LeetCode Hot 100/对称二叉树.md",
|
||||
"16-LeetCode Hot 100/翻转二叉树.md",
|
||||
"16-LeetCode Hot 100/二叉树的最大深度.md",
|
||||
"16-LeetCode Hot 100/二叉树的中序遍历.md",
|
||||
"16-LeetCode Hot 100/最小栈.md",
|
||||
"16-LeetCode Hot 100/除自身以外数组的乘积.md",
|
||||
"16-LeetCode Hot 100/最长连续序列.md",
|
||||
"16-LeetCode Hot 100/完全平方数.md",
|
||||
"16-LeetCode Hot 100/最大正方形.md",
|
||||
"16-LeetCode Hot 100/柱状图中最大的矩形.md",
|
||||
"16-LeetCode Hot 100/单词搜索.md",
|
||||
"16-LeetCode Hot 100/子集.md",
|
||||
"16-LeetCode Hot 100/最长回文子串.md",
|
||||
"16-LeetCode Hot 100/括号生成.md",
|
||||
"16-LeetCode Hot 100/删除链表的倒数第N个结点.md",
|
||||
"16-LeetCode Hot 100/电话号码的字母组合.md",
|
||||
"16-LeetCode Hot 100/盛最多水的容器.md",
|
||||
"16-LeetCode Hot 100/三数之和.md",
|
||||
"16-LeetCode Hot 100/无重复字符的最长子串.md",
|
||||
"16-LeetCode Hot 100/两数相加.md",
|
||||
"16-LeetCode Hot 100",
|
||||
"00-项目概述/项目概述.md",
|
||||
"00-项目概述",
|
||||
"questions/04-消息队列/消息队列_RocketMQ_Kafka.md",
|
||||
"questions/05-并发编程/ConcurrentHashMap原理.md",
|
||||
"questions/02-数据库/MySQL主从延迟.md",
|
||||
"questions/02-数据库/分库分表.md",
|
||||
"questions/02-数据库/数据库锁机制.md",
|
||||
"questions/02-数据库/事务隔离级别.md",
|
||||
"questions/14-Web3与区块链/Golang与区块链开发.md",
|
||||
"questions/02-数据库/MyBatis核心原理.md",
|
||||
"questions/02-数据库/MySQL索引优化.md",
|
||||
"questions/01-分布式系统/MySQL主从延迟.md",
|
||||
"questions/01-分布式系统/CAP理论和BASE理论.md",
|
||||
"questions/01-分布式系统/一致性哈希.md",
|
||||
"questions/01-分布式系统/数据库锁机制.md",
|
||||
"questions/01-分布式系统/数据库分库分表.md",
|
||||
"questions/01-分布式系统/事务隔离级别.md",
|
||||
"questions/01-分布式系统/分布式ID生成.md",
|
||||
"questions/15-简历面试/场景设计题.md",
|
||||
"questions/15-简历面试/个人发展题.md",
|
||||
"questions/15-简历面试/离职原因与动机.md",
|
||||
"questions/15-简历面试/项目深挖题.md",
|
||||
"questions/15-简历面试/README.md",
|
||||
"questions/15-简历面试/薪资谈判.md",
|
||||
"questions/15-简历面试",
|
||||
"questions/01-分布式系统/分布式锁.md",
|
||||
"questions/14-Web3与区块链/README.md",
|
||||
"questions/14-Web3与区块链/简历项目Web3迁移.md",
|
||||
"questions/14-Web3与区块链",
|
||||
"12-面试技巧",
|
||||
"08-算法与数据结构",
|
||||
"questions/13-Golang语言",
|
||||
"questions/12-面试技巧",
|
||||
"questions/11-运维",
|
||||
"questions/10-中间件",
|
||||
"questions/09-网络与安全"
|
||||
"questions/10-中间件"
|
||||
]
|
||||
}
|
||||
195
16-LeetCode Hot 100/三数之和.md
Normal file
195
16-LeetCode Hot 100/三数之和.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 三数之和 (3Sum)
|
||||
|
||||
LeetCode 15. Medium
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个整数数组 `nums`,判断是否存在三元组 `[nums[i], nums[j], nums[k]]` 满足 `i != j`、`i != k` 且 `j != k`,同时还满足 `nums[i] + nums[j] + nums[k] == 0`。
|
||||
|
||||
请你返回所有和为 0 且不重复的三元组。
|
||||
|
||||
**注意**:答案中不可以包含重复的三元组。
|
||||
|
||||
**示例 1**:
|
||||
```
|
||||
输入:nums = [-1,0,1,2,-1,-4]
|
||||
输出:[[-1,-1,2],[-1,0,1]]
|
||||
解释:
|
||||
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0
|
||||
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0
|
||||
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0
|
||||
不同的三元组是 [-1,0,1] 和 [-1,-1,2]
|
||||
注意,输出的顺序和三元组的顺序并不重要。
|
||||
```
|
||||
|
||||
**示例 2**:
|
||||
```
|
||||
输入:nums = [0,1,1]
|
||||
输出:[]
|
||||
解释:唯一可能的三元组和不为 0
|
||||
```
|
||||
|
||||
**示例 3**:
|
||||
```
|
||||
输入:nums = [0,0,0]
|
||||
输出:[[0,0,0]]
|
||||
解释:唯一可能的三元组和为 0
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 核心思想
|
||||
**排序 + 双指针**:先排序,固定第一个数,再用双指针找后两个数。
|
||||
|
||||
### 算法流程
|
||||
1. **排序数组**:便于去重和双指针操作
|
||||
2. **遍历第一个数**:
|
||||
- 跳过重复元素
|
||||
- 如果当前数 > 0,直接退出(后面都 > 0)
|
||||
3. **双指针找后两个数**:
|
||||
- left = i + 1, right = len(nums) - 1
|
||||
- 根据 sum 与 0 的关系移动指针
|
||||
- 跳过重复元素
|
||||
|
||||
### 复杂度分析
|
||||
- **时间复杂度**:O(n²),排序 O(n log n) + 双指针 O(n²)
|
||||
- **空间复杂度**:O(1),不考虑结果存储
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 图解过程
|
||||
|
||||
```
|
||||
数组: [-4, -1, -1, 0, 1, 2]
|
||||
↑ ↑ ↑
|
||||
i left right
|
||||
|
||||
第一轮: i = 0, nums[i] = -4
|
||||
left = 1, right = 5
|
||||
sum = -4 + (-1) + 2 = -3 < 0
|
||||
left++
|
||||
|
||||
left = 2, right = 5
|
||||
sum = -4 + (-1) + 2 = -3 < 0
|
||||
left++
|
||||
|
||||
left = 3, right = 5
|
||||
sum = -4 + 0 + 2 = -2 < 0
|
||||
left++
|
||||
|
||||
left = 4, right = 5
|
||||
sum = -4 + 1 + 2 = -1 < 0
|
||||
left++
|
||||
left >= right, 退出
|
||||
|
||||
第二轮: i = 1, nums[i] = -1
|
||||
left = 2, right = 5
|
||||
sum = -1 + (-1) + 2 = 0 ✓
|
||||
结果: [-1, -1, 2]
|
||||
|
||||
left = 3, right = 4
|
||||
sum = -1 + 0 + 1 = 0 ✓
|
||||
结果: [-1, 0, 1]
|
||||
|
||||
第三轮: i = 2, nums[i] = -1 (重复,跳过)
|
||||
第四轮: i = 3, nums[i] = 0 > 0, 退出
|
||||
|
||||
最终结果: [[-1,-1,2], [-1,0,1]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如果是四数之和?
|
||||
**方法**:在三层循环 + 双指针,时间 O(n³)
|
||||
|
||||
```go
|
||||
func fourSum(nums []int, target int) [][]int {
|
||||
result := [][]int{}
|
||||
sort.Ints(nums)
|
||||
n := len(nums)
|
||||
|
||||
for i := 0; i < n-3; i++ {
|
||||
if i > 0 && nums[i] == nums[i-1] {
|
||||
continue
|
||||
}
|
||||
|
||||
for j := i + 1; j < n-2; j++ {
|
||||
if j > i+1 && nums[j] == nums[j-1] {
|
||||
continue
|
||||
}
|
||||
|
||||
left, right := j+1, n-1
|
||||
for left < right {
|
||||
sum := nums[i] + nums[j] + nums[left] + nums[right]
|
||||
|
||||
if sum == target {
|
||||
result = append(result, []int{nums[i], nums[j], nums[left], nums[right]})
|
||||
|
||||
for left < right && nums[left] == nums[left+1] {
|
||||
left++
|
||||
}
|
||||
for left < right && nums[right] == nums[right-1] {
|
||||
right--
|
||||
}
|
||||
|
||||
left++
|
||||
right--
|
||||
} else if sum < target {
|
||||
left++
|
||||
} else {
|
||||
right--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如果数组很大,如何优化?
|
||||
**优化**:
|
||||
1. 提前终止:`nums[i] * 3 > target`(正数情况)
|
||||
2. 二分查找:确定第二个数后,二分查找后两个
|
||||
3. 哈希表:空间换时间
|
||||
|
||||
---
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 深度理解
|
||||
- **排序的作用**:去重 + 双指针基础
|
||||
- **双指针原理**:利用有序性,单向移动
|
||||
- **去重策略**:多处去重,确保结果唯一
|
||||
|
||||
### 实战扩展
|
||||
- **大数据场景**:外部排序 + 分段处理
|
||||
- **分布式场景**:MapReduce 框架
|
||||
- **业务场景**:推荐系统、用户画像匹配
|
||||
|
||||
### 变形题目
|
||||
1. [16. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/)
|
||||
2. [18. 四数之和](https://leetcode.cn/problems/4sum/)
|
||||
3. [259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这道题的核心是:
|
||||
1. **排序**:为双指针和去重创造条件
|
||||
2. **固定一个数**:将问题转化为两数之和
|
||||
3. **双指针**:根据 sum 与 target 的关系移动指针
|
||||
4. **多重去重**:i、left、right 都要跳过重复元素
|
||||
|
||||
**易错点**:
|
||||
- 忘记排序
|
||||
- 去重逻辑不完整
|
||||
- left 和 right 的移动条件
|
||||
- 优化提前终止的条件
|
||||
|
||||
**最优解法**:排序 + 双指针,时间 O(n²),空间 O(1)
|
||||
119
16-LeetCode Hot 100/两数相加.md
Normal file
119
16-LeetCode Hot 100/两数相加.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 两数相加 (Add Two Numbers)
|
||||
|
||||
LeetCode 2. Medium
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你两个 **非空** 的链表,表示两个非负的整数。它们每位数字都是按照 **逆序** 的方式存储的,并且每个节点只能存储 **一位** 数字。
|
||||
|
||||
请你将两个数相加,并以相同形式返回一个表示和的链表。
|
||||
|
||||
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
|
||||
|
||||
**示例 1**:
|
||||
```
|
||||
输入:l1 = [2,4,3], l2 = [5,6,4]
|
||||
输出:[7,0,8]
|
||||
解释:342 + 465 = 807
|
||||
```
|
||||
|
||||
**示例 2**:
|
||||
```
|
||||
输入:l1 = [0], l2 = [0]
|
||||
输出:[0]
|
||||
```
|
||||
|
||||
**示例 3**:
|
||||
```
|
||||
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
|
||||
输出:[8,9,9,9,0,0,0,1]
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 核心思想
|
||||
模拟竖式加法,同时遍历两个链表,逐位相加并处理进位。
|
||||
|
||||
### 算法流程
|
||||
1. 初始化哑节点和进位 carry = 0
|
||||
2. 同时遍历两个链表:
|
||||
- 计算当前位的和:sum = l1.val + l2.val + carry
|
||||
- 更新进位:carry = sum / 10
|
||||
- 创建新节点:sum % 10
|
||||
3. 处理最后的进位
|
||||
4. 返回结果链表
|
||||
|
||||
### 复杂度分析
|
||||
- **时间复杂度**:O(max(m, n)),m 和 n 分别为两个链表的长度
|
||||
- **空间复杂度**:O(1),不考虑结果链表的空间
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如果链表是正序存储的,如何解决?
|
||||
**方法**:反转链表 + 上述算法,或使用栈
|
||||
|
||||
```go
|
||||
func addTwoNumbersReverse(l1, l2 *ListNode) *ListNode {
|
||||
// 反转链表
|
||||
l1 = reverseList(l1)
|
||||
l2 = reverseList(l2)
|
||||
|
||||
// 使用相同算法相加
|
||||
result := addTwoNumbers(l1, l2)
|
||||
|
||||
// 反转结果
|
||||
return reverseList(result)
|
||||
}
|
||||
|
||||
func reverseList(head *ListNode) *ListNode {
|
||||
var prev *ListNode
|
||||
for head != nil {
|
||||
next := head.Next
|
||||
head.Next = prev
|
||||
prev = head
|
||||
head = next
|
||||
}
|
||||
return prev
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如何处理多个链表相加?
|
||||
**方法**:逐个相加或使用优先队列(最小堆)
|
||||
|
||||
---
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 深度理解
|
||||
- **链表操作**:指针移动、内存管理
|
||||
- **边界处理**:不同长度链表、最后进位
|
||||
- **哑节点技巧**:简化代码逻辑
|
||||
|
||||
### 实战扩展
|
||||
- **大数据场景**:链表表示超大整数相加
|
||||
- **分布式场景**:MapReduce 实现大数相加
|
||||
- **面试追问**:时间复杂度能否优化?如何测试?
|
||||
|
||||
### 变形题目
|
||||
1. [445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) - 链表正序存储
|
||||
2. [链表相加系列](https://leetcode.cn/tag/linked-list/) - 更多链表操作
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这道题的核心是:
|
||||
1. **模拟加法**:逐位相加,处理进位
|
||||
2. **链表遍历**:同时遍历两条链表
|
||||
3. **边界处理**:不同长度、最后进位
|
||||
|
||||
**易错点**:
|
||||
- 忘记处理最后的进位
|
||||
- 链表长度不一致时的处理
|
||||
- 哑节点的使用(简化代码)
|
||||
|
||||
**最优解法**:一次遍历,时间 O(max(m,n)),空间 O(1)
|
||||
37
16-LeetCode Hot 100/二叉树的中序遍历.md
Normal file
37
16-LeetCode Hot 100/二叉树的中序遍历.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 二叉树的中序遍历 (Binary Tree Inorder Traversal)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个二叉树的根节点,返回它的中序遍历。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:递归
|
||||
|
||||
### 方法二:迭代(栈)
|
||||
|
||||
## 解法
|
||||
|
||||
```go
|
||||
func inorderTraversal(root *TreeNode) []int {
|
||||
result := []int{}
|
||||
stack := []*TreeNode{}
|
||||
curr := root
|
||||
|
||||
for curr != nil || len(stack) > 0 {
|
||||
for curr != nil {
|
||||
stack = append(stack, curr)
|
||||
curr = curr.Left
|
||||
}
|
||||
|
||||
curr = stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
result = append(result, curr.Val)
|
||||
curr = curr.Right
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(n) 空间
|
||||
29
16-LeetCode Hot 100/二叉树的最大深度.md
Normal file
29
16-LeetCode Hot 100/二叉树的最大深度.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 二叉树的最大深度 (Maximum Depth of Binary Tree)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个二叉树,找出其最大深度。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### DFS / BFS
|
||||
|
||||
## 解法
|
||||
|
||||
```go
|
||||
func maxDepth(root *TreeNode) int {
|
||||
if root == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
left := maxDepth(root.Left)
|
||||
right := maxDepth(root.Right)
|
||||
|
||||
if left > right {
|
||||
return left + 1
|
||||
}
|
||||
return right + 1
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(h) 空间(h 为高度)
|
||||
41
16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md
Normal file
41
16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 从前序与中序遍历序列构造二叉树
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 递归构造
|
||||
|
||||
前序遍历:[根, [左子树], [右子树]]
|
||||
中序遍历:[[左子树], 根, [右子树]]
|
||||
|
||||
## 解法
|
||||
|
||||
```go
|
||||
func buildTree(preorder []int, inorder []int) *TreeNode {
|
||||
if len(preorder) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
root := &TreeNode{Val: preorder[0]}
|
||||
index := findIndex(inorder, preorder[0])
|
||||
|
||||
root.Left = buildTree(preorder[1:1+index], inorder[:index])
|
||||
root.Right = buildTree(preorder[1+index:], inorder[index+1:])
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func findIndex(arr []int, target int) int {
|
||||
for i, v := range arr {
|
||||
if v == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n²) 时间(可用哈希表优化到 O(n)),O(n) 空间
|
||||
487
16-LeetCode Hot 100/删除链表的倒数第N个结点.md
Normal file
487
16-LeetCode Hot 100/删除链表的倒数第N个结点.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# 删除链表的倒数第N个结点 (Remove Nth Node From End of List)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:head = [1,2,3,4,5], n = 2
|
||||
输出:[1,2,3,5]
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:head = [1], n = 1
|
||||
输出:[]
|
||||
```
|
||||
|
||||
**示例 3:**
|
||||
```
|
||||
输入:head = [1,2], n = 1
|
||||
输出:[1]
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- 链表中结点的数目为 `sz`
|
||||
- `1 <= sz <= 30`
|
||||
- `0 <= Node.val <= 100`
|
||||
- `1 <= n <= sz`
|
||||
|
||||
### 进阶
|
||||
|
||||
你能尝试使用一趟扫描实现吗?
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:双指针法(推荐)
|
||||
|
||||
**核心思想:**使用两个指针 `fast` 和 `slow`,`fast` 先移动 `n` 步,然后 `fast` 和 `slow` 一起移动,直到 `fast` 到达链表末尾。此时 `slow` 指向要删除结点的前一个结点。
|
||||
|
||||
**算法步骤:**
|
||||
1. 创建哑结点 `dummy`,指向链表头
|
||||
2. 初始化 `fast` 和 `slow` 指针都指向 `dummy`
|
||||
3. `fast` 先移动 `n + 1` 步
|
||||
4. `fast` 和 `slow` 同时移动,直到 `fast` 为 `nil`
|
||||
5. 此时 `slow.next` 就是要删除的结点,执行 `slow.next = slow.next.next`
|
||||
6. 返回 `dummy.next`
|
||||
|
||||
**为什么移动 n + 1 步?**
|
||||
- 这样 `slow` 最终会停在要删除结点的前一个结点
|
||||
- 方便删除操作:`slow.next = slow.next.next`
|
||||
|
||||
### 方法二:计算长度法
|
||||
|
||||
**核心思想:**先遍历链表计算长度,然后计算要删除的正数位置,再遍历到该位置删除结点。
|
||||
|
||||
**算法步骤:**
|
||||
1. 遍历链表,计算长度 `length`
|
||||
2. 要删除的正数位置为 `length - n`
|
||||
3. 创建哑结点 `dummy`,指向链表头
|
||||
4. 遍历到第 `length - n - 1` 个结点
|
||||
5. 删除下一个结点
|
||||
6. 返回 `dummy.next`
|
||||
|
||||
### 方法三:栈法
|
||||
|
||||
**核心思想:**将所有结点压入栈中,然后弹出 `n` 个结点,栈顶就是要删除结点的前一个结点。
|
||||
|
||||
**算法步骤:**
|
||||
1. 创建哑结点 `dummy`
|
||||
2. 将所有结点压入栈
|
||||
3. 弹出 `n` 个结点
|
||||
4. 栈顶结点的 `next` 指向要删除结点的 `next`
|
||||
5. 返回 `dummy.next`
|
||||
|
||||
## 解法
|
||||
|
||||
### Go 实现(双指针法)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ListNode 链表结点定义
|
||||
type ListNode struct {
|
||||
Val int
|
||||
Next *ListNode
|
||||
}
|
||||
|
||||
func removeNthFromEnd(head *ListNode, n int) *ListNode {
|
||||
// 创建哑结点,处理删除头结点的特殊情况
|
||||
dummy := &ListNode{0, head}
|
||||
fast, slow := dummy, dummy
|
||||
|
||||
// fast 先移动 n + 1 步
|
||||
for i := 0; i <= n; i++ {
|
||||
fast = fast.Next
|
||||
}
|
||||
|
||||
// fast 和 slow 一起移动,直到 fast 为 nil
|
||||
for fast != nil {
|
||||
fast = fast.Next
|
||||
slow = slow.Next
|
||||
}
|
||||
|
||||
// 删除 slow 的下一个结点
|
||||
slow.Next = slow.Next.Next
|
||||
|
||||
return dummy.Next
|
||||
}
|
||||
|
||||
// 辅助函数:创建链表
|
||||
func createList(nums []int) *ListNode {
|
||||
dummy := &ListNode{}
|
||||
current := dummy
|
||||
for _, num := range nums {
|
||||
current.Next = &ListNode{num, nil}
|
||||
current = current.Next
|
||||
}
|
||||
return dummy.Next
|
||||
}
|
||||
|
||||
// 辅助函数:打印链表
|
||||
func printList(head *ListNode) {
|
||||
current := head
|
||||
for current != nil {
|
||||
fmt.Printf("%d", current.Val)
|
||||
if current.Next != nil {
|
||||
fmt.Printf(" -> ")
|
||||
}
|
||||
current = current.Next
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
func main() {
|
||||
// 测试用例1
|
||||
head1 := createList([]int{1, 2, 3, 4, 5})
|
||||
fmt.Print("输入: ")
|
||||
printList(head1)
|
||||
fmt.Printf("n = 2\n")
|
||||
result1 := removeNthFromEnd(head1, 2)
|
||||
fmt.Print("输出: ")
|
||||
printList(result1)
|
||||
|
||||
// 测试用例2: 删除头结点
|
||||
head2 := createList([]int{1})
|
||||
fmt.Print("\n输入: ")
|
||||
printList(head2)
|
||||
fmt.Printf("n = 1\n")
|
||||
result2 := removeNthFromEnd(head2, 1)
|
||||
fmt.Print("输出: ")
|
||||
printList(result2)
|
||||
|
||||
// 测试用例3: 删除最后一个结点
|
||||
head3 := createList([]int{1, 2})
|
||||
fmt.Print("\n输入: ")
|
||||
printList(head3)
|
||||
fmt.Printf("n = 1\n")
|
||||
result3 := removeNthFromEnd(head3, 1)
|
||||
fmt.Print("输出: ")
|
||||
printList(result3)
|
||||
|
||||
// 测试用例4: 长链表
|
||||
head4 := createList([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
|
||||
fmt.Print("\n输入: ")
|
||||
printList(head4)
|
||||
fmt.Printf("n = 5\n")
|
||||
result4 := removeNthFromEnd(head4, 5)
|
||||
fmt.Print("输出: ")
|
||||
printList(result4)
|
||||
}
|
||||
```
|
||||
|
||||
### Go 实现(计算长度法)
|
||||
|
||||
```go
|
||||
func removeNthFromEndByLength(head *ListNode, n int) *ListNode {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算链表长度
|
||||
length := 0
|
||||
current := head
|
||||
for current != nil {
|
||||
length++
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
// 要删除的正数位置
|
||||
pos := length - n
|
||||
|
||||
// 创建哑结点
|
||||
dummy := &ListNode{0, head}
|
||||
current = dummy
|
||||
|
||||
// 移动到要删除结点的前一个结点
|
||||
for i := 0; i < pos; i++ {
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
// 删除结点
|
||||
current.Next = current.Next.Next
|
||||
|
||||
return dummy.Next
|
||||
}
|
||||
```
|
||||
|
||||
### Go 实现(栈法)
|
||||
|
||||
```go
|
||||
func removeNthFromEndByStack(head *ListNode, n int) *ListNode {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建哑结点
|
||||
dummy := &ListNode{0, head}
|
||||
|
||||
// 将所有结点压入栈
|
||||
var stack []*ListNode
|
||||
current := dummy
|
||||
for current != nil {
|
||||
stack = append(stack, current)
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
// 弹出 n 个结点
|
||||
for i := 0; i < n; i++ {
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
|
||||
// 栈顶就是要删除结点的前一个结点
|
||||
prev := stack[len(stack)-1]
|
||||
prev.Next = prev.Next.Next
|
||||
|
||||
return dummy.Next
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
### 双指针法
|
||||
|
||||
- **时间复杂度:** O(L)
|
||||
- 其中 L 是链表长度
|
||||
- 只需遍历链表一次
|
||||
|
||||
- **空间复杂度:** O(1)
|
||||
- 只使用了常数级别的额外空间
|
||||
- 只需要几个指针变量
|
||||
|
||||
### 计算长度法
|
||||
|
||||
- **时间复杂度:** O(L)
|
||||
- 第一次遍历计算长度:O(L)
|
||||
- 第二次遍历删除结点:O(L)
|
||||
- 总时间复杂度:O(2L) = O(L)
|
||||
|
||||
- **空间复杂度:** O(1)
|
||||
- 只使用了常数级别的额外空间
|
||||
|
||||
### 栈法
|
||||
|
||||
- **时间复杂度:** O(L)
|
||||
- 需要遍历链表一次
|
||||
|
||||
- **空间复杂度:** O(L)
|
||||
- 需要额外的栈空间存储所有结点
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如果链表是循环链表,应该如何处理?
|
||||
|
||||
**方法**:检测循环,计算长度,然后调整删除位置
|
||||
|
||||
```go
|
||||
func removeNthFromEndCircular(head *ListNode, n int) *ListNode {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算链表长度并检测循环
|
||||
length := 1
|
||||
slow, fast := head, head.Next
|
||||
for fast != nil && fast.Next != nil && slow != fast {
|
||||
slow = slow.Next
|
||||
fast = fast.Next.Next
|
||||
length++
|
||||
}
|
||||
|
||||
// 如果有循环
|
||||
if slow == fast {
|
||||
// 计算循环长度
|
||||
cycleLength := 1
|
||||
slow = slow.Next
|
||||
for slow != fast {
|
||||
slow = slow.Next
|
||||
cycleLength++
|
||||
}
|
||||
|
||||
// 总长度
|
||||
totalLength := length + cycleLength - 1
|
||||
pos := totalLength - n
|
||||
|
||||
// 处理位置调整
|
||||
if pos < 0 {
|
||||
pos += totalLength
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
return removeNthFromEndByPosition(head, pos)
|
||||
}
|
||||
|
||||
// 没有循环,使用原有方法
|
||||
return removeNthFromEnd(head, n)
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如果要求删除前 n 个结点,应该如何修改?
|
||||
|
||||
**方法**:直接删除前 n 个结点
|
||||
|
||||
```go
|
||||
func removeFirstNNodes(head *ListNode, n int) *ListNode {
|
||||
for i := 0; i < n && head != nil; i++ {
|
||||
head = head.Next
|
||||
}
|
||||
return head
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 如果链表很长,如何优化内存使用?
|
||||
|
||||
**方法**:
|
||||
1. 使用固定大小的滑动窗口
|
||||
2. 避免存储整个链表
|
||||
3. 使用递归(但会增加栈空间)
|
||||
|
||||
```go
|
||||
func removeNthFromEndOptimized(head *ListNode, n int) *ListNode {
|
||||
// 使用固定大小的窗口
|
||||
dummy := &ListNode{0, head}
|
||||
slow, fast := dummy, dummy
|
||||
|
||||
// fast 先移动 n + 1 步
|
||||
for i := 0; i <= n; i++ {
|
||||
if fast == nil {
|
||||
return head // n > 链表长度
|
||||
}
|
||||
fast = fast.Next
|
||||
}
|
||||
|
||||
// 移动窗口
|
||||
for fast != nil {
|
||||
slow = slow.Next
|
||||
fast = fast.Next
|
||||
}
|
||||
|
||||
// 删除结点
|
||||
slow.Next = slow.Next.Next
|
||||
|
||||
return dummy.Next
|
||||
}
|
||||
```
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 1. 深度理解:为什么需要哑结点?
|
||||
|
||||
**关键点:**
|
||||
- 处理删除头结点的特殊情况
|
||||
- 统一处理逻辑,减少边界条件判断
|
||||
- 简化代码,提高可读性
|
||||
|
||||
**哑结点的作用:**
|
||||
```go
|
||||
// 没有哑结点的情况
|
||||
func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode {
|
||||
// 需要特殊处理删除头结点的情况
|
||||
length := 0
|
||||
current := head
|
||||
for current != nil {
|
||||
length++
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
if length == n {
|
||||
return head.Next // 删除头结点
|
||||
}
|
||||
|
||||
// ... 其他逻辑
|
||||
return head
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实战扩展:链表操作的通用技巧
|
||||
|
||||
**技巧总结:**
|
||||
- 使用哑结点简化边界处理
|
||||
- 双指针技巧:快慢指针、前后指针
|
||||
- 递归处理链表问题
|
||||
- 栈辅助解决链表问题
|
||||
|
||||
**通用模板:**
|
||||
```go
|
||||
func solveLinkedListProblem(head *ListNode) *ListNode {
|
||||
dummy := &ListNode{0, head}
|
||||
// ... 使用双指针或其他技巧
|
||||
return dummy.Next
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 变形题目
|
||||
|
||||
1. [19. 删除链表的倒数第N个节点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) - 原题
|
||||
2. [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) - 删除所有重复元素
|
||||
3. [83. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) - 删除重复元素保留一个
|
||||
4. [203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/) - 删除指定值的节点
|
||||
|
||||
### 4. 优化技巧
|
||||
|
||||
**空间优化:**
|
||||
- 原地操作,不使用额外空间
|
||||
- 递归改为迭代
|
||||
|
||||
**时间优化:**
|
||||
- 一次遍历完成
|
||||
- 提前终止条件
|
||||
|
||||
**代码优化:**
|
||||
- 合并重复逻辑
|
||||
- 减少不必要的变量
|
||||
|
||||
### 5. 实际应用场景
|
||||
|
||||
**应用场景:**
|
||||
- 缓存淘汰策略(LRU)
|
||||
- 音乐播放列表管理
|
||||
- 浏览器历史记录
|
||||
- 撤销/重做功能
|
||||
|
||||
**面试问题:**
|
||||
- 如何处理并发访问的链表?
|
||||
- 如何实现线程安全的链表操作?
|
||||
|
||||
### 6. 面试技巧
|
||||
|
||||
**常见面试问题:**
|
||||
1. 时间/空间复杂度分析
|
||||
2. 边界条件处理
|
||||
3. 优化思路
|
||||
4. 相关题目变体
|
||||
|
||||
**回答技巧:**
|
||||
- 先给出暴力解法
|
||||
- 逐步优化
|
||||
- 说明权衡取舍
|
||||
|
||||
### 7. 相关题目推荐
|
||||
|
||||
**相关题目:**
|
||||
1. [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
|
||||
2. [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)
|
||||
3. [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)
|
||||
4. [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)
|
||||
|
||||
## 总结
|
||||
|
||||
这道题的核心是:
|
||||
1. **双指针法**:一次遍历,快慢指针配合
|
||||
2. **边界处理**:使用哑结点简化删除头结点的处理
|
||||
3. **多种解法**:双指针、计算长度、栈法各有优劣
|
||||
|
||||
**易错点**:
|
||||
- 忘记处理删除头结点的情况
|
||||
- 快指针移动步数错误(应该是 n+1)
|
||||
- 空链表的特殊情况处理
|
||||
- 循环链表的特殊情况
|
||||
|
||||
**最优解法**:双指针法,时间 O(L),空间 O(1)
|
||||
114
16-LeetCode Hot 100/单词搜索.md
Normal file
114
16-LeetCode Hot 100/单词搜索.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 单词搜索 (Word Search)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个 `m x n` 二维字符网格 `board` 和一个字符串单词 `word`。如果 `word` 存在于网格中,返回 `true`;否则,返回 `false`。
|
||||
|
||||
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
|
||||
输出:true
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
|
||||
输出:true
|
||||
```
|
||||
|
||||
**示例 3:**
|
||||
```
|
||||
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
|
||||
输出:false
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:DFS + 回溯(推荐)
|
||||
|
||||
**核心思想:**对每个位置进行 DFS,搜索是否存在匹配的单词路径。
|
||||
|
||||
**算法步骤:**
|
||||
1. 遍历网格的每个位置
|
||||
2. 如果当前位置字符匹配单词首字符,开始 DFS
|
||||
3. DFS 过程中:
|
||||
- 标记当前已访问
|
||||
- 向四个方向递归搜索
|
||||
- 如果找到完整单词,返回 true
|
||||
- 回溯时撤销访问标记
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func exist(board [][]byte, word string) bool {
|
||||
m, n := len(board), len(board[0])
|
||||
visited := make([][]bool, m)
|
||||
for i := range visited {
|
||||
visited[i] = make([]bool, n)
|
||||
}
|
||||
|
||||
var dfs func(i, j, k int) bool
|
||||
dfs = func(i, j, k int) bool {
|
||||
// 找到完整单词
|
||||
if k == len(word) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 边界检查或不匹配
|
||||
if i < 0 || i >= m || j < 0 || j >= n ||
|
||||
visited[i][j] || board[i][j] != word[k] {
|
||||
return false
|
||||
}
|
||||
|
||||
// 标记访问
|
||||
visited[i][j] = true
|
||||
|
||||
// 向四个方向搜索
|
||||
found := dfs(i+1, j, k+1) ||
|
||||
dfs(i-1, j, k+1) ||
|
||||
dfs(i, j+1, k+1) ||
|
||||
dfs(i, j-1, k+1)
|
||||
|
||||
// 回溯:取消标记
|
||||
visited[i][j] = false
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
for i := 0; i < m; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
if board[i][j] == word[0] && dfs(i, j, 0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
**LeetCode 212:** 给定一个 m x n 二维字符网格 board 和一个单词列表 words,返回所有在二维网格和字典中出现的单词。
|
||||
|
||||
```go
|
||||
func findWords(board [][]byte, words []string) []string {
|
||||
// 构建 Trie 树
|
||||
trie := buildTrie(words)
|
||||
result := []string{}
|
||||
|
||||
for i := 0; i < len(board); i++ {
|
||||
for j := 0; j < len(board[0]); j++ {
|
||||
dfsBoard(board, i, j, trie, &result)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
368
16-LeetCode Hot 100/子集.md
Normal file
368
16-LeetCode Hot 100/子集.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# 子集 (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))
|
||||
}
|
||||
```
|
||||
|
||||
```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)
|
||||
- 需要存储所有子集
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### 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
|
||||
35
16-LeetCode Hot 100/完全平方数.md
Normal file
35
16-LeetCode Hot 100/完全平方数.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 完全平方数 (Perfect Squares)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个整数 n,返回和为 n 的完全平方数的最少数量。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 动态规划
|
||||
|
||||
dp[i] = min(dp[i - j*j] + 1) for all j where j*j <= i
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func numSquares(n int) int {
|
||||
dp := make([]int, n+1)
|
||||
for i := range dp {
|
||||
dp[i] = math.MaxInt32
|
||||
}
|
||||
dp[0] = 0
|
||||
|
||||
for i := 1; i <= n; i++ {
|
||||
for j := 1; j*j <= i; j++ {
|
||||
if dp[i-j*j]+1 < dp[i] {
|
||||
dp[i] = dp[i-j*j] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n√n) 时间,O(n) 空间
|
||||
32
16-LeetCode Hot 100/对称二叉树.md
Normal file
32
16-LeetCode Hot 100/对称二叉树.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 对称二叉树 (Symmetric Tree)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个二叉树的根节点 root,检查它是否轴对称。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 递归比较
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func isSymmetric(root *TreeNode) bool {
|
||||
return check(root.Left, root.Right)
|
||||
}
|
||||
|
||||
func check(left, right *TreeNode) bool {
|
||||
if left == nil && right == nil {
|
||||
return true
|
||||
}
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return left.Val == right.Val &&
|
||||
check(left.Left, right.Right) &&
|
||||
check(left.Right, right.Left)
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(h) 空间
|
||||
591
16-LeetCode Hot 100/括号生成.md
Normal file
591
16-LeetCode Hot 100/括号生成.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# 括号生成 (Generate Parentheses)
|
||||
|
||||
## 题目描述
|
||||
|
||||
数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:n = 3
|
||||
输出:["((()))","(()())","(())()","()(())","()()()"]
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:n = 1
|
||||
输出:["()"]
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- `1 <= n <= 8`
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:回溯法(推荐)
|
||||
|
||||
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性:
|
||||
1. 左括号数量不能超过 n
|
||||
2. 右括号数量不能超过左括号数量
|
||||
|
||||
**算法步骤:**
|
||||
1. 初始化结果数组 `result` 和当前字符串 `current`
|
||||
2. 定义回溯函数 `backtrack(open, close)`:
|
||||
- `open`:已使用的左括号数量
|
||||
- `close`:已使用的右括号数量
|
||||
3. 终止条件:`len(current) == 2 * n`,将 `current` 加入 `result`
|
||||
4. 选择条件:
|
||||
- 如果 `open < n`,可以添加左括号
|
||||
- 如果 `close < open`,可以添加右括号
|
||||
5. 递归调用后撤销选择(回溯)
|
||||
|
||||
**为什么这样做?**
|
||||
- 通过限制 `close < open`,保证任何时候右括号数量不超过左括号数量
|
||||
- 通过限制 `open < n`,保证左括号数量不超过 n
|
||||
- 这样生成的所有组合都是有效的
|
||||
|
||||
### 方法二:DFS 深度优先搜索
|
||||
|
||||
**核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。
|
||||
|
||||
**算法步骤:**
|
||||
1. 构建一个递归树,每个节点代表一个状态
|
||||
2. 从根节点开始,每次可以选择添加左括号或右括号
|
||||
3. 剪枝:不符合条件的分支直接跳过
|
||||
4. 到达叶子节点(长度为 2n)时,记录结果
|
||||
|
||||
### 方法三:动态规划
|
||||
|
||||
**核心思想:**利用卡特兰数(Catalan Number)的性质。n 对括号的有效组合数等于第 n 个卡特兰数。
|
||||
|
||||
**递推公式:**
|
||||
- `dp[n]` 表示 n 对括号的所有有效组合
|
||||
- `dp[n] = "(" + dp[i] + ")" + dp[n-1-i]`,其中 `i` 从 0 到 n-1
|
||||
|
||||
**算法步骤:**
|
||||
1. 初始化 `dp[0] = [""]`
|
||||
2. 对于 `i` 从 1 到 n:
|
||||
- 对于 `j` 从 0 到 i-1:
|
||||
- 将 `dp[j]` 的每个组合加上一对括号,再拼接 `dp[i-1-j]` 的每个组合
|
||||
3. 返回 `dp[n]`
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现(回溯法)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func generateParenthesis(n int) []string {
|
||||
result := []string{}
|
||||
current := []byte{}
|
||||
|
||||
var backtrack func(open, close int)
|
||||
backtrack = func(open, close int) {
|
||||
// 终止条件:生成了 2n 个括号
|
||||
if len(current) == 2*n {
|
||||
result = append(result, string(current))
|
||||
return
|
||||
}
|
||||
|
||||
// 添加左括号:左括号数量小于 n
|
||||
if open < n {
|
||||
current = append(current, '(')
|
||||
backtrack(open+1, close)
|
||||
current = current[:len(current)-1] // 回溯
|
||||
}
|
||||
|
||||
// 添加右括号:右括号数量小于左括号数量
|
||||
if close < open {
|
||||
current = append(current, ')')
|
||||
backtrack(open, close+1)
|
||||
current = current[:len(current)-1] // 回溯
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0, 0)
|
||||
return result
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
func main() {
|
||||
// 测试用例1
|
||||
n1 := 3
|
||||
fmt.Printf("输入: n = %d\n", n1)
|
||||
fmt.Printf("输出: %v\n", generateParenthesis(n1))
|
||||
|
||||
// 测试用例2
|
||||
n2 := 1
|
||||
fmt.Printf("\n输入: n = %d\n", n2)
|
||||
fmt.Printf("输出: %v\n", generateParenthesis(n2))
|
||||
|
||||
// 测试用例3
|
||||
n3 := 4
|
||||
fmt.Printf("\n输入: n = %d\n", n3)
|
||||
result3 := generateParenthesis(n3)
|
||||
fmt.Printf("输出长度: %d\n", len(result3))
|
||||
fmt.Printf("输出: %v\n", result3)
|
||||
|
||||
// 验证卡特兰数
|
||||
for i := 1; i <= 8; i++ {
|
||||
fmt.Printf("n = %d, 组合数 = %d\n", i, len(generateParenthesis(i)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
func generateParenthesisDP(n int) []string {
|
||||
if n == 0 {
|
||||
return []string{""}
|
||||
}
|
||||
|
||||
dp := make([][]string, n+1)
|
||||
dp[0] = []string{""}
|
||||
|
||||
for i := 1; i <= n; i++ {
|
||||
dp[i] = []string{}
|
||||
for j := 0; j < i; j++ {
|
||||
for _, left := range dp[j] {
|
||||
for _, right := range dp[i-1-j] {
|
||||
dp[i] = append(dp[i], "("+left+")"+right)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
- **时间复杂度:** O(4^n / √n)
|
||||
- 在回溯树中,每个节点最多有 2 个分支
|
||||
- 树的高度为 2n
|
||||
- 但是由于剪枝,实际复杂度约为卡特兰数 C(n)
|
||||
- 卡特兰数约为 O(4^n / (n^(3/2) * √π))
|
||||
|
||||
- **空间复杂度:** O(n)
|
||||
- 递归栈深度最大为 2n
|
||||
- 存储结果的空间不算在内(这是必须的)
|
||||
|
||||
### 动态规划
|
||||
|
||||
- **时间复杂度:** O(4^n / √n)
|
||||
- 与回溯法类似,需要生成所有有效组合
|
||||
|
||||
- **空间复杂度:** O(4^n / √n)
|
||||
- 需要存储中间结果和最终结果
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如何判断一个括号字符串是否有效?
|
||||
|
||||
**A:** 使用栈或者计数器。
|
||||
|
||||
```go
|
||||
// 方法1: 使用栈
|
||||
func isValid(s string) bool {
|
||||
stack := []byte{}
|
||||
for _, c := range []byte(s) {
|
||||
if c == '(' {
|
||||
stack = append(stack, c)
|
||||
} else if len(stack) > 0 {
|
||||
stack = stack[:len(stack)-1]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(stack) == 0
|
||||
}
|
||||
|
||||
// 方法2: 使用计数器
|
||||
func isValidSimple(s string) bool {
|
||||
count := 0
|
||||
for _, c := range s {
|
||||
if c == '(' {
|
||||
count++
|
||||
} else if c == ')' {
|
||||
count--
|
||||
}
|
||||
if count < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return count == 0
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如果有三种括号 ()、[]、{},应该如何生成?
|
||||
|
||||
**A:** 需要更复杂的逻辑来保证括号匹配。
|
||||
|
||||
```go
|
||||
func generateMultipleParentheses(n int) []string {
|
||||
types := []byte{'(', ')', '[', ']', '{', '}'}
|
||||
result := []string{}
|
||||
current := []byte{}
|
||||
stack := []byte{}
|
||||
|
||||
var backtrack func(int)
|
||||
backtrack = func(length int) {
|
||||
if len(current) == 2*n {
|
||||
result = append(result, string(current))
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(types); i += 2 {
|
||||
// 添加左括号
|
||||
if length < n {
|
||||
current = append(current, types[i])
|
||||
stack = append(stack, types[i])
|
||||
backtrack(length + 1)
|
||||
current = current[:len(current)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; i < len(types); i += 2 {
|
||||
// 添加右括号:必须与栈顶匹配
|
||||
if len(stack) > 0 && stack[len(stack)-1] == types[i-1] {
|
||||
current = append(current, types[i])
|
||||
stack = stack[:len(stack)-1]
|
||||
backtrack(length)
|
||||
current = current[:len(current)-1]
|
||||
stack = append(stack, types[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 如何优化内存使用,特别是对于大的 n?
|
||||
|
||||
**A:** 可以使用生成器模式,逐个生成结果而不是全部存储。
|
||||
|
||||
```go
|
||||
func generateParenthesisGenerator(n int, callback func(string)) {
|
||||
current := make([]byte, 0, 2*n)
|
||||
|
||||
var backtrack func(open, close int)
|
||||
backtrack = func(open, close int) {
|
||||
if len(current) == 2*n {
|
||||
callback(string(current))
|
||||
return
|
||||
}
|
||||
|
||||
if open < n {
|
||||
current = append(current, '(')
|
||||
backtrack(open+1, close)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
|
||||
if close < open {
|
||||
current = append(current, ')')
|
||||
backtrack(open, close+1)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0, 0)
|
||||
}
|
||||
```
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 1. 深度理解:卡特兰数(Catalan Number)
|
||||
|
||||
**定义:**卡特兰数是组合数学中经常出现的数列,在许多计数问题中出现。
|
||||
|
||||
**公式:**
|
||||
- C(n) = (2n)! / ((n+1)! × n!)
|
||||
- C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0)
|
||||
|
||||
**前几项:**1, 1, 2, 5, 14, 42, 132, 429, 1430, ...
|
||||
|
||||
**应用场景:**
|
||||
1. 括号匹配问题(本题)
|
||||
2. 二叉搜索树的计数
|
||||
3. 出栈序列的计数
|
||||
4. 路径计数(不穿过对角线)
|
||||
|
||||
**计算卡特兰数:**
|
||||
|
||||
```go
|
||||
func catalanNumber(n int) int {
|
||||
if n <= 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// 动态规划计算
|
||||
dp := make([]int, n+1)
|
||||
dp[0], dp[1] = 1, 1
|
||||
|
||||
for i := 2; i <= n; i++ {
|
||||
for j := 0; j < i; j++ {
|
||||
dp[i] += dp[j] * dp[i-1-j]
|
||||
}
|
||||
}
|
||||
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实战扩展:通用回溯框架
|
||||
|
||||
**回溯法通用模板:**
|
||||
|
||||
```go
|
||||
func backtrack(路径, 选择列表) {
|
||||
if 满足结束条件 {
|
||||
result = append(result, 路径)
|
||||
return
|
||||
}
|
||||
|
||||
for 选择 in 选择列表 {
|
||||
// 做选择
|
||||
路径.add(选择)
|
||||
|
||||
// 递归
|
||||
backtrack(路径, 选择列表)
|
||||
|
||||
// 撤销选择(回溯)
|
||||
路径.remove(选择)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**应用示例:排列问题**
|
||||
|
||||
```go
|
||||
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:最长有效括号
|
||||
|
||||
**LeetCode 32:** 给定一个只包含 '(' 和 ')' 的字符串,找出最长有效(正确闭合)括号子串的长度。
|
||||
|
||||
```go
|
||||
func longestValidParentheses(s string) int {
|
||||
maxLen := 0
|
||||
stack := []int{-1} // 初始化为 -1,便于计算长度
|
||||
|
||||
for i, c := range s {
|
||||
if c == '(' {
|
||||
stack = append(stack, i)
|
||||
} else {
|
||||
stack = stack[:len(stack)-1]
|
||||
if len(stack) == 0 {
|
||||
stack = append(stack, i)
|
||||
} else {
|
||||
length := i - stack[len(stack)-1]
|
||||
if length > maxLen {
|
||||
maxLen = length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxLen
|
||||
}
|
||||
```
|
||||
|
||||
#### 变形2:不同的二叉搜索树
|
||||
|
||||
**LeetCode 96:** 给定 n,求恰好由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种?
|
||||
|
||||
```go
|
||||
func numTrees(n int) int {
|
||||
dp := make([]int, n+1)
|
||||
dp[0], dp[1] = 1, 1
|
||||
|
||||
for i := 2; i <= n; i++ {
|
||||
for j := 1; j <= i; j++ {
|
||||
dp[i] += dp[j-1] * dp[i-j]
|
||||
}
|
||||
}
|
||||
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
#### 变形3:括号分数
|
||||
|
||||
**LeetCode 856:** 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数:
|
||||
- `()` 得 1 分
|
||||
- `AB` 得 `A + B` 分,其中 A 和 B 是平衡括号字符串
|
||||
- `(A)` 得 `2 × A` 分,其中 A 是平衡括号字符串
|
||||
|
||||
```go
|
||||
func scoreOfParentheses(s string) int {
|
||||
stack := []int{0} // 栈底保存当前层的分数
|
||||
|
||||
for _, c := range s {
|
||||
if c == '(' {
|
||||
stack = append(stack, 0) // 新的一层,初始分数为 0
|
||||
} else {
|
||||
// 弹出当前层的分数
|
||||
top := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
|
||||
// 计算分数
|
||||
if top == 0 {
|
||||
stack[len(stack)-1] += 1
|
||||
} else {
|
||||
stack[len(stack)-1] += 2 * top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stack[0]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 优化技巧
|
||||
|
||||
#### 优化1:剪枝优化
|
||||
|
||||
在回溯过程中,尽早发现不可能的解并剪枝。
|
||||
|
||||
```go
|
||||
func generateParenthesisOptimized(n int) []string {
|
||||
result := []string{}
|
||||
current := []byte{}
|
||||
|
||||
var backtrack func(open, close int)
|
||||
backtrack = func(open, close int) {
|
||||
// 剪枝:如果剩余的右括号太多,无法完成
|
||||
if close > open {
|
||||
return
|
||||
}
|
||||
|
||||
if len(current) == 2*n {
|
||||
result = append(result, string(current))
|
||||
return
|
||||
}
|
||||
|
||||
if open < n {
|
||||
current = append(current, '(')
|
||||
backtrack(open+1, close)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
|
||||
if close < open {
|
||||
current = append(current, ')')
|
||||
backtrack(open, close+1)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0, 0)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
#### 优化2:迭代优化
|
||||
|
||||
使用迭代代替递归,避免栈溢出。
|
||||
|
||||
```go
|
||||
func generateParenthesisIterative(n int) []string {
|
||||
type state struct {
|
||||
current string
|
||||
open int
|
||||
close int
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
stack := []state{{"", 0, 0}}
|
||||
|
||||
for len(stack) > 0 {
|
||||
// 弹出栈顶
|
||||
s := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
|
||||
if len(s.current) == 2*n {
|
||||
result = append(result, s.current)
|
||||
continue
|
||||
}
|
||||
|
||||
if s.open < n {
|
||||
stack = append(stack, state{s.current + "(", s.open + 1, s.close})
|
||||
}
|
||||
|
||||
if s.close < s.open {
|
||||
stack = append(stack, state{s.current + ")", s.open, s.close + 1})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 实际应用场景
|
||||
|
||||
- **编译器:** 语法分析和表达式求值
|
||||
- **代码格式化:** 自动添加括号
|
||||
- **数学表达式:** 验证表达式有效性
|
||||
- **数据验证:** 检查嵌套结构(如 HTML 标签)
|
||||
|
||||
### 6. 面试技巧
|
||||
|
||||
**面试官可能会问:**
|
||||
1. "为什么要用回溯法而不是暴力枚举?"
|
||||
2. "卡特兰数和这个问题有什么关系?"
|
||||
3. "如何证明你的算法生成的所有组合都是有效的?"
|
||||
|
||||
**回答要点:**
|
||||
1. 回溯法通过剪枝避免了无效组合的生成,效率更高
|
||||
2. n 对括号的有效组合数等于第 n 个卡特兰数
|
||||
3. 通过维护 `open` 和 `close` 计数器,保证了右括号永远不超过左括号
|
||||
|
||||
### 7. 相关题目推荐
|
||||
|
||||
- LeetCode 22: 括号生成(本题)
|
||||
- LeetCode 17: 电话号码的字母组合
|
||||
- LeetCode 32: 最长有效括号
|
||||
- LeetCode 39: 组合总和
|
||||
- LeetCode 46: 全排列
|
||||
- LeetCode 78: 子集
|
||||
- LeetCode 96: 不同的二叉搜索树
|
||||
206
16-LeetCode Hot 100/无重复字符的最长子串.md
Normal file
206
16-LeetCode Hot 100/无重复字符的最长子串.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 无重复字符的最长子串 (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" 是一个子序列,不是子串。
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 核心思想
|
||||
使用**滑动窗口**(Sliding Window)+ **哈希表**记录字符位置。
|
||||
|
||||
### 算法流程
|
||||
1. 维护一个窗口 [left, right]
|
||||
2. 使用哈希表记录每个字符最后一次出现的位置
|
||||
3. 遍历字符串:
|
||||
- 如果当前字符在窗口内出现,移动 left 到重复字符的下一位
|
||||
- 更新哈希表和最大长度
|
||||
|
||||
### 复杂度分析
|
||||
- **时间复杂度**:O(n),n 为字符串长度
|
||||
- **空间复杂度**:O(min(m, n)),m 为字符集大小
|
||||
|
||||
---
|
||||
|
||||
## 解法
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 图解过程
|
||||
|
||||
```
|
||||
字符串: "abcabcbb"
|
||||
|
||||
步骤1: [a]bcabcbb
|
||||
left=0, right=0, maxLength=1
|
||||
|
||||
步骤2: [a,b]cabcbb
|
||||
left=0, right=1, maxLength=2
|
||||
|
||||
步骤3: [a,b,c]abcbb
|
||||
left=0, right=2, maxLength=3
|
||||
|
||||
步骤4: a[b,c,a]bcbb (发现重复,left移动)
|
||||
left=1, right=3, maxLength=3
|
||||
|
||||
步骤5: ab[c,a,b]cbb (发现重复,left移动)
|
||||
left=2, right=4, maxLength=3
|
||||
|
||||
步骤6: abc[a,b,c]bb (发现重复,left移动)
|
||||
left=3, right=5, maxLength=3
|
||||
|
||||
步骤7: abca[b,c,b]b (发现重复,left移动)
|
||||
left=4, right=6, maxLength=3
|
||||
|
||||
步骤8: abcab[c,b,b] (发现重复,left移动)
|
||||
left=5, right=7, maxLength=3
|
||||
|
||||
结果: maxLength = 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### 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]
|
||||
}
|
||||
```
|
||||
|
||||
### 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))
|
||||
59
16-LeetCode Hot 100/最大正方形.md
Normal file
59
16-LeetCode Hot 100/最大正方形.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 最大正方形 (Maximal Square)
|
||||
|
||||
## 题目描述
|
||||
|
||||
在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 动态规划
|
||||
|
||||
**状态定义:** dp[i][j] 表示以 (i, j) 为右下角的最大正方形边长。
|
||||
|
||||
**状态转移:** dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func maximalSquare(matrix [][]byte) int {
|
||||
if len(matrix) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
m, n := len(matrix), len(matrix[0])
|
||||
dp := make([][]int, m+1)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n+1)
|
||||
}
|
||||
|
||||
maxSide := 0
|
||||
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 1; j <= n; j++ {
|
||||
if matrix[i-1][j-1] == '1' {
|
||||
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
|
||||
if dp[i][j] > maxSide {
|
||||
maxSide = dp[i][j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxSide * maxSide
|
||||
}
|
||||
|
||||
func min(a, b, c int) int {
|
||||
if a < b {
|
||||
if a < c {
|
||||
return a
|
||||
}
|
||||
return c
|
||||
}
|
||||
if b < c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(mn) 时间,O(mn) 空间
|
||||
56
16-LeetCode Hot 100/最小栈.md
Normal file
56
16-LeetCode Hot 100/最小栈.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 最小栈 (Min Stack)
|
||||
|
||||
## 题目描述
|
||||
|
||||
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 辅助栈
|
||||
|
||||
使用两个栈,一个存储所有元素,另一个存储当前最小值。
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
type MinStack struct {
|
||||
stack []int
|
||||
minStack []int
|
||||
}
|
||||
|
||||
func Constructor() MinStack {
|
||||
return MinStack{
|
||||
stack: []int{},
|
||||
minStack: []int{},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MinStack) Push(val int) {
|
||||
this.stack = append(this.stack, val)
|
||||
if len(this.minStack) == 0 {
|
||||
this.minStack = append(this.minStack, val)
|
||||
} else {
|
||||
min := this.minStack[len(this.minStack)-1]
|
||||
if val < min {
|
||||
this.minStack = append(this.minStack, val)
|
||||
} else {
|
||||
this.minStack = append(this.minStack, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MinStack) Pop() {
|
||||
this.stack = this.stack[:len(this.stack)-1]
|
||||
this.minStack = this.minStack[:len(this.minStack)-1]
|
||||
}
|
||||
|
||||
func (this *MinStack) Top() int {
|
||||
return this.stack[len(this.stack)-1]
|
||||
}
|
||||
|
||||
func (this *MinStack) GetMin() int {
|
||||
return this.minStack[len(this.minStack)-1]
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** 所有操作 O(1) 时间
|
||||
534
16-LeetCode Hot 100/最长回文子串.md
Normal file
534
16-LeetCode Hot 100/最长回文子串.md
Normal file
@@ -0,0 +1,534 @@
|
||||
# 最长回文子串 (Longest Palindromic Substring)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个字符串 `s`,找到 `s` 中最长的回文子串。
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:s = "babad"
|
||||
输出:"bab"
|
||||
解释:"aba" 同样是符合题意的答案。
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:s = "cbbd"
|
||||
输出:"bb"
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- `1 <= s.length <= 1000`
|
||||
- `s` 仅由数字和英文字母组成
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:动态规划(推荐)
|
||||
|
||||
**核心思想:**使用二维数组 `dp[i][j]` 表示 `s[i:j+1]` 是否为回文串。
|
||||
|
||||
**状态转移方程:**
|
||||
- `dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])`
|
||||
- 如果 `s[i] == s[j]` 且 `dp[i+1][j-1]` 为真(或子串长度小于3),则 `dp[i][j]` 为真
|
||||
|
||||
**算法步骤:**
|
||||
1. 初始化 `dp` 数组,所有单个字符都是回文串
|
||||
2. 按长度递增的顺序遍历所有子串
|
||||
3. 更新最长回文子串的起始位置和长度
|
||||
|
||||
### 方法二:中心扩展法(最优)
|
||||
|
||||
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
|
||||
|
||||
**算法步骤:**
|
||||
1. 遍历每个字符作为中心点
|
||||
2. 从中心点向两边扩展,直到不再是回文串
|
||||
3. 记录最长的回文串
|
||||
4. 需要考虑奇数长度和偶数长度两种情况
|
||||
|
||||
### 方法三:Manacher 算法(最优)
|
||||
|
||||
**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。
|
||||
|
||||
**算法步骤:**
|
||||
1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度
|
||||
2. 使用数组 `P` 记录以每个字符为中心的最长回文半径
|
||||
3. 利用对称性快速计算回文半径
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现(中心扩展法)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func longestPalindrome(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
|
||||
start, maxLen := 0, 1
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
// 奇数长度:以当前字符为中心
|
||||
len1 := expandAroundCenter(s, i, i)
|
||||
// 偶数长度:以当前字符和下一个字符之间为中心
|
||||
len2 := expandAroundCenter(s, i, i+1)
|
||||
|
||||
currentLen := max(len1, len2)
|
||||
if currentLen > maxLen {
|
||||
maxLen = currentLen
|
||||
start = i - (currentLen-1)/2
|
||||
}
|
||||
}
|
||||
|
||||
return s[start : start+maxLen]
|
||||
}
|
||||
|
||||
func expandAroundCenter(s string, left, right int) int {
|
||||
for left >= 0 && right < len(s) && s[left] == s[right] {
|
||||
left--
|
||||
right++
|
||||
}
|
||||
return right - left - 1
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
func main() {
|
||||
// 测试用例1
|
||||
s1 := "babad"
|
||||
fmt.Printf("输入: %s\n", s1)
|
||||
fmt.Printf("输出: %s\n", longestPalindrome(s1))
|
||||
|
||||
// 测试用例2
|
||||
s2 := "cbbd"
|
||||
fmt.Printf("\n输入: %s\n", s2)
|
||||
fmt.Printf("输出: %s\n", longestPalindrome(s2))
|
||||
|
||||
// 测试用例3: 单个字符
|
||||
s3 := "a"
|
||||
fmt.Printf("\n输入: %s\n", s3)
|
||||
fmt.Printf("输出: %s\n", longestPalindrome(s3))
|
||||
|
||||
// 测试用例4: 全部相同
|
||||
s4 := "aaaa"
|
||||
fmt.Printf("\n输入: %s\n", s4)
|
||||
fmt.Printf("输出: %s\n", longestPalindrome(s4))
|
||||
|
||||
// 测试用例5: 无回文
|
||||
s5 := "abc"
|
||||
fmt.Printf("\n输入: %s\n", s5)
|
||||
fmt.Printf("输出: %s\n", longestPalindrome(s5))
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
func longestPalindromeDP(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
|
||||
n := len(s)
|
||||
dp := make([][]bool, n)
|
||||
for i := range dp {
|
||||
dp[i] = make([]bool, n)
|
||||
}
|
||||
|
||||
start, maxLen := 0, 1
|
||||
|
||||
// 初始化:所有单个字符都是回文串
|
||||
for i := 0; i < n; i++ {
|
||||
dp[i][i] = true
|
||||
}
|
||||
|
||||
// 按长度递增的顺序遍历
|
||||
for length := 2; length <= n; length++ {
|
||||
for i := 0; i <= n-length; i++ {
|
||||
j := i + length - 1
|
||||
|
||||
if s[i] == s[j] {
|
||||
if length == 2 || dp[i+1][j-1] {
|
||||
dp[i][j] = true
|
||||
if length > maxLen {
|
||||
maxLen = length
|
||||
start = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s[start : start+maxLen]
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
func longestPalindromeManacher(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
|
||||
// 预处理:插入特殊字符
|
||||
t := "#"
|
||||
for i := 0; i < len(s); i++ {
|
||||
t += string(s[i]) + "#"
|
||||
}
|
||||
|
||||
n := len(t)
|
||||
p := make([]int, n)
|
||||
center, right := 0, 0
|
||||
maxCenter, maxLen := 0, 0
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if i < right {
|
||||
mirror := 2*center - i
|
||||
p[i] = min(right-i, p[mirror])
|
||||
}
|
||||
|
||||
// 尝试扩展
|
||||
for i+p[i]+1 < n && i-p[i]-1 >= 0 && t[i+p[i]+1] == t[i-p[i]-1] {
|
||||
p[i]++
|
||||
}
|
||||
|
||||
// 更新中心和右边界
|
||||
if i+p[i] > right {
|
||||
center = i
|
||||
right = i + p[i]
|
||||
}
|
||||
|
||||
// 更新最大回文串
|
||||
if p[i] > maxLen {
|
||||
maxLen = p[i]
|
||||
maxCenter = i
|
||||
}
|
||||
}
|
||||
|
||||
// 计算原字符串中的起始位置
|
||||
start := (maxCenter - maxLen) / 2
|
||||
return s[start : start+maxLen]
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
### 中心扩展法
|
||||
|
||||
- **时间复杂度:** O(n²)
|
||||
- 外层循环遍历 n 个字符
|
||||
- 内层扩展最多 O(n) 次
|
||||
- 总时间复杂度:O(n²)
|
||||
|
||||
- **空间复杂度:** O(1)
|
||||
- 只使用了常数级别的额外空间
|
||||
|
||||
### 动态规划
|
||||
|
||||
- **时间复杂度:** O(n²)
|
||||
- 需要填充 n×n 的 dp 数组
|
||||
- 但由于剪枝,实际复杂度约为 O(n²/2)
|
||||
|
||||
- **空间复杂度:** O(n²)
|
||||
- 需要存储 n×n 的 dp 数组
|
||||
|
||||
### Manacher 算法
|
||||
|
||||
- **时间复杂度:** O(n)
|
||||
- 只需遍历字符串一次
|
||||
- 利用对称性避免重复计算
|
||||
|
||||
- **空间复杂度:** O(n)
|
||||
- 需要存储处理后的字符串和半径数组
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如何找到所有回文子串?
|
||||
|
||||
**A:** 修改中心扩展法,找到每个回文子串都记录下来。
|
||||
|
||||
```go
|
||||
func findAllPalindromes(s string) []string {
|
||||
result := []string{}
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
// 奇数长度
|
||||
l, r := i, i
|
||||
for l >= 0 && r < len(s) && s[l] == s[r] {
|
||||
result = append(result, s[l:r+1])
|
||||
l--
|
||||
r++
|
||||
}
|
||||
|
||||
// 偶数长度
|
||||
l, r = i, i+1
|
||||
for l >= 0 && r < len(s) && s[l] == s[r] {
|
||||
result = append(result, s[l:r+1])
|
||||
l--
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如何判断一个字符串是否可以通过重新排列成为回文串?
|
||||
|
||||
**A:** 统计每个字符出现的次数,最多只能有一个字符出现奇数次。
|
||||
|
||||
```go
|
||||
func canPermutePalindrome(s string) bool {
|
||||
count := make(map[rune]int)
|
||||
for _, c := range s {
|
||||
count[c]++
|
||||
}
|
||||
|
||||
oddCount := 0
|
||||
for _, c := range count {
|
||||
if c%2 == 1 {
|
||||
oddCount++
|
||||
}
|
||||
}
|
||||
|
||||
return oddCount <= 1
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 最长回文子序列(非连续)如何求解?
|
||||
|
||||
**A:** 使用动态规划,`dp[i][j]` 表示 `s[i:j+1]` 的最长回文子序列长度。
|
||||
|
||||
```go
|
||||
func longestPalindromeSubseq(s string) int {
|
||||
n := len(s)
|
||||
dp := make([][]int, n)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n)
|
||||
dp[i][i] = 1
|
||||
}
|
||||
|
||||
for length := 2; length <= n; length++ {
|
||||
for i := 0; i <= n-length; i++ {
|
||||
j := i + length - 1
|
||||
if s[i] == s[j] {
|
||||
dp[i][j] = dp[i+1][j-1] + 2
|
||||
} else {
|
||||
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[0][n-1]
|
||||
}
|
||||
```
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 1. 深度理解:为什么中心扩展法最优?
|
||||
|
||||
**对比分析:**
|
||||
- **暴力法:** O(n³) - 枚举所有子串 O(n²),判断是否回文 O(n)
|
||||
- **动态规划:** O(n²) 时间,O(n²) 空间
|
||||
- **中心扩展法:** O(n²) 时间,O(1) 空间
|
||||
- **Manacher 算法:** O(n) 时间,O(n) 空间
|
||||
|
||||
**选择建议:**
|
||||
- **面试:** 中心扩展法 - 代码简洁,易于实现
|
||||
- **实际应用:** Manacher 算法 - 性能最优
|
||||
- **学习:** 都要掌握,理解不同思路
|
||||
|
||||
### 2. 实战扩展:回文串相关算法
|
||||
|
||||
#### 短回文串构造
|
||||
|
||||
**LeetCode 214:** 给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
|
||||
|
||||
```go
|
||||
func shortestPalindrome(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
|
||||
// 找到最长的回文前缀
|
||||
n := len(s)
|
||||
rev := reverse(s)
|
||||
combined := s + "#" + rev
|
||||
|
||||
// KMP 算法计算最长公共前后缀
|
||||
pi := make([]int, len(combined))
|
||||
for i := 1; i < len(combined); i++ {
|
||||
j := pi[i-1]
|
||||
for j > 0 && combined[i] != combined[j] {
|
||||
j = pi[j-1]
|
||||
}
|
||||
if combined[i] == combined[j] {
|
||||
j++
|
||||
}
|
||||
pi[i] = j
|
||||
}
|
||||
|
||||
// 添加剩余字符的逆序
|
||||
add := rev[:n-pi[len(combined)-1]]
|
||||
return add + s
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
runes := []rune(s)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 变形题目
|
||||
|
||||
#### 变形1:回文子串个数
|
||||
|
||||
**LeetCode 647:** 给定一个字符串,计算这个字符串中有多少个回文子串。
|
||||
|
||||
```go
|
||||
func countSubstrings(s string) int {
|
||||
count := 0
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
// 奇数长度
|
||||
count += expandAroundCenterCount(s, i, i)
|
||||
// 偶数长度
|
||||
count += expandAroundCenterCount(s, i, i+1)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func expandAroundCenterCount(s string, left, right int) int {
|
||||
count := 0
|
||||
for left >= 0 && right < len(s) && s[left] == s[right] {
|
||||
count++
|
||||
left--
|
||||
right++
|
||||
}
|
||||
return count
|
||||
}
|
||||
```
|
||||
|
||||
#### 变形2:分割回文串
|
||||
|
||||
**LeetCode 131:** 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
|
||||
|
||||
```go
|
||||
func partition(s string) [][]string {
|
||||
result := [][]string{}
|
||||
current := []string{}
|
||||
|
||||
var backtrack func(start int)
|
||||
backtrack = func(start int) {
|
||||
if start == len(s) {
|
||||
temp := make([]string, len(current))
|
||||
copy(temp, current)
|
||||
result = append(result, temp)
|
||||
return
|
||||
}
|
||||
|
||||
for end := start + 1; end <= len(s); end++ {
|
||||
if isPalindrome(s[start:end]) {
|
||||
current = append(current, s[start:end])
|
||||
backtrack(end)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0)
|
||||
return result
|
||||
}
|
||||
|
||||
func isPalindrome(s string) bool {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
if s[i] != s[j] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 优化技巧
|
||||
|
||||
#### 优化1:提前终止
|
||||
|
||||
如果找到的回文串已经接近最大可能长度,可以提前终止。
|
||||
|
||||
```go
|
||||
func longestPalindromeOptimized(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
|
||||
start, maxLen := 0, 1
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
// 提前终止:不可能找到更长的回文串了
|
||||
if maxLen >= 2*(len(s)-i) {
|
||||
break
|
||||
}
|
||||
|
||||
len1 := expandAroundCenter(s, i, i)
|
||||
len2 := expandAroundCenter(s, i, i+1)
|
||||
currentLen := max(len1, len2)
|
||||
|
||||
if currentLen > maxLen {
|
||||
maxLen = currentLen
|
||||
start = i - (currentLen-1)/2
|
||||
}
|
||||
}
|
||||
|
||||
return s[start : start+maxLen]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 实际应用场景
|
||||
|
||||
- **DNA 序列分析:** 寻找重复序列
|
||||
- **文本编辑:** 检查拼写和语法
|
||||
- **数据压缩:** 利用重复模式
|
||||
- **模式匹配:** 查找对称模式
|
||||
|
||||
### 6. 面试技巧
|
||||
|
||||
**面试官可能会问:**
|
||||
1. "为什么中心扩展法比动态规划更好?"
|
||||
2. "Manacher 算法的核心思想是什么?"
|
||||
3. "如何处理 unicode 字符?"
|
||||
|
||||
**回答要点:**
|
||||
1. 中心扩展法空间复杂度 O(1),动态规划需要 O(n²)
|
||||
2. Manacher 算法利用回文串的对称性,避免重复计算
|
||||
3. 使用 rune 类型处理 unicode 字符
|
||||
|
||||
### 7. 相关题目推荐
|
||||
|
||||
- LeetCode 5: 最长回文子串(本题)
|
||||
- LeetCode 125: 验证回文串
|
||||
- LeetCode 131: 分割回文串
|
||||
- LeetCode 214: 最短回文串
|
||||
- LeetCode 516: 最长回文子序列
|
||||
- LeetCode 647: 回文子串个数
|
||||
44
16-LeetCode Hot 100/最长连续序列.md
Normal file
44
16-LeetCode Hot 100/最长连续序列.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 最长连续序列 (Longest Consecutive Sequence)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 哈希表
|
||||
|
||||
将数字存入哈希表,对于每个数字,如果它是序列的起点(num-1 不在集合中),则向后查找。
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func longestConsecutive(nums []int) int {
|
||||
numSet := make(map[int]bool)
|
||||
for _, num := range nums {
|
||||
numSet[num] = true
|
||||
}
|
||||
|
||||
longest := 0
|
||||
|
||||
for num := range numSet {
|
||||
if !numSet[num-1] { // 是序列起点
|
||||
currentNum := num
|
||||
current := 1
|
||||
|
||||
for numSet[currentNum+1] {
|
||||
currentNum++
|
||||
current++
|
||||
}
|
||||
|
||||
if current > longest {
|
||||
longest = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(n) 空间
|
||||
56
16-LeetCode Hot 100/柱状图中最大的矩形.md
Normal file
56
16-LeetCode Hot 100/柱状图中最大的矩形.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 柱状图中最大的矩形 (Largest Rectangle in Histogram)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。
|
||||
|
||||
求在该柱状图中,能够勾勒出来的矩形的最大面积。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:单调栈(推荐)
|
||||
|
||||
**核心思想:**使用单调递增栈,存储柱子的索引。当遇到比栈顶小的柱子时,弹出栈顶并计算面积。
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func largestRectangleArea(heights []int) int {
|
||||
stack := []int{}
|
||||
maxArea := 0
|
||||
n := len(heights)
|
||||
|
||||
for i := 0; i <= n; i++ {
|
||||
h := 0
|
||||
if i < n {
|
||||
h = heights[i]
|
||||
}
|
||||
|
||||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||||
height := heights[stack[len(stack)-1]]
|
||||
stack = stack[:len(stack)-1]
|
||||
|
||||
width := i
|
||||
if len(stack) > 0 {
|
||||
width = i - stack[len(stack)-1] - 1
|
||||
}
|
||||
|
||||
area := height * width
|
||||
if area > maxArea {
|
||||
maxArea = area
|
||||
}
|
||||
}
|
||||
|
||||
stack = append(stack, i)
|
||||
}
|
||||
|
||||
return maxArea
|
||||
}
|
||||
```
|
||||
|
||||
- LeetCode 85: 最大矩形(二维版本)
|
||||
- LeetCode 42: 接雨水
|
||||
524
16-LeetCode Hot 100/电话号码的字母组合.md
Normal file
524
16-LeetCode Hot 100/电话号码的字母组合.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# 电话号码的字母组合 (Letter Combinations of a Phone Number)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个仅包含数字 `2-9` 的字符串,返回所有它能表示的字母组合。答案可以按 **任意顺序** 返回。
|
||||
|
||||
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
|
||||
|
||||
```
|
||||
2: abc
|
||||
3: def
|
||||
4: ghi
|
||||
5: jkl
|
||||
6: mno
|
||||
7: pqrs
|
||||
8: tuv
|
||||
9: wxyz
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:digits = "23"
|
||||
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:digits = ""
|
||||
输出:[]
|
||||
```
|
||||
|
||||
**示例 3:**
|
||||
```
|
||||
输入:digits = "2"
|
||||
输出:["a","b","c"]
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- `0 <= digits.length <= 4`
|
||||
- `digits[i]` 是范围 `['2', '9']` 的一个数字。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:回溯法(推荐)
|
||||
|
||||
**核心思想:**使用回溯算法遍历所有可能的字母组合。每次递归处理一个数字,尝试该数字对应的所有字母。
|
||||
|
||||
**算法步骤:**
|
||||
1. 建立数字到字母的映射表
|
||||
2. 如果输入为空,直接返回空数组
|
||||
3. 使用回溯函数生成组合:
|
||||
- 当前索引等于 `digits` 长度时,将当前组合加入结果
|
||||
- 否则,遍历当前数字对应的所有字母,递归处理下一个数字
|
||||
|
||||
### 方法二:队列迭代法
|
||||
|
||||
**核心思想:**使用队列逐层构建所有可能的组合。每次处理一个数字,将队列中所有组合与该数字对应的所有字母组合。
|
||||
|
||||
**算法步骤:**
|
||||
1. 建立数字到字母的映射表
|
||||
2. 初始化队列为空字符串
|
||||
3. 对于每个数字:
|
||||
- 取出队列中所有现有组合
|
||||
- 将每个组合与当前数字对应的所有字母拼接
|
||||
- 将新组合放回队列
|
||||
4. 返回队列中的所有组合
|
||||
|
||||
### 方法三:递归分治法
|
||||
|
||||
**核心思想:**将问题分解为子问题。对于 `digits = "23"`,先处理 `"2"` 得到 `["a","b","c"]`,再处理 `"3"` 得到 `["d","e","f"]`,最后组合所有可能。
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现(回溯法)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func letterCombinations(digits string) []string {
|
||||
if digits == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// 数字到字母的映射
|
||||
phoneMap := map[byte]string{
|
||||
'2': "abc",
|
||||
'3': "def",
|
||||
'4': "ghi",
|
||||
'5': "jkl",
|
||||
'6': "mno",
|
||||
'7': "pqrs",
|
||||
'8': "tuv",
|
||||
'9': "wxyz",
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
current := []byte{}
|
||||
|
||||
var backtrack func(index int)
|
||||
backtrack = func(index int) {
|
||||
if index == len(digits) {
|
||||
// 将当前组合加入结果
|
||||
result = append(result, string(current))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前数字对应的所有字母
|
||||
letters := phoneMap[digits[index]]
|
||||
for i := 0; i < len(letters); i++ {
|
||||
// 选择当前字母
|
||||
current = append(current, letters[i])
|
||||
// 递归处理下一个数字
|
||||
backtrack(index + 1)
|
||||
// 撤销选择(回溯)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0)
|
||||
return result
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
func main() {
|
||||
// 测试用例1
|
||||
digits1 := "23"
|
||||
fmt.Printf("输入: %s\n", digits1)
|
||||
fmt.Printf("输出: %v\n", letterCombinations(digits1))
|
||||
|
||||
// 测试用例2
|
||||
digits2 := ""
|
||||
fmt.Printf("\n输入: %s\n", digits2)
|
||||
fmt.Printf("输出: %v\n", letterCombinations(digits2))
|
||||
|
||||
// 测试用例3
|
||||
digits3 := "2"
|
||||
fmt.Printf("\n输入: %s\n", digits3)
|
||||
fmt.Printf("输出: %v\n", letterCombinations(digits3))
|
||||
|
||||
// 测试用例4: 最长输入
|
||||
digits4 := "9999"
|
||||
fmt.Printf("\n输入: %s\n", digits4)
|
||||
fmt.Printf("输出长度: %d\n", len(letterCombinations(digits4)))
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
func letterCombinationsIterative(digits string) []string {
|
||||
if digits == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
phoneMap := map[string]string{
|
||||
"2": "abc",
|
||||
"3": "def",
|
||||
"4": "ghi",
|
||||
"5": "jkl",
|
||||
"6": "mno",
|
||||
"7": "pqrs",
|
||||
"8": "tuv",
|
||||
"9": "wxyz",
|
||||
}
|
||||
|
||||
// 初始化队列
|
||||
queue := []string{""}
|
||||
|
||||
for _, digit := range digits {
|
||||
letters := phoneMap[string(digit)]
|
||||
newQueue := []string{}
|
||||
|
||||
// 取出队列中所有组合,与当前字母组合
|
||||
for _, combination := range queue {
|
||||
for i := 0; i < len(letters); i++ {
|
||||
newCombination := combination + string(letters[i])
|
||||
newQueue = append(newQueue, newCombination)
|
||||
}
|
||||
}
|
||||
|
||||
queue = newQueue
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
```
|
||||
|
||||
- **时间复杂度:** O(3^m × 4^n)
|
||||
- 其中 m 是对应 3 个字母的数字个数(2, 3, 4, 5, 6, 8)
|
||||
- n 是对应 4 个字母的数字个数(7, 9)
|
||||
- 最坏情况:所有数字都是 7 或 9,时间复杂度为 O(4^n)
|
||||
- 最好情况:所有数字都是 2 或 3,时间复杂度为 O(3^n)
|
||||
|
||||
- **空间复杂度:** O(m + n)
|
||||
- 其中 m 是输入数字的长度(递归栈深度)
|
||||
- n 是所有可能组合的总数
|
||||
- 需要存储结果数组,空间复杂度为 O(3^m × 4^n)
|
||||
|
||||
### 队列迭代法
|
||||
|
||||
- **时间复杂度:** O(3^m × 4^n)
|
||||
- 与回溯法相同,需要遍历所有可能的组合
|
||||
|
||||
- **空间复杂度:** O(3^m × 4^n)
|
||||
- 需要存储所有中间结果和最终结果
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如果数字字符串包含 '0' 和 '1',应该如何处理?
|
||||
|
||||
**A:** '0' 和 '1' 不对应任何字母,可以跳过或返回空字符串。
|
||||
|
||||
```go
|
||||
// Go 版本:跳过 0 和 1
|
||||
func letterCombinationsWithZero(digits string) []string {
|
||||
if digits == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
phoneMap := map[byte]string{
|
||||
'0': "",
|
||||
'1': "",
|
||||
'2': "abc",
|
||||
// ... 其他映射
|
||||
}
|
||||
|
||||
// 在回溯时,如果当前数字没有对应字母,直接跳过
|
||||
var backtrack func(index int)
|
||||
backtrack = func(index int) {
|
||||
if index == len(digits) {
|
||||
if len(current) > 0 { // 确保至少有一个字母
|
||||
result = append(result, string(current))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
letters := phoneMap[digits[index]]
|
||||
if letters == "" {
|
||||
// 跳过没有字母的数字
|
||||
backtrack(index + 1)
|
||||
} else {
|
||||
for i := 0; i < len(letters); i++ {
|
||||
current = append(current, letters[i])
|
||||
backtrack(index + 1)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如果要求结果按字典序排序,应该如何实现?
|
||||
|
||||
**A:** 在生成所有组合后,使用排序算法对结果进行排序。
|
||||
|
||||
```go
|
||||
import "sort"
|
||||
|
||||
func letterCombinationsSorted(digits string) []string {
|
||||
result := letterCombinations(digits)
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 如果只要求返回第 k 个组合(从 1 开始),应该如何优化?
|
||||
|
||||
**A:** 可以直接计算第 k 个组合,无需生成所有组合。
|
||||
|
||||
```go
|
||||
func getKthCombination(digits string, k int) string {
|
||||
if digits == "" || k <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
phoneMap := map[byte]string{
|
||||
'2': "abc",
|
||||
'3': "def",
|
||||
'4': "ghi",
|
||||
'5': "jkl",
|
||||
'6': "mno",
|
||||
'7': "pqrs",
|
||||
'8': "tuv",
|
||||
'9': "wxyz",
|
||||
}
|
||||
|
||||
result := make([]byte, len(digits))
|
||||
k-- // 转换为从 0 开始
|
||||
|
||||
for i := 0; i < len(digits); i++ {
|
||||
letters := phoneMap[digits[i]]
|
||||
count := len(letters)
|
||||
|
||||
// 计算当前位置应该选择哪个字母
|
||||
index := k % count
|
||||
result[i] = letters[index]
|
||||
|
||||
// 更新 k
|
||||
k /= count
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
```
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 1. 深度理解:回溯法的本质
|
||||
|
||||
**回溯法 = 暴力搜索 + 剪枝**
|
||||
|
||||
- **暴力搜索:**遍历所有可能的解空间
|
||||
- **剪枝:**在搜索过程中跳过不可能的解
|
||||
|
||||
**回溯法的三个关键要素:**
|
||||
1. **路径:**已经做出的选择
|
||||
2. **选择列表:**当前可以做的选择
|
||||
3. **结束条件:**到达决策树底层,无法再做选择
|
||||
|
||||
**回溯法框架:**
|
||||
|
||||
```go
|
||||
func backtrack(路径, 选择列表) {
|
||||
if 满足结束条件 {
|
||||
result = append(result, 路径)
|
||||
return
|
||||
}
|
||||
|
||||
for 选择 in 选择列表 {
|
||||
// 做选择
|
||||
路径.add(选择)
|
||||
backtrack(路径, 选择列表)
|
||||
// 撤销选择
|
||||
路径.remove(选择)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实战扩展:通用组合问题
|
||||
|
||||
#### 例子:生成所有有效的 IP 地址
|
||||
|
||||
**LeetCode 93:** 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
|
||||
|
||||
```go
|
||||
func restoreIpAddresses(s string) []string {
|
||||
result := []string{}
|
||||
if len(s) < 4 || len(s) > 12 {
|
||||
return result
|
||||
}
|
||||
|
||||
current := []string{}
|
||||
|
||||
var backtrack func(start int)
|
||||
backtrack = func(start int) {
|
||||
// 已经有 4 段,且用完了所有字符
|
||||
if len(current) == 4 {
|
||||
if start == len(s) {
|
||||
result = append(result, strings.Join(current, "."))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试取 1-3 个字符
|
||||
for i := 1; i <= 3 && start+i <= len(s); i++ {
|
||||
segment := s[start : start+i]
|
||||
|
||||
// 检查是否有效的 IP 段
|
||||
if (i > 1 && segment[0] == '0') || // 不能有前导 0
|
||||
(i == 3 && segment > "255") { // 不能大于 255
|
||||
continue
|
||||
}
|
||||
|
||||
current = append(current, segment)
|
||||
backtrack(start + i)
|
||||
current = current[:len(current)-1]
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 变形题目
|
||||
|
||||
#### 变形1:带权重的字母组合
|
||||
|
||||
每个数字对应字母,但字母有不同的权重(频率),要求按权重排序返回组合。
|
||||
|
||||
#### 变形2:键盘路径
|
||||
|
||||
给定两个数字,返回从第一个数字的字母到第二个数字的字母的所有路径。
|
||||
|
||||
#### 变形3:有效单词组合
|
||||
|
||||
给定数字字符串和单词列表,返回所有能组成的有效单词组合。
|
||||
|
||||
```go
|
||||
func letterCombinationsValidWords(digits string, wordList []string) []string {
|
||||
allCombinations := letterCombinations(digits)
|
||||
wordSet := make(map[string]bool)
|
||||
for _, word := range wordList {
|
||||
wordSet[word] = true
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
for _, combo := range allCombinations {
|
||||
if wordSet[combo] {
|
||||
result = append(result, combo)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 优化技巧
|
||||
|
||||
#### 优化1:提前终止
|
||||
|
||||
如果当前组合不可能形成有效解,提前终止递归。
|
||||
|
||||
```go
|
||||
func letterCombinationsPrune(digits string) []string {
|
||||
// 预先计算每个数字的字母数量
|
||||
letterCount := map[byte]int{
|
||||
'2': 3, '3': 3, '4': 3, '5': 3,
|
||||
'6': 3, '7': 4, '8': 3, '9': 4,
|
||||
}
|
||||
|
||||
// 计算总组合数
|
||||
totalCombinations := 1
|
||||
for _, digit := range digits {
|
||||
totalCombinations *= letterCount[digit]
|
||||
}
|
||||
|
||||
// 如果组合数过多,可以提前返回
|
||||
if totalCombinations > 10000 {
|
||||
return []string{} // 或者返回部分结果
|
||||
}
|
||||
|
||||
return letterCombinations(digits)
|
||||
}
|
||||
```
|
||||
|
||||
#### 优化2:并行处理
|
||||
|
||||
对于长数字字符串,可以并行处理不同分支。
|
||||
|
||||
```go
|
||||
func letterCombinationsParallel(digits string) []string {
|
||||
if len(digits) <= 2 {
|
||||
return letterCombinations(digits)
|
||||
}
|
||||
|
||||
// 分割任务
|
||||
mid := len(digits) / 2
|
||||
leftDigits := digits[:mid]
|
||||
rightDigits := digits[mid:]
|
||||
|
||||
// 并行处理
|
||||
leftCh := make(chan []string, 1)
|
||||
rightCh := make(chan []string, 1)
|
||||
|
||||
go func() {
|
||||
leftCh <- letterCombinations(leftDigits)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
rightCh <- letterCombinations(rightDigits)
|
||||
}()
|
||||
|
||||
leftCombinations := <-leftCh
|
||||
rightCombinations := <-rightCh
|
||||
|
||||
// 合并结果
|
||||
result := []string{}
|
||||
for _, left := range leftCombinations {
|
||||
for _, right := range rightCombinations {
|
||||
result = append(result, left+right)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 实际应用场景
|
||||
|
||||
- **短信验证码:** 生成验证码的所有可能组合
|
||||
- **密码破解:** 暴力破解基于数字密码的字母组合
|
||||
- **自动补全:** 输入部分数字时,提示所有可能的单词
|
||||
- **数据压缩:** 使用数字编码代替字母组合
|
||||
|
||||
### 6. 面试技巧
|
||||
|
||||
**面试官可能会问:**
|
||||
1. "回溯法和递归有什么区别?"
|
||||
2. "如何优化空间复杂度?"
|
||||
3. "如果输入非常长,如何处理?"
|
||||
|
||||
**回答要点:**
|
||||
1. 回溯法是递归的一种特殊形式,强调在搜索过程中撤销选择
|
||||
2. 使用迭代法可以减少递归栈空间
|
||||
3. 考虑分治、并行处理或者只返回部分结果
|
||||
|
||||
### 7. 相关题目推荐
|
||||
|
||||
- LeetCode 17: 电话号码的字母组合(本题)
|
||||
- LeetCode 22: 括号生成
|
||||
- LeetCode 39: 组合总和
|
||||
- LeetCode 46: 全排列
|
||||
- LeetCode 77: 组合
|
||||
- LeetCode 78: 子集
|
||||
- LeetCode 93: 复原 IP 地址
|
||||
374
16-LeetCode Hot 100/盛最多水的容器.md
Normal file
374
16-LeetCode Hot 100/盛最多水的容器.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# 盛最多水的容器 (Container With Most Water)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个长度为 `n` 的整数数组 `height`。有 `n` 条垂直线,第 `i` 条线的两个端点是 `(i, 0)` 和 `(i, height[i])`。
|
||||
|
||||
找出两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
|
||||
|
||||
返回容器可以储存的最大水量。
|
||||
|
||||
**说明:**你不能倾斜容器。
|
||||
|
||||
### 示例
|
||||
|
||||
**示例 1:**
|
||||
```
|
||||
输入:[1,8,6,2,5,4,8,3,7]
|
||||
输出:49
|
||||
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
|
||||
```
|
||||
|
||||
**示例 2:**
|
||||
```
|
||||
输入:[1,1]
|
||||
输出:1
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- `n == height.length`
|
||||
- `2 <= n <= 10^5`
|
||||
- `0 <= height[i] <= 10^4`
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 方法一:双指针法(最优解)
|
||||
|
||||
**核心思想:**使用两个指针,一个在数组开头,一个在数组末尾。每次移动较短的指针向中间靠拢。
|
||||
|
||||
**为什么这样做?**
|
||||
- 容器的容量由 `min(height[left], height[right]) * (right - left)` 决定
|
||||
- 如果移动较高的指针,宽度减小,高度只能保持不变或减小,容量一定不会增大
|
||||
- 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线,从而增大容量
|
||||
|
||||
**算法步骤:**
|
||||
1. 初始化 `left = 0`,`right = len(height) - 1`,`maxArea = 0`
|
||||
2. 当 `left < right` 时:
|
||||
- 计算当前面积:`area = min(height[left], height[right]) * (right - left)`
|
||||
- 更新 `maxArea = max(maxArea, area)`
|
||||
- 如果 `height[left] < height[right]`,则 `left++`,否则 `right--`
|
||||
3. 返回 `maxArea`
|
||||
|
||||
### 方法二:暴力枚举(不推荐)
|
||||
|
||||
枚举所有可能的线对,计算每对线构成的容器容量,取最大值。时间复杂度 O(n²),会超时。
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Go 实现
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func maxArea(height []int) int {
|
||||
left, right := 0, len(height)-1
|
||||
maxArea := 0
|
||||
|
||||
for left < right {
|
||||
// 计算当前面积
|
||||
width := right - left
|
||||
h := height[left]
|
||||
if height[right] < h {
|
||||
h = height[right]
|
||||
}
|
||||
area := width * h
|
||||
|
||||
// 更新最大面积
|
||||
if area > maxArea {
|
||||
maxArea = area
|
||||
}
|
||||
|
||||
// 移动较短的指针
|
||||
if height[left] < height[right] {
|
||||
left++
|
||||
} else {
|
||||
right--
|
||||
}
|
||||
}
|
||||
|
||||
return maxArea
|
||||
}
|
||||
|
||||
// 测试用例
|
||||
func main() {
|
||||
// 测试用例1
|
||||
height1 := []int{1, 8, 6, 2, 5, 4, 8, 3, 7}
|
||||
fmt.Printf("输入: %v\n", height1)
|
||||
fmt.Printf("输出: %d\n", maxArea(height1)) // 期望输出: 49
|
||||
|
||||
// 测试用例2
|
||||
height2 := []int{1, 1}
|
||||
fmt.Printf("\n输入: %v\n", height2)
|
||||
fmt.Printf("输出: %d\n", maxArea(height2)) // 期望输出: 1
|
||||
|
||||
// 测试用例3: 递增序列
|
||||
height3 := []int{1, 2, 3, 4, 5}
|
||||
fmt.Printf("\n输入: %v\n", height3)
|
||||
fmt.Printf("输出: %d\n", maxArea(height3)) // 期望输出: 6
|
||||
|
||||
// 测试用例4: 递减序列
|
||||
height4 := []int{5, 4, 3, 2, 1}
|
||||
fmt.Printf("\n输入: %v\n", height4)
|
||||
fmt.Printf("输出: %d\n", maxArea(height4)) // 期望输出: 6
|
||||
|
||||
// 测试用例5: 包含0
|
||||
height5 := []int{0, 2}
|
||||
fmt.Printf("\n输入: %v\n", height5)
|
||||
fmt.Printf("输出: %d\n", maxArea(height5)) // 期望输出: 0
|
||||
}
|
||||
```
|
||||
|
||||
- **时间复杂度:** O(n)
|
||||
- 只需遍历数组一次,每次移动一个指针
|
||||
- 指针最多移动 n 次
|
||||
|
||||
- **空间复杂度:** O(1)
|
||||
- 只使用了常数级别的额外空间
|
||||
- 只需要几个变量存储指针和最大面积
|
||||
|
||||
### 暴力枚举
|
||||
- **时间复杂度:** O(n²)
|
||||
- 需要枚举所有可能的线对
|
||||
- 共有 n(n-1)/2 种组合
|
||||
|
||||
- **空间复杂度:** O(1)
|
||||
- 只需要常数级别的额外空间
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如果可以倾斜容器,问题会如何变化?
|
||||
**A:** 如果可以倾斜容器,问题会变得复杂得多。需要考虑水的倾斜角度和容器的几何形状,这涉及到更多的物理和几何计算。
|
||||
|
||||
### Q2: 如果需要返回构成最大容器的两条线的索引,应该如何修改?
|
||||
**A:** 在更新 `maxArea` 的同时,记录当前的 `left` 和 `right` 索引。
|
||||
|
||||
```go
|
||||
// Go 版本
|
||||
func maxAreaWithIndex(height []int) (int, int, int) {
|
||||
left, right := 0, len(height)-1
|
||||
maxArea, bestL, bestR := 0, 0, 0
|
||||
|
||||
for left < right {
|
||||
h := height[left]
|
||||
if height[right] < h {
|
||||
h = height[right]
|
||||
}
|
||||
area := (right - left) * h
|
||||
|
||||
if area > maxArea {
|
||||
maxArea = area
|
||||
bestL, bestR = left, right
|
||||
}
|
||||
|
||||
if height[left] < height[right] {
|
||||
left++
|
||||
} else {
|
||||
right--
|
||||
}
|
||||
}
|
||||
|
||||
return maxArea, bestL, bestR
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 如果数组中有负数,应该如何处理?
|
||||
**A:** 如果高度可以为负数,需要先过滤掉负数或取绝对值。通常物理意义上的高度不应为负,但如果题目允许,可以这样处理:
|
||||
|
||||
```go
|
||||
// 处理负数:取绝对值
|
||||
func maxAreaWithNegative(height []int) int {
|
||||
left, right := 0, len(height)-1
|
||||
maxArea := 0
|
||||
|
||||
for left < right {
|
||||
h := height[left]
|
||||
if height[right] < h {
|
||||
h = height[right]
|
||||
}
|
||||
h = abs(h) // 取绝对值
|
||||
area := (right - left) * h
|
||||
maxArea = max(maxArea, area)
|
||||
|
||||
if abs(height[left]) < abs(height[right]) {
|
||||
left++
|
||||
} else {
|
||||
right--
|
||||
}
|
||||
}
|
||||
|
||||
return maxArea
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 1. 深度理解:为什么双指针法一定正确?
|
||||
|
||||
**数学证明:**
|
||||
|
||||
假设当前指针在 `left` 和 `right`,且 `height[left] < height[right]`。
|
||||
|
||||
我们要证明:移动 `right` 指针一定不会得到更大的面积。
|
||||
|
||||
- 当前面积:`S1 = height[left] * (right - left)`
|
||||
- 移动 `right` 后的面积:`S2 = min(height[left], height[right-1]) * (right - 1 - left)`
|
||||
- 由于 `height[left] < height[right]`,且 `right-1 < right`
|
||||
- 所以 `S2 <= height[left] * (right - 1 - left) < height[left] * (right - left) = S1`
|
||||
|
||||
因此,移动较高的指针不会得到更大的面积。
|
||||
|
||||
### 2. 实战扩展:接雨水问题 (Trapping Rain Water)
|
||||
|
||||
**LeetCode 42:** 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
|
||||
|
||||
```go
|
||||
func trap(height []int) int {
|
||||
if len(height) < 3 {
|
||||
return 0
|
||||
}
|
||||
|
||||
left, right := 0, len(height)-1
|
||||
leftMax, rightMax := 0, 0
|
||||
water := 0
|
||||
|
||||
for left < right {
|
||||
if height[left] < height[right] {
|
||||
if height[left] >= leftMax {
|
||||
leftMax = height[left]
|
||||
} else {
|
||||
water += leftMax - height[left]
|
||||
}
|
||||
left++
|
||||
} else {
|
||||
if height[right] >= rightMax {
|
||||
rightMax = height[right]
|
||||
} else {
|
||||
water += rightMax - height[right]
|
||||
}
|
||||
right--
|
||||
}
|
||||
}
|
||||
|
||||
return water
|
||||
}
|
||||
```
|
||||
|
||||
**核心区别:**
|
||||
- 盛水容器:找两条线构成最大面积
|
||||
- 接雨水:计算所有能接的雨水总量
|
||||
|
||||
### 3. 变形题目
|
||||
|
||||
#### 变形1:盛最多水的容器 II(允许倾斜)
|
||||
|
||||
如果允许容器倾斜,最大水量取决于两条线之间的最小距离和角度。
|
||||
|
||||
#### 变形2:三维盛水
|
||||
|
||||
给定一个 m × n 的矩阵,每个格子表示高度,找出能盛最多水的四个角构成的容器。
|
||||
|
||||
#### 变形3:动态盛水
|
||||
|
||||
容器的高度会随时间变化,求某个时间段内能盛的最大水量。
|
||||
|
||||
### 4. 优化技巧
|
||||
|
||||
#### 优化1:提前终止
|
||||
|
||||
如果当前可能的面积(即使宽度最大)已经小于 `maxArea`,可以提前终止。
|
||||
|
||||
```go
|
||||
func maxAreaOptimized(height []int) int {
|
||||
left, right := 0, len(height)-1
|
||||
maxArea := 0
|
||||
|
||||
for left < right {
|
||||
area := (right - left) * min(height[left], height[right])
|
||||
maxArea = max(maxArea, area)
|
||||
|
||||
// 提前终止:如果当前宽度已经很窄,可能无法超过maxArea
|
||||
if right-left <= maxArea/max(height[left], height[right]) {
|
||||
break
|
||||
}
|
||||
|
||||
if height[left] < height[right] {
|
||||
left++
|
||||
} else {
|
||||
right--
|
||||
}
|
||||
}
|
||||
|
||||
return maxArea
|
||||
}
|
||||
```
|
||||
|
||||
#### 优化2:跳过明显不可能的线
|
||||
|
||||
如果移动后的线高度比移动前还低,可以继续移动直到找到更高的线。
|
||||
|
||||
```go
|
||||
func maxAreaSkip(height []int) int {
|
||||
left, right := 0, len(height)-1
|
||||
maxArea := 0
|
||||
|
||||
for left < right {
|
||||
area := (right - left) * min(height[left], height[right])
|
||||
maxArea = max(maxArea, area)
|
||||
|
||||
if height[left] < height[right] {
|
||||
oldLeft := left
|
||||
left++
|
||||
// 跳过比oldLeft还低的线
|
||||
for left < right && height[left] <= height[oldLeft] {
|
||||
left++
|
||||
}
|
||||
} else {
|
||||
oldRight := right
|
||||
right--
|
||||
// 跳过比oldRight还低的线
|
||||
for left < right && height[right] <= height[oldRight] {
|
||||
right--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxArea
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 实际应用场景
|
||||
|
||||
- **水库设计:** 计算水库的最大蓄水量
|
||||
- **城市规划:** 确定建筑物之间的最佳距离以最大化绿化面积
|
||||
- **数据压缩:** 在某些压缩算法中寻找最优的分段点
|
||||
|
||||
### 6. 面试技巧
|
||||
|
||||
**面试官可能会问:**
|
||||
1. "为什么双指针法一定能找到最优解?"
|
||||
2. "如果数组有 10^8 个元素,你的算法还能用吗?"
|
||||
3. "如何证明你的算法是正确的?"
|
||||
|
||||
**回答要点:**
|
||||
1. 给出数学证明(如上所述)
|
||||
2. 讨论算法的局限性,考虑分布式处理
|
||||
3. 提供正确的证明思路
|
||||
|
||||
### 7. 相关题目推荐
|
||||
|
||||
- LeetCode 42: 接雨水
|
||||
- LeetCode 11: 盛最多水的容器(本题)
|
||||
- LeetCode 84: 柱状图中最大的矩形
|
||||
- LeetCode 85: 最大矩形
|
||||
24
16-LeetCode Hot 100/翻转二叉树.md
Normal file
24
16-LeetCode Hot 100/翻转二叉树.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 翻转二叉树 (Invert Binary Tree)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 递归 / 迭代
|
||||
|
||||
## Go 代码(递归)
|
||||
|
||||
```go
|
||||
func invertTree(root *TreeNode) *TreeNode {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(h) 空间
|
||||
28
16-LeetCode Hot 100/路径总和.md
Normal file
28
16-LeetCode Hot 100/路径总和.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 路径总和 (Path Sum)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### DFS
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||||
if root == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if root.Left == nil && root.Right == nil {
|
||||
return root.Val == targetSum
|
||||
}
|
||||
|
||||
return hasPathSum(root.Left, targetSum-root.Val) ||
|
||||
hasPathSum(root.Right, targetSum-root.Val)
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(h) 空间
|
||||
37
16-LeetCode Hot 100/除自身以外数组的乘积.md
Normal file
37
16-LeetCode Hot 100/除自身以外数组的乘积.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 除自身以外数组的乘积 (Product of Array Except Self)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 左右乘积列表
|
||||
|
||||
分别计算每个位置的左侧乘积和右侧乘积,然后相乘。
|
||||
|
||||
## Go 代码
|
||||
|
||||
```go
|
||||
func productExceptSelf(nums []int) []int {
|
||||
n := len(nums)
|
||||
answer := make([]int, n)
|
||||
|
||||
// 左侧乘积
|
||||
answer[0] = 1
|
||||
for i := 1; i < n; i++ {
|
||||
answer[i] = answer[i-1] * nums[i-1]
|
||||
}
|
||||
|
||||
// 右侧乘积并更新
|
||||
right := 1
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
answer[i] = answer[i] * right
|
||||
right *= nums[i]
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
```
|
||||
|
||||
**复杂度:** O(n) 时间,O(1) 额外空间(不包括输出数组)
|
||||
Reference in New Issue
Block a user