vault backup: 2026-05-16 14:15:58

This commit is contained in:
2026-05-16 14:15:58 +08:00
parent 9067e29acb
commit 128bac36e7
13 changed files with 1714 additions and 0 deletions
@@ -0,0 +1,279 @@
---
tags: [笔试, 微派, 链表, 双指针, Go]
create time: 2026-05-16 14:40
---
# 02 - 删除链表倒数第 n 个节点
## 题面
**输入**:
1. 一个单向链表的头结点 `head`
2. 整数 `n`(`1 ≤ n ≤ 链表长度`)
**输出**: 删除倒数第 n 个节点后的链表头结点
**要求**:
- ACM 模式:从 stdin 读取,stdout 输出结果
- **一次遍历**完成(O(1) 额外空间)
- 被删除节点一定存在
**示例 1**:
```
输入: head = [1, 2, 3, 4, 5], n = 2
输出: [1, 2, 3, 5]
解释: 倒数第 2 个是 4,删除后得到 1->2->3->5
```
**示例 2**:
```
输入: head = [1], n = 1
输出: []
解释: 只有一个节点,删除它后链表为空
```
**示例 3**:
```
输入: head = [1, 2], n = 1
输出: [1]
解释: 倒数第 1 个(即尾节点 2),删除后剩下 1
```
> [!question] 💡 思考一下
> 如果题目说"只允许遍历一次",为什么不能先走一遍求长度再走一遍删除?
---
## 思路
### 方法一:快慢指针(双指针)⭐ 最优
#### 核心思想
```mermaid
flowchart LR
A["dummy -> 1 -> 2 -> 3 -> 4 -> 5<br/>n = 2"] --> B["fast 和 slow 都从 dummy 出发"]
B --> C["第一步: fast 先走 n+1 = 3 步"]
C --> D["此时 fast=dummy, slow=dummy<br/>fast 在空位, slow 在 dummy"]
D --> E["第二步: 一起走直到 fast == nil"]
E --> F["slow 停在 3 的位置"]
F --> G["slow.Next = slow.Next.Next<br/>跳过节点 4"]
G --> H["删除完成: 1->2->3->5"]
```
#### 关键步骤
1. **创建虚拟头结点 `dummy`**,指向 `head`
- 为什么要 dummy?因为**可能删除头结点本身**(如示例 2),用 dummy 可以避免特殊判断。
2. **快指针 `fast` 先走 n+1 步**
- 走到 n+1 而不是 n,是为了让慢指针最终停在**要删除节点的前驱**。
3. **快慢指针同步前进**,直到 `fast == nil`
4. **执行删除**: `slow.Next = slow.Next.Next`
#### 为什么是 n+1 步?
```
链表: dummy → 1 → 2 → 3 → 4 → 5
↑
删除倒数第 2 个(节点 4)
fast 先走 3 步 (n+1):
dummy → 1 → 2 → 3 → 4 → 5
^ ^ ^
dummy dummy(fast) ...继续...
不对,让我重新演示:
dummy → 1 → 2 → 3 → 4 → 5 → nil
^
fast 走1步 → 1
fast 走2步 → 2
fast 走3步 → 3 (n+1=3 步)
现在 fast/slow 同时走:
slow→1, fast→4 (step 1)
slow→2, fast→5 (step 2)
slow→3, fast→nil (step 3, 停止)
slow 停在 3,slow.Next = 4(要删除的节点)✓
```
> [!tip] 规律总结
> - 快指针先走 `n+1` 步
> - 当快指针到达 `nil` 时,慢指针恰好停在被删节点的**前驱**
> - 时间复杂度 O(n),空间复杂度 O(1)
---
### 方法二:两次遍历(备选)
虽然题目通常要求一次遍历,但了解这个方法有助于面试中展示全面性:
1. 第一次遍历计算链表长度 L
2. 第二次遍历走到正数第 `(L-n)` 个节点,执行删除
时间 O(n),空间 O(1),但需要**两次**遍历。
---
### 方法三:栈(备选)
1. 将所有节点入栈
2. 弹出 n 次,第 n 次弹出的就是要删除的节点
3. 栈顶元素即为前驱节点,执行删除
时间 O(n),空间 O(n)(不满足 O(1) 空间要求)。
---
## 代码提示
```go
// 1. 定义 ListNode 结构体
// 2. 创建 dummy := &ListNode{Next: head}
// 3. fast, slow := dummy, dummy
// 4. for i := 0; i <= n; i++ { fast = fast.Next } // 先走 n+1 步
// 5. for fast != nil { fast = fast.Next; slow = slow.Next }
// 6. slow.Next = slow.Next.Next // 删除
// 7. return dummy.Next
```
---
## 技巧
> [!note] ACM 模式的辅助函数
> 在 ACM 模式下,通常需要写 helper 函数来将数组转为链表,以及将链表输出为数组格式。这些虽然不是算法核心,但**必不可少**。
```go
// 常用模板:数组转链表
func arrToList(arr []int) *ListNode {
dummy := &ListNode{}
cur := dummy
for _, v := range arr {
cur.Next = &ListNode{Val: v}
cur = cur.Next
}
return dummy.Next
}
// 常用模板:链表转数组
func listToArray(head *ListNode) []int {
var result []int
for head != nil {
result = append(result, head.Val)
head = head.Next
}
return result
}
```
> [!warning] ⚠️ 边界检查
> 删除首节点的情况由 `dummy` 自动处理,不需要 `if head == nil` 判断。但要注意 `n` 超过链表长度的情况——题目保证合法时可跳过。
---
## 代码
```go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// ListNode 单链表节点定义
type ListNode struct {
Val int
Next *ListNode
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 第一行: 数组形式的链表 "1 2 3 4 5"
if !scanner.Scan() {
return
}
parts := strings.Fields(scanner.Text())
n := len(parts)
// 第二行: 目标 n
if !scanner.Scan() {
return
}
removeN, _ := strconv.Atoi(strings.TrimSpace(scanner.Text()))
// 数组转链表
head := arrToList(parts)
// 删除倒数第 n 个节点
newHead := removeNthFromEnd(head, removeN)
// 输出: 逗号分隔或空格分隔均可
output := listToArray(newHead)
if len(output) == 0 {
fmt.Println("[]")
} else {
fmt.Print("[")
for i, v := range output {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println("]")
}
}
// removeNthFromEnd 一次遍历删除倒数第 n 个节点
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 创建虚拟头结点,简化头节点删除的处理
dummy := &ListNode{Next: head}
fast, slow := dummy, dummy
// 快指针先走 n+1 步
for i := 0; i <= n; i++ {
fast = fast.Next
}
// 快慢指针同步前进,直到 fast 到达末尾
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// slow 现在停在要删除节点的前驱
slow.Next = slow.Next.Next
return dummy.Next
}
// arrToList 将字符串数组转换为链表
func arrToList(parts []string) *ListNode {
dummy := &ListNode{}
cur := dummy
for _, p := range parts {
v, _ := strconv.Atoi(p)
cur.Next = &ListNode{Val: v}
cur = cur.Next
}
return dummy.Next
}
// listToArray 将链表转换为切片
func listToArray(head *ListNode) []int {
var result []int
for head != nil {
result = append(result, head.Val)
head = head.Next
}
return result
}
```