Files
interview/16-LeetCode Hot 100/删除链表的倒数第N个结点.md
yasinshaw 15dbd75004 refactor: remove Java code sections from all LeetCode Hot 100 markdown files
- 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
2026-03-05 12:31:48 +08:00

487 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 删除链表的倒数第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)