Files
interview/16-LeetCode Hot 100/删除链表的倒数第N个结点.md
yasinshaw f0833d63cf docs: 增强 LeetCode 题目解题思路详细程度
对两个 LeetCode Hot 100 题目进行了详细的思路改进:

## 改进内容

### 1. 两数相加.md
- 新增"思路推导"部分:从暴力解法分析到优化思考
- 详细化"解题思路":分步骤说明每个关键点
- 增加"关键细节说明":4个核心细节深入分析
- 增加"边界条件分析":4种边界情况完整演示
- 增加"Q&A 问题解释":5个常见问题详细解答
- 增加"执行过程演示":完整执行过程可视化
- 增加"常见错误":5个典型错误对比说明

### 2. 删除链表的倒数第N个结点.md
- 新增"思路推导"部分:暴力解法到双指针优化
- 详细化双指针法、计算长度法、栈法的完整流程
- 增加"关键细节说明":n+1步、哑节点等核心概念
- 增加"边界条件分析":删除头、尾、中间节点等场景
- 增加"Q&A 问题解释":5个核心问题深入解答
- 增加"执行过程演示":完整执行过程可视化
- 增加"常见错误":5个典型错误对比说明

## 改进效果
- 从简单算法流程升级为完整思考路径
- 从基础步骤说明升级为详细原理分析
- 从复杂度概览升级为逐步推导过程
- 增加了可视化执行过程和常见错误对比
- 更适合面试准备和深度理解

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:31:34 +08:00

1263 lines
26 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`
### 进阶
你能尝试使用一趟扫描实现吗?
## 思路推导
### 暴力解法分析
**思路1两次遍历 - 计算长度法**
```go
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 第一次遍历:计算链表长度
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
}
```
**时间复杂度**O(2L) = O(L),其中 L 是链表长度
**空间复杂度**O(1)
**问题分析**
-**思路清晰**:先算长度,再定位删除
-**两次遍历**:需要遍历链表两次
-**效率一般**:虽然时间复杂度是 O(L)但常数因子是2
### 优化思考
**观察**
- 题目进阶要求:**能否尝试使用一趟扫描实现?**
- 关键问题如何在一次遍历中找到倒数第N个节点
**关键洞察**
```
如果两个指针相距 N 个节点,当快指针到达末尾时,
慢指针恰好指向倒数第N个节点
示例删除倒数第2个节点 (n=2)
原始链表1 -> 2 -> 3 -> 4 -> 5
步骤1fast 先移动 n+1 = 3 步
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow
步骤2fast 和 slow 同时移动,直到 fast 到达末尾
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
步骤3此时 slow 指向要删除节点的前一个节点
要删除的是 4slow 指向 3
执行删除slow.Next = slow.Next.Next
结果1 -> 2 -> 3 -> 5
```
### 为什么这样思考?
**1. 双指针的巧妙之处**
- 快慢指针相距 N 个节点
- 当快指针到达末尾时,慢指针刚好在目标位置
- **类比**就像两个人赛跑一个人先跑N步然后同时跑当先跑的人到达终点时后跑的人距离终点还有N步
**2. 为什么是 n+1 步而不是 n 步?**
```
如果移动 n 步:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow
同时移动后:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow
此时 slow 指向要删除的节点本身,而不是前一个节点
删除操作需要前一个节点slow.Next = slow.Next.Next
如果移动 n+1 步:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow
同时移动后:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
此时 slow 指向要删除节点的前一个节点
可以方便删除slow.Next = slow.Next.Next
```
**3. 为什么需要哑节点?**
```
没有哑节点的情况:
head -> 1 -> 2 -> 3 -> 4 -> 5
如果要删除头节点倒数第5个节点
- slow 会指向 nilhead 的前一个节点不存在)
- 无法执行删除操作
有哑节点的情况:
dummy -> head -> 1 -> 2 -> 3 -> 4 -> 5
如果要删除头节点:
- slow 指向 dummy
- 执行 dummy.Next = dummy.Next.Next
- 成功删除 head 节点
```
**4. 时间复杂度的优化**
- 计算长度法O(2L) - 两次遍历
- 双指针法O(L) - 一次遍历
- 虽然都是 O(L),但双指针法的常数因子更小
---
## 解题思路
### 方法一:双指针法(推荐)
**核心思想**:使用两个指针 `fast``slow``fast` 先移动 `n+1` 步,然后 `fast``slow` 一起移动,直到 `fast` 到达链表末尾。此时 `slow` 指向要删除结点的前一个结点。
### 详细算法流程(双指针法)
**步骤1初始化哑节点和指针**
```go
dummy := &ListNode{0, head} // 哑节点,简化头节点删除
fast := dummy // 快指针
slow := dummy // 慢指针
```
**关键点**
- 为什么需要哑节点?
- 统一处理删除头节点的情况
- 避免 nil 指针的边界判断
- 为什么 fast 和 slow 都指向 dummy
- 保证 fast 和 slow 的距离准确
- 从同一个起点开始,距离计算更清晰
**步骤2fast 先移动 n+1 步**
```go
for i := 0; i <= n; i++ {
fast = fast.Next
}
```
**关键点**
- 为什么是 `i <= n`n+1 步)而不是 `i < n`n 步)?
- n+1 步slow 最终指向要删除节点的前一个节点
- n 步slow 最终指向要删除的节点本身
- 删除操作需要前一个节点
- 为什么要移动 n+1 步?
- 让 fast 和 slow 之间拉开 n 个节点的距离
- 当 fast 到达末尾nilslow 刚好在目标位置
**示例**
```
链表1 -> 2 -> 3 -> 4 -> 5
删除倒数第 2 个节点n=2
fast 移动 3 步n+1=3
初始: dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast/slow
第1步 dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
↑slow
第2步 dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
↑slow
第3步 dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
↑slow
```
**步骤3fast 和 slow 同时移动,直到 fast 为 nil**
```go
for fast != nil {
fast = fast.Next
slow = slow.Next
}
```
**关键点**
- 为什么条件是 `fast != nil`
- fast 最终会指向最后一个节点的 Next即 nil
- 当 fast 为 nil 时slow 刚好在目标位置
- 为什么 fast 和 slow 都移动一步?
- 保持两者之间的距离不变
- 维持 n 个节点的间距
**示例**
```
继续上面的例子:
fast 在节点 4slow 在 dummy
第1次同时移动
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow
第2次同时移动
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
fast 为 nil循环结束
slow 指向节点 3是要删除节点 4 的前一个节点
```
**步骤4删除节点**
```go
slow.Next = slow.Next.Next
```
**关键点**
- 为什么可以这样做?
- slow 指向要删除节点的前一个节点
- slow.Next 是要删除的节点
- slow.Next.Next 是要删除节点的下一个节点
- 直接跳过要删除的节点
**步骤5返回结果**
```go
return dummy.Next
```
**关键点**
- 为什么返回 `dummy.Next` 而不是 `dummy`
- dummy 是哑节点,不是链表的一部分
- dummy.Next 才是真正的链表头
- 即使删除了头节点,也能正确返回
---
### 关键细节说明
**细节1为什么是 n+1 步?**
```go
// ❌ 错误:移动 n 步
for i := 0; i < n; i++ {
fast = fast.Next
}
// 结果slow 指向要删除的节点本身,无法删除
// ✅ 正确:移动 n+1 步
for i := 0; i <= n; i++ {
fast = fast.Next
}
// 结果slow 指向要删除节点的前一个节点
```
**图解**
```
链表1 -> 2 -> 3 -> 4 -> 5
删除倒数第 2 个节点n=2即删除节点 4
移动 n 步(错误):
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
同时移动后:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
slow 指向节点 4无法删除自己
❌ 无法执行 slow.Next = slow.Next.Next
移动 n+1 步(正确):
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
同时移动后:
fast
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
slow
slow 指向节点 3可以删除下一个节点 4
✅ 执行 slow.Next = slow.Next.Next 成功
```
**细节2为什么需要哑节点**
```go
// ❌ 没有哑节点:删除头节点会出错
func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode {
fast := head
slow := head
// fast 移动 n 步
for i := 0; i < n; i++ {
fast = fast.Next
}
// 同时移动
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// 如果要删除的是头节点slow 会是 nil
// slow.Next 会空指针异常
slow.Next = slow.Next.Next
return head
}
// ✅ 有哑节点:统一处理
func removeNthFromEndWithDummy(head *ListNode, n int) *ListNode {
dummy := &ListNode{0, head}
fast := dummy
slow := dummy
// fast 移动 n+1 步
for i := 0; i <= n; i++ {
fast = fast.Next
}
// 同时移动
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// 即使删除头节点slow 也指向 dummy不会是 nil
slow.Next = slow.Next.Next
return dummy.Next
}
```
**细节3边界条件 - 只有一个节点**
```
输入head = [1], n = 1
步骤:
1. dummy -> 1 -> nil
2. fast 移动 2 步n+1=2
- 第1步fast = 1
- 第2步fast = nil
3. fast 已经是 nil不进入同时移动的循环
4. slow 指向 dummy
5. 执行 slow.Next = slow.Next.Next
- dummy.Next = dummy.Next.Next
- dummy.Next = 1.Next = nil
6. 返回 dummy.Next = nil
输出:[]
```
**细节4边界条件 - 删除最后一个节点**
```
输入head = [1,2], n = 1
步骤:
1. dummy -> 1 -> 2 -> nil
2. fast 移动 2 步n+1=2
- 第1步fast = 1
- 第2步fast = 2
3. 同时移动
- 第1次fast = nil, slow = 1
4. slow 指向节点 1
5. 执行 slow.Next = slow.Next.Next
- 1.Next = 1.Next.Next
- 1.Next = 2.Next = nil
6. 返回 dummy.Next = 1
输出:[1]
```
---
### 边界条件分析
**边界1删除头节点**
```
输入head = [1,2,3,4,5], n = 5
过程:
- fast 移动 6 步n+1=6
- fast 最终为 nil
- slow 还在 dummy没有移动
- 执行 dummy.Next = dummy.Next.Next
- dummy.Next 原本指向 1现在指向 2
输出:[2,3,4,5]
```
**边界2删除尾节点**
```
输入head = [1,2,3,4,5], n = 1
过程:
- fast 移动 2 步n+1=2fast 在节点 2
- 同时移动:
- 第1次fast=3, slow=1
- 第2次fast=4, slow=2
- 第3次fast=5, slow=3
- 第4次fast=nil, slow=4
- slow 指向节点 4
- 执行 4.Next = 4.Next.Next = nil
输出:[1,2,3,4]
```
**边界3删除中间节点**
```
输入head = [1,2,3,4,5], n = 3
过程:
- fast 移动 4 步n+1=4fast 在节点 4
- 同时移动:
- 第1次fast=5, slow=1
- 第2次fast=nil, slow=2
- slow 指向节点 2
- 执行 2.Next = 2.Next.Next = 4
输出:[1,2,4,5]
```
**边界4只有一个节点**
```
输入head = [1], n = 1
过程:
- fast 移动 2 步n+1=2fast 为 nil
- 没有进入同时移动循环
- slow 指向 dummy
- 执行 dummy.Next = dummy.Next.Next = nil
输出:[]
```
---
### Q&A 问题解释
**Q1为什么双指针法只需要一次遍历**
A
- 快指针先走 n+1 步,建立了 n 个节点的距离
- 然后快慢指针同时移动,保持这个距离
- 当快指针到达末尾时,慢指针刚好在目标位置
- 整个过程只需要遍历链表一次
**Q2如果 n 大于链表长度怎么办?**
A
- 根据题目约束:`1 <= n <= sz`sz 是链表长度)
- 所以 n 不会大于链表长度
- 但如果需要防御性编程,可以添加检查:
```go
// 计算链表长度
length := 0
current := head
for current != nil {
length++
current = current.Next
}
// 检查 n 是否合法
if n > length {
return head // n 超出范围,不删除
}
```
**Q3为什么栈法的空间复杂度是 O(L)**
A
- 栈法需要将所有节点压入栈
- 栈的大小等于链表长度
- 所以空间复杂度是 O(L)
- 双指针法只需要几个指针变量,空间复杂度是 O(1)
**Q4三种方法各有什么优缺点**
A
```
双指针法(推荐):
✅ 一次遍历,时间最优
✅ 空间复杂度 O(1)
✅ 代码简洁优雅
❌ 思路相对复杂
计算长度法:
✅ 思路清晰,容易理解
✅ 空间复杂度 O(1)
❌ 需要两次遍历
❌ 时间复杂度常数因子较大
栈法:
✅ 思路直观
✅ 容易理解和实现
❌ 空间复杂度 O(L)
❌ 不是最优解
```
**Q5如何处理循环链表**
A
- 需要先检测链表是否有环
- 如果有环,计算环的长度
- 然后调整删除位置
- 详见"进阶问题"部分
---
### 复杂度分析(详细版)
**双指针法**
```
时间复杂度:
- fast 先移动 n+1 步O(n)
- fast 和 slow 同时移动O(L-n)
- 总计O(n) + O(L-n) = O(L)
空间复杂度:
- 只使用固定数量的指针dummy, fast, slow
- 不随输入规模增长
- 总计O(1)
```
**计算长度法**
```
时间复杂度:
- 第一次遍历计算长度O(L)
- 第二次遍历删除节点O(L-n)
- 总计O(L) + O(L-n) = O(2L) = O(L)
空间复杂度:
- 只使用固定数量的变量
- 总计O(1)
```
**栈法**
```
时间复杂度:
- 遍历链表入栈O(L)
- 弹出 n 个节点O(n)
- 总计O(L) + O(n) = O(L)
空间复杂度:
- 栈存储所有节点O(L)
- 总计O(L)
```
---
### 执行过程演示
**示例输入**head = [1,2,3,4,5], n = 2
```
=== 初始状态 ===
dummy -> 1 -> 2 -> 3 -> 4 -> 5
fast/slow
=== fast 移动 n+1 = 3 步 ===
第1步
dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
slow
第2步
dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
slow
第3步
dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
slow
=== fast 和 slow 同时移动 ===
第1次同时移动
dummy -> 1 -> 2 -> 3 -> 4 -> 5
↑fast
slow
第2次同时移动
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nil
↑fast
slow
fast 为 nil循环结束
=== 删除节点 ===
slow 指向节点 3
执行 slow.Next = slow.Next.Next
即 3.Next = 3.Next.Next = 5
=== 最终结果 ===
dummy -> 1 -> 2 -> 3 -> 5
返回 dummy.Next = [1,2,3,5]
```
---
### 常见错误
**错误1fast 移动 n 步而不是 n+1 步**
```go
// ❌ 错误
for i := 0; i < n; i++ { // 只移动 n 步
fast = fast.Next
}
// slow 指向要删除的节点本身,无法删除
// ✅ 正确
for i := 0; i <= n; i++ { // 移动 n+1 步
fast = fast.Next
}
// slow 指向要删除节点的前一个节点
```
**错误2没有使用哑节点**
```go
// ❌ 错误:删除头节点时会出错
func removeNthFromEnd(head *ListNode, n int) *ListNode {
fast := head
slow := head
// ... 移动逻辑
slow.Next = slow.Next.Next // 删除头节点时 slow 可能是 nil
return head
}
// ✅ 正确
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{0, head} // 使用哑节点
fast := dummy
slow := dummy
// ... 移动逻辑
slow.Next = slow.Next.Next // 不会空指针
return dummy.Next
}
```
**错误3返回 dummy 而不是 dummy.Next**
```go
// ❌ 错误:多了一个哑节点
return dummy // 返回 [0,1,2,3,5]
// ✅ 正确
return dummy.Next // 返回 [1,2,3,5]
```
**错误4同时移动时忘记移动 slow**
```go
// ❌ 错误:只移动了 fast
for fast != nil {
fast = fast.Next
// 忘记移动 slow
}
// ✅ 正确
for fast != nil {
fast = fast.Next
slow = slow.Next // 也要移动 slow
}
```
**错误5删除节点时直接操作 head**
```go
// ❌ 错误:无法处理删除头节点的情况
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// ... 找到要删除的节点
if 要删除的是头节点 {
head = head.Next // 这样修改不会影响返回值
}
return head
}
// ✅ 正确:使用哑节点统一处理
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{0, head}
// ... 删除逻辑
return dummy.Next // 即使删除头节点也能正确返回
}
```
---
### 方法二:计算长度法
**核心思想**:先遍历链表计算长度,然后计算要删除的正数位置,再遍历到该位置删除结点。
**详细算法流程**
**步骤1计算链表长度**
```go
length := 0
current := head
for current != nil {
length++
current = current.Next
}
```
**步骤2计算要删除的正数位置**
```go
pos := length - n // 倒数第 n 个 = 正数第 (length-n) 个
```
**步骤3创建哑节点并移动到目标位置**
```go
dummy := &ListNode{0, head}
current = dummy
for i := 0; i < pos; i++ {
current = current.Next
}
```
**步骤4删除节点**
```go
current.Next = current.Next.Next
```
**步骤5返回结果**
```go
return dummy.Next
```
---
### 方法三:栈法
**核心思想**:将所有结点压入栈中,然后弹出 `n` 个结点,栈顶就是要删除结点的前一个结点。
**详细算法流程**
**步骤1创建哑节点**
```go
dummy := &ListNode{0, head}
```
**步骤2将所有节点压入栈**
```go
var stack []*ListNode
current := dummy
for current != nil {
stack = append(stack, current)
current = current.Next
}
```
**步骤3弹出 n 个节点**
```go
for i := 0; i < n; i++ {
stack = stack[:len(stack)-1]
}
```
**步骤4删除节点**
```go
prev := stack[len(stack)-1]
prev.Next = prev.Next.Next
```
**步骤5返回结果**
```go
return dummy.Next
```
**优缺点**
- ✅ 思路直观,容易理解
- ❌ 空间复杂度 O(L),不是最优解
- ❌ 不推荐使用,双指针法更优
## 解法
### 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)