--- 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
n = 2"] --> B["fast 和 slow 都从 dummy 出发"] B --> C["第一步: fast 先走 n+1 = 3 步"] C --> D["此时 fast=dummy, slow=dummy
fast 在空位, slow 在 dummy"] D --> E["第二步: 一起走直到 fast == nil"] E --> F["slow 停在 3 的位置"] F --> G["slow.Next = slow.Next.Next
跳过节点 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 } ```