6.3 KiB
6.3 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-05-16 14:40 |
02 - 删除链表倒数第 n 个节点
题面
输入:
- 一个单向链表的头结点
head - 整数
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] 💡 思考一下 如果题目说"只允许遍历一次",为什么不能先走一遍求长度再走一遍删除?
思路
方法一:快慢指针(双指针)⭐ 最优
核心思想
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"]
关键步骤
-
创建虚拟头结点
dummy,指向head- 为什么要 dummy?因为可能删除头结点本身(如示例 2),用 dummy 可以避免特殊判断。
-
快指针
fast先走 n+1 步- 走到 n+1 而不是 n,是为了让慢指针最终停在要删除节点的前驱。
-
快慢指针同步前进,直到
fast == nil -
执行删除:
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)
方法二:两次遍历(备选)
虽然题目通常要求一次遍历,但了解这个方法有助于面试中展示全面性:
- 第一次遍历计算链表长度 L
- 第二次遍历走到正数第
(L-n)个节点,执行删除
时间 O(n),空间 O(1),但需要两次遍历。
方法三:栈(备选)
- 将所有节点入栈
- 弹出 n 次,第 n 次弹出的就是要删除的节点
- 栈顶元素即为前驱节点,执行删除
时间 O(n),空间 O(n)(不满足 O(1) 空间要求)。
代码提示
// 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 函数来将数组转为链表,以及将链表输出为数组格式。这些虽然不是算法核心,但必不可少。
// 常用模板:数组转链表
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超过链表长度的情况——题目保证合法时可跳过。
代码
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
}