vault backup: 2026-03-05 12:23:56

This commit is contained in:
2026-03-05 12:23:56 +08:00
parent bc0ed186c8
commit dcd3e136ec
7 changed files with 2571 additions and 8 deletions

View File

@@ -196,6 +196,13 @@
},
"active": "fcbc762a80282002",
"lastOpenFiles": [
"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",
@@ -217,20 +224,13 @@
"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-中间件"
]
}

View File

@@ -0,0 +1,313 @@
# 三数之和 (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),不考虑结果存储
---
## Go 解法
```go
func threeSum(nums []int) [][]int {
result := [][]int{}
n := len(nums)
// 排序
sort.Ints(nums)
for i := 0; i < n-2; i++ {
// 去重:跳过重复的第一个数
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 优化:如果最小数 > 0后面不可能有解
if nums[i] > 0 {
break
}
// 双指针
left, right := i+1, n-1
for left < right {
sum := nums[i] + nums[left] + nums[right]
if sum == 0 {
result = append(result, []int{nums[i], nums[left], nums[right]})
// 去重:跳过重复的 left
for left < right && nums[left] == nums[left+1] {
left++
}
// 去重:跳过重复的 right
for left < right && nums[right] == nums[right-1] {
right--
}
// 移动指针
left++
right--
} else if sum < 0 {
// 和太小left 向右移
left++
} else {
// 和太大right 向左移
right--
}
}
}
return result
}
```
### Go 代码要点
1. `sort.Ints()` 排序
2. `append()` 添加结果
3. 多重去重逻辑
---
## Java 解法
```java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// 去重:跳过重复的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 优化:如果最小数 > 0后面不可能有解
if (nums[i] > 0) {
break;
}
// 双指针
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重:跳过重复的 left
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// 去重:跳过重复的 right
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
}
```
### Java 代码要点
1. `Arrays.sort()` 排序
2. `Arrays.asList()` 创建列表
3. `ArrayList` 存储结果
---
## 图解过程
```
数组: [-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)

View File

@@ -0,0 +1,227 @@
# 两数相加 (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),不考虑结果链表的空间
---
## Go 解法
```go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
// 哑节点,简化边界处理
dummy := &ListNode{Val: 0}
current := dummy
carry := 0
// 同时遍历两个链表
for l1 != nil || l2 != nil {
// 获取当前位的值如果链表已结束则为0
x, y := 0, 0
if l1 != nil {
x = l1.Val
l1 = l1.Next
}
if l2 != nil {
y = l2.Val
l2 = l2.Next
}
// 计算和与进位
sum := x + y + carry
carry = sum / 10
// 创建新节点
current.Next = &ListNode{Val: sum % 10}
current = current.Next
}
// 处理最后的进位
if carry > 0 {
current.Next = &ListNode{Val: carry}
}
return dummy.Next
}
```
### Go 代码要点
1. 使用哑节点简化头节点处理
2. 注意 Go 的 nil 判断
3. 整数除法和取模:`sum / 10``sum % 10`
---
## Java 解法
```java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 哑节点
ListNode dummy = new ListNode(0);
ListNode current = dummy;
int carry = 0;
// 同时遍历两个链表
while (l1 != null || l2 != null) {
// 获取当前位的值
int x = (l1 != null) ? l1.val : 0;
int y = (l2 != null) ? l2.val : 0;
// 计算和与进位
int sum = x + y + carry;
carry = sum / 10;
// 创建新节点
current.next = new ListNode(sum % 10);
current = current.next;
// 移动指针
if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}
// 处理最后的进位
if (carry > 0) {
current.next = new ListNode(carry);
}
return dummy.next;
}
}
```
### Java 代码要点
1. 三元运算符处理空指针
2. 对象引用操作current.next
3. 注意 null 判断
---
## 进阶问题
### 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)

View File

@@ -0,0 +1,691 @@
# 删除链表的倒数第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)
}
```
### Java 实现(双指针法)
```java
public class RemoveNthFromEnd {
// 链表结点定义
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public ListNode removeNthFromEnd(ListNode head, int n) {
// 创建哑结点,处理删除头结点的特殊情况
ListNode dummy = new ListNode(0, head);
ListNode fast = dummy;
ListNode slow = dummy;
// fast 先移动 n + 1 步
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// fast 和 slow 一起移动,直到 fast 为 null
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除 slow 的下一个结点
slow.next = slow.next.next;
return dummy.next;
}
// 辅助函数:创建链表
private ListNode createList(int[] nums) {
ListNode dummy = new ListNode();
ListNode current = dummy;
for (int num : nums) {
current.next = new ListNode(num);
current = current.next;
}
return dummy.next;
}
// 辅助函数:打印链表
private void printList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val);
if (current.next != null) {
System.out.print(" -> ");
}
current = current.next;
}
System.out.println();
}
// 测试用例
public static void main(String[] args) {
RemoveNthFromEnd solution = new RemoveNthFromEnd();
// 测试用例1
ListNode head1 = solution.createList(new int[]{1, 2, 3, 4, 5});
System.out.print("输入: ");
solution.printList(head1);
System.out.println("n = 2");
ListNode result1 = solution.removeNthFromEnd(head1, 2);
System.out.print("输出: ");
solution.printList(result1);
// 测试用例2: 删除头结点
ListNode head2 = solution.createList(new int[]{1});
System.out.print("\n输入: ");
solution.printList(head2);
System.out.println("n = 1");
ListNode result2 = solution.removeNthFromEnd(head2, 1);
System.out.print("输出: ");
solution.printList(result2);
// 测试用例3: 删除最后一个结点
ListNode head3 = solution.createList(new int[]{1, 2});
System.out.print("\n输入: ");
solution.printList(head3);
System.out.println("n = 1");
ListNode result3 = solution.removeNthFromEnd(head3, 1);
System.out.print("输出: ");
solution.printList(result3);
// 测试用例4: 长链表
ListNode head4 = solution.createList(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
System.out.print("\n输入: ");
solution.printList(head4);
System.out.println("n = 5");
ListNode result4 = solution.removeNthFromEnd(head4, 5);
System.out.print("输出: ");
solution.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
}
```
### Java 实现(栈法)
```java
import java.util.Stack;
public ListNode removeNthFromEndByStack(ListNode head, int n) {
// 创建哑结点
ListNode dummy = new ListNode(0, head);
// 将所有结点压入栈
Stack<ListNode> stack = new Stack<>();
ListNode current = dummy;
while (current != null) {
stack.push(current);
current = current.next;
}
// 弹出 n 个结点
for (int i = 0; i < n; i++) {
stack.pop();
}
// 栈顶就是要删除结点的前一个结点
ListNode prev = stack.peek();
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: 如果链表是循环链表,应该如何处理?
**A:** 需要先判断是否为循环链表,如果是,需要找到尾结点并断开循环。
```go
func removeNthFromEndCircular(head *ListNode, n int) *ListNode {
if head == nil {
return nil
}
// 检测是否有环
hasCycle := detectCycle(head)
if !hasCycle {
return removeNthFromEnd(head, n)
}
// 如果有环,需要先找到环的入口和长度
// 然后根据 n 的值决定如何删除
// 这是一个复杂的问题,需要更多边界条件处理
return head
}
func detectCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
```
### Q2: 如果要求删除前 n 个结点,应该如何修改?
**A:** 直接遍历到第 n-1 个结点,然后删除后续所有结点。
```go
func removeFirstN(head *ListNode, n int) *ListNode {
if n <= 0 {
return head
}
dummy := &ListNode{0, head}
current := dummy
// 移动到第 n 个结点的前一个结点
for i := 0; i < n && current != nil; i++ {
current = current.Next
}
if current != nil {
current.Next = nil
}
return dummy.Next
}
```
### Q3: 如果链表很长,如何优化内存使用?
**A:** 使用双指针法是最优的,因为它不需要额外的空间。另外,可以考虑使用尾递归优化(如果语言支持)。
## P7 加分项
### 1. 深度理解:为什么需要哑结点?
**哑结点的作用:**
1. **统一处理:** 避免单独处理删除头结点的特殊情况
2. **简化边界条件:** 当要删除的是头结点时,普通方法需要特殊处理
3. **代码简洁:** 使用哑结点后,删除操作统一为 `prev.next = prev.next.next`
**没有哑结点的问题:**
```go
// 没有哑结点的版本(需要特殊处理删除头结点)
func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode {
length := 0
current := head
for current != nil {
length++
current = current.Next
}
if n == length {
// 要删除的是头结点,特殊处理
return head.Next
}
pos := length - n
current = head
for i := 0; i < pos-1; i++ {
current = current.Next
}
current.Next = current.Next.Next
return head
}
```
### 2. 实战扩展:链表操作的通用技巧
#### 技巧1快慢指针的应用
- **找中点:** fast 移动 2 步slow 移动 1 步
- **找倒数第 k 个:** fast 先移动 k 步
- **检测环:** fast 移动 2 步slow 移动 1 步
```go
// 找链表中点
func findMiddle(head *ListNode) *ListNode {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
// 检测环
func hasCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
```
#### 技巧2虚拟头结点的使用
- **统一操作:** 避免边界条件判断
- **简化代码:** 使删除、插入操作更简洁
- **常见场景:** 删除操作、插入操作
### 3. 变形题目
#### 变形1删除链表中的重复元素
**LeetCode 83:** 删除排序链表中的重复元素,使得每个元素只出现一次。
```go
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil {
return nil
}
current := head
for current.Next != nil {
if current.Val == current.Next.Val {
current.Next = current.Next.Next
} else {
current = current.Next
}
}
return head
}
```
#### 变形2删除链表中的所有重复元素
**LeetCode 82:** 删除排序链表中所有重复的元素,只保留原始链表中没有重复出现的数字。
```go
func deleteDuplicatesAll(head *ListNode) *ListNode {
dummy := &ListNode{0, head}
prev := dummy
for prev.Next != nil {
curr := prev.Next
// 检查是否有重复
if curr.Next != nil && curr.Val == curr.Next.Val {
// 跳过所有重复的值
val := curr.Val
for curr != nil && curr.Val == val {
curr = curr.Next
}
prev.Next = curr
} else {
prev = prev.Next
}
}
return dummy.Next
}
```
#### 变形3旋转链表
**LeetCode 61:** 将链表每个节点向右移动 k 个位置。
```go
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil || k == 0 {
return head
}
// 计算链表长度并连接成环
length := 1
tail := head
for tail.Next != nil {
tail = tail.Next
length++
}
tail.Next = head
// 计算新的尾结点位置
k = k % length
stepsToNewTail := length - k
newTail := head
for i := 1; i < stepsToNewTail; i++ {
newTail = newTail.Next
}
newHead := newTail.Next
newTail.Next = nil
return newHead
}
```
### 4. 优化技巧
#### 优化1一次遍历删除多个结点
如果需要删除多个位置的结点,可以在一次遍历中完成。
```go
func removeNodes(head *ListNode, positions []int) *ListNode {
dummy := &ListNode{0, head}
posMap := make(map[int]bool)
for _, pos := range positions {
posMap[pos] = true
}
prev := dummy
curr := head
index := 1
for curr != nil {
if posMap[index] {
prev.Next = curr.Next
} else {
prev = curr
}
curr = curr.Next
index++
}
return dummy.Next
}
```
#### 优化2递归解法优雅但可能栈溢出
```go
func removeNthFromEndRecursive(head *ListNode, n int) *ListNode {
counter := 0
return removeHelper(head, &counter, n)
}
func removeHelper(node *ListNode, counter *int, n int) *ListNode {
if node == nil {
return nil
}
node.Next = removeHelper(node.Next, counter, n)
*counter++
if *counter == n {
return node.Next
}
return node
}
```
### 5. 实际应用场景
- **LRU 缓存:** 删除最近最少使用的数据
- **浏览器历史记录:** 删除特定位置的历史记录
- **文本编辑器:** 撤销操作(删除最近的修改)
- **任务队列:** 删除超时或取消的任务
### 6. 面试技巧
**面试官可能会问:**
1. "为什么选择双指针法而不是计算长度法?"
2. "如果链表很长,递归解法会有什么问题?"
3. "如何证明你的算法是正确的?"
**回答要点:**
1. 双指针法只需一次遍历,代码简洁,空间复杂度低
2. 递归可能导致栈溢出,对于长链表不推荐
3. 可以通过画图、举例、边界条件分析来证明正确性
### 7. 相关题目推荐
- LeetCode 19: 删除链表的倒数第 N 个结点(本题)
- LeetCode 61: 旋转链表
- LeetCode 83: 删除排序链表中的重复元素
- LeetCode 82: 删除排序链表中的所有重复元素
- LeetCode 206: 反转链表
- LeetCode 142: 环形链表 II

View File

@@ -0,0 +1,246 @@
# 无重复字符的最长子串 (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 解法
```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
}
```
### Go 代码要点
1. 使用 `range` 遍历字符串,自动处理 Unicode
2. `map[rune]int` 记录字符索引
3. 条件判断:`idx >= left` 确保在窗口内
---
## Java 解法
```java
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符最后出现的位置
Map<Character, Integer> charIndex = new HashMap<>();
int maxLength = 0;
int left = 0;
for (int right = 0; right < s.length(); right++) {
char char = s.charAt(right);
// 如果字符已存在且在窗口内,移动左边界
if (charIndex.containsKey(char) && charIndex.get(char) >= left) {
left = charIndex.get(char) + 1;
}
// 更新字符位置
charIndex.put(char, right);
// 更新最大长度
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
}
```
### Java 代码要点
1. `HashMap` 记录字符索引
2. `charAt()` 遍历字符串
3. `Math.max()` 更新最大值
---
## 图解过程
```
字符串: "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))

View File

@@ -0,0 +1,647 @@
# 电话号码的字母组合 (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)))
}
```
### Java 实现(回溯法)
```java
import java.util.ArrayList;
import java.util.List;
public class LetterCombinations {
public List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return result;
}
// 数字到字母的映射
String[] phoneMap = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz" // 9
};
StringBuilder current = new StringBuilder();
backtrack(digits, 0, phoneMap, current, result);
return result;
}
private void backtrack(String digits, int index, String[] phoneMap,
StringBuilder current, List<String> result) {
if (index == digits.length()) {
result.add(current.toString());
return;
}
// 获取当前数字对应的所有字母
int digit = digits.charAt(index) - '0';
String letters = phoneMap[digit];
for (int i = 0; i < letters.length(); i++) {
// 选择当前字母
current.append(letters.charAt(i));
// 递归处理下一个数字
backtrack(digits, index + 1, phoneMap, current, result);
// 撤销选择(回溯)
current.deleteCharAt(current.length() - 1);
}
}
// 测试用例
public static void main(String[] args) {
LetterCombinations solution = new LetterCombinations();
// 测试用例1
String digits1 = "23";
System.out.println("输入: " + digits1);
System.out.println("输出: " + solution.letterCombinations(digits1));
// 测试用例2
String digits2 = "";
System.out.println("\n输入: " + digits2);
System.out.println("输出: " + solution.letterCombinations(digits2));
// 测试用例3
String digits3 = "2";
System.out.println("\n输入: " + digits3);
System.out.println("输出: " + solution.letterCombinations(digits3));
// 测试用例4: 最长输入
String digits4 = "9999";
System.out.println("\n输入: " + digits4);
System.out.println("输出长度: " + solution.letterCombinations(digits4).size());
}
}
```
### Go 实现(队列迭代法)
```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
}
```
### Java 实现(队列迭代法)
```java
public List<String> letterCombinationsIterative(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return result;
}
String[] phoneMap = {
"", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
// 初始化队列
List<String> queue = new ArrayList<>();
queue.add("");
for (int i = 0; i < digits.length(); i++) {
int digit = digits.charAt(i) - '0';
String letters = phoneMap[digit];
List<String> newQueue = new ArrayList<>();
// 取出队列中所有组合,与当前字母组合
for (String combination : queue) {
for (int j = 0; j < letters.length(); j++) {
newQueue.add(combination + letters.charAt(j));
}
}
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 地址

View File

@@ -0,0 +1,439 @@
# 盛最多水的容器 (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
}
```
### Java 实现
```java
public class ContainerWithMostWater {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while (left < right) {
// 计算当前面积
int width = right - left;
int h = Math.min(height[left], height[right]);
int area = width * h;
// 更新最大面积
maxArea = Math.max(maxArea, area);
// 移动较短的指针
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
// 测试用例
public static void main(String[] args) {
ContainerWithMostWater solution = new ContainerWithMostWater();
// 测试用例1
int[] height1 = {1, 8, 6, 2, 5, 4, 8, 3, 7};
System.out.println("输入: [1, 8, 6, 2, 5, 4, 8, 3, 7]");
System.out.println("输出: " + solution.maxArea(height1)); // 期望输出: 49
// 测试用例2
int[] height2 = {1, 1};
System.out.println("\n输入: [1, 1]");
System.out.println("输出: " + solution.maxArea(height2)); // 期望输出: 1
// 测试用例3: 递增序列
int[] height3 = {1, 2, 3, 4, 5};
System.out.println("\n输入: [1, 2, 3, 4, 5]");
System.out.println("输出: " + solution.maxArea(height3)); // 期望输出: 6
// 测试用例4: 递减序列
int[] height4 = {5, 4, 3, 2, 1};
System.out.println("\n输入: [5, 4, 3, 2, 1]");
System.out.println("输出: " + solution.maxArea(height4)); // 期望输出: 6
// 测试用例5: 包含0
int[] height5 = {0, 2};
System.out.println("\n输入: [0, 2]");
System.out.println("输出: " + solution.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: 最大矩形