- Remove all "## Java 解法" sections and Java code blocks - Replace "## Go 解法" with "## 解法" - Remove "### Go 代码要点" and "### Java 代码要点" sections - Keep all Go code sections intact - Maintain complete documentation structure and content - Update 22 markdown files in the LeetCode Hot 100 directory
487 lines
11 KiB
Markdown
487 lines
11 KiB
Markdown
# 删除链表的倒数第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) |