Files
leetcode-go/笔试/微派 Test1/02-删除链表倒数第N个节点.md
T

280 lines
6.3 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.
---
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
}
```