diff --git a/docs/algorithm/linked-list-top10.md b/docs/algorithm/linked-list-top10.md new file mode 100644 index 0000000..d3ba6d9 --- /dev/null +++ b/docs/algorithm/linked-list-top10.md @@ -0,0 +1,1158 @@ +# 十大经典链表题(Go · ACM 模式) + +!!! note "💡 一句话概述" + 精选 10 道由易到难的 LeetCode 经典链表题,每道题重点讲解**核心思路、指针操作和边界处理**,并给出完整的 Go **ACM 模式**(可编译运行的含 `main` 输入输出)题解。 + +--- + +## 🔑 核心概念 + +1. **虚拟头节点(dummy node)** — 链表操作的第一法宝:在头节点前加一个哨兵节点,统一处理头节点被删除/替换的情况,避免大量 `if` 判断 +2. **快慢指针** — 链表操作的第二法宝:两个指针以不同速度遍历,常用于找中间节点、检测环、找倒数第 k 个节点 +3. **原地修改 vs 新建链表** — 明确题目要求:是修改原链表的 `Next` 指针,还是创建新节点组成新链表 +4. **指针断链前先保存** — 修改 `Next` 指针前,务必用临时变量保存原来的 `Next`,否则链表一断就找不回来了 +5. **递归 vs 迭代** — 链表天然具有递归结构(头 + 剩余链表),很多问题递归写法更简洁,但要注意栈深度 + +--- + +## 📝 详细说明 + +### 链表节点定义 + +本文所有题目使用以下链表节点定义: + +```go +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} +``` + +### 链表操作通用技巧 + +| 技巧 | 适用场景 | 说明 | +|------|----------|------| +| dummy node | 头节点可能变化 | 返回 `dummy.Next` 即可 | +| 快慢指针 | 中间节点、倒数第 k 个 | 快指针先走 k 步,或快指针速度是慢指针两倍 | +| 三指针(prev, curr, next) | 反转、插入、删除 | 保存三个状态,防止断链 | +| 哈希表辅助 | 检测环、相交 | 空间换时间 | +| 递归 | 反转、合并、回文 | 利用链表递归结构 | + +--- + +## 第 1 题:反转链表 + +!!! note "题目" + 给你单链表的头节点 `head`,请你反转链表,并返回反转后的链表头。(LeetCode 206) + +### 思路与操作 + +这是链表最基础的操作,核心是**三指针迭代**:`prev`(已反转部分的头)、`curr`(当前节点)、`next`(暂存下一个节点)。 + +- 初始:`prev = nil`,`curr = head` +- 每次循环:保存 `next = curr.Next`,将 `curr.Next` 指向 `prev`,然后 `prev = curr`,`curr = next` +- 结束条件:`curr == nil`,此时 `prev` 就是新的头节点 + +```mermaid +graph LR + subgraph 初始状态 + A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"] + end + subgraph 反转后 + E2["5"] --> D2["4"] --> C2["3"] --> B2["2"] --> A2["1"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 反转链表 - 迭代法 +func reverseList(head *ListNode) *ListNode { + var prev *ListNode + curr := head + for curr != nil { + next := curr.Next // 暂存下一个节点 + curr.Next = prev // 当前节点指向前一个节点 + prev = curr // prev 前进 + curr = next // curr 前进 + } + return prev +} + +func main() { + var n int + fmt.Scan(&n) + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + } + + // 反转链表 + head := reverseList(dummy.Next) + + // 输出结果 + for head != nil { + if head.Next != nil { + fmt.Printf("%d ", head.Val) + } else { + fmt.Printf("%d", head.Val) + } + head = head.Next + } + fmt.Println() +} +``` + +!!! tip "递归解法" + 递归解法更简洁,但要注意递归深度。`func reverseList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } newHead := reverseList(head.Next) head.Next.Next = head; head.Next = nil; return newHead }` + +--- + +## 第 2 题:检测环形链表 + +!!! note "题目" + 给你一个链表的头节点 `head`,判断链表中是否有环。如果链表中存在环,则返回 `true`;否则返回 `false`。(LeetCode 141) + +### 思路与操作 + +**快慢指针法**:快指针每次走 2 步,慢指针每次走 1 步。如果链表有环,快慢指针一定会在环内相遇(就像在操场跑步,跑得快的人一定会追上跑得慢的人)。 + +- 慢指针 `slow`:每次走 1 步 +- 快指针 `fast`:每次走 2 步 +- 如果 `fast` 或 `fast.Next` 变成 `nil`,说明无环 +- 如果 `fast == slow`,说明有环 + +```mermaid +graph LR + subgraph 有环链表 + A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"] + E --> C + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 检测环形链表 - 快慢指针 +func hasCycle(head *ListNode) bool { + if head == nil || head.Next == nil { + return false + } + slow := head // 慢指针:每次走 1 步 + fast := head.Next // 快指针:每次走 2 步 + + for slow != fast { + if fast == nil || fast.Next == nil { + return false // 快指针到头了,说明无环 + } + slow = slow.Next + fast = fast.Next.Next + } + return true // 快慢指针相遇,有环 +} + +func main() { + var n, pos int + fmt.Scan(&n, &n, &pos) // n 为节点数,pos 为环入口位置(-1 表示无环) + + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + nodes := make([]*ListNode, n) + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + nodes[i] = curr + } + + // 创建环(如果 pos != -1) + if pos != -1 && pos < n { + nodes[n-1].Next = nodes[pos] + } + + fmt.Println(hasCycle(dummy.Next)) +} +``` + +--- + +## 第 3 题:合并两个有序链表 + +!!! note "题目" + 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(LeetCode 21) + +### 思路与操作 + +**双指针比较法**:用两个指针分别遍历两个链表,每次选择较小的节点接到结果链表后面。 + +- 创建 `dummy` 节点作为结果链表的头 +- 比较 `l1.Val` 和 `l2.Val`,将较小的节点接到 `curr` 后面 +- 当一个链表遍历完,直接将另一个链表剩余部分接到 `curr` 后面 + +```mermaid +graph TD + subgraph 输入 + A1["1"] --> A2["2"] --> A3["4"] + B1["1"] --> B2["3"] --> B3["4"] + end + subgraph 输出 + C1["1"] --> C2["1"] --> C3["2"] --> C4["3"] --> C5["4"] --> C6["4"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 合并两个有序链表 +func mergeTwoLists(l1, l2 *ListNode) *ListNode { + dummy := &ListNode{} // 虚拟头节点 + curr := dummy + + for l1 != nil && l2 != nil { + if l1.Val <= l2.Val { + curr.Next = l1 + l1 = l1.Next + } else { + curr.Next = l2 + l2 = l2.Next + } + curr = curr.Next + } + + // 接上剩余部分 + if l1 != nil { + curr.Next = l1 + } + if l2 != nil { + curr.Next = l2 + } + + return dummy.Next +} + +func main() { + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 读取第一个链表 + sc.Scan() + var n1 int + fmt.Sscan(sc.Text(), &n1) + dummy1 := &ListNode{} + curr1 := dummy1 + for i := 0; i < n1; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr1.Next = &ListNode{Val: val} + curr1 = curr1.Next + } + + // 读取第二个链表 + sc.Scan() + var n2 int + fmt.Sscan(sc.Text(), &n2) + dummy2 := &ListNode{} + curr2 := dummy2 + for i := 0; i < n2; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr2.Next = &ListNode{Val: val} + curr2 = curr2.Next + } + + // 合并并输出 + head := mergeTwoLists(dummy1.Next, dummy2.Next) + for head != nil { + if head.Next != nil { + fmt.Printf("%d ", head.Val) + } else { + fmt.Printf("%d", head.Val) + } + head = head.Next + } + fmt.Println() +} +``` + +--- + +## 第 4 题:删除链表倒数第 N 个节点 + +!!! note "题目" + 给你一个链表,删除链表的倒数第 `n` 个节点,并且返回链表的头节点。(LeetCode 19) + +### 思路与操作 + +**快慢指针法**:让快指针先走 `n` 步,然后快慢指针一起走,当快指针到达末尾时,慢指针正好指向倒数第 `n` 个节点的前一个节点。 + +- 快指针 `fast` 先走 `n` 步 +- 快慢指针一起走,直到 `fast.Next == nil` +- 此时 `slow.Next` 就是倒数第 `n` 个节点,执行删除操作 + +```mermaid +graph LR + subgraph 示例 + A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"] + end + subgraph 删除倒数第2个 + A2["1"] --> B2["2"] --> C2["3"] --> E2["5"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 删除链表倒数第N个节点 +func removeNthFromEnd(head *ListNode, n int) *ListNode { + dummy := &ListNode{Next: head} // 虚拟头节点,处理删除头节点的情况 + fast := dummy + slow := dummy + + // 快指针先走 n+1 步(因为要找到倒数第 n 个的前一个) + for i := 0; i <= n; i++ { + fast = fast.Next + } + + // 快慢指针一起走 + for fast != nil { + fast = fast.Next + slow = slow.Next + } + + // 删除 slow.Next 节点 + slow.Next = slow.Next.Next + + return dummy.Next +} + +func main() { + var n, k int + fmt.Scan(&n) + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + } + fmt.Scan(&k) // 读取要删除的倒数第 k 个 + + // 删除并输出 + head := removeNthFromEnd(dummy.Next, k) + for head != nil { + if head.Next != nil { + fmt.Printf("%d ", head.Val) + } else { + fmt.Printf("%d", head.Val) + } + head = head.Next + } + fmt.Println() +} +``` + +--- + +## 第 5 题:链表的中间节点 + +!!! note "题目" + 给定一个头结点为 `head` 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。(LeetCode 876) + +### 思路与操作 + +**快慢指针法**:快指针每次走 2 步,慢指针每次走 1 步。当快指针到达末尾时,慢指针正好在中间。 + +- 快指针 `fast`:每次走 2 步 +- 慢指针 `slow`:每次走 1 步 +- 当 `fast == nil || fast.Next == nil` 时,`slow` 就是中间节点 + +```mermaid +graph LR + subgraph 奇数个节点 + A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"] + end + subgraph 中间节点 + C2["3"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 链表的中间节点 +func middleNode(head *ListNode) *ListNode { + slow := head // 慢指针:每次走 1 步 + fast := head // 快指针:每次走 2 步 + + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + + return slow +} + +func main() { + var n int + fmt.Scan(&n) + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + } + + // 找中间节点并输出 + mid := middleNode(dummy.Next) + fmt.Println(mid.Val) +} +``` + +--- + +## 第 6 题:相交链表 + +!!! note "题目" + 给你两个单链表的头节点 `headA` 和 `headB`,请你找出并返回两个单链表相交的起始节点。如果不存在相交节点,返回 `null`。(LeetCode 160) + +### 思路与操作 + +**双指针法**:让两个指针分别从两个链表头开始走,当走到末尾时切换到另一个链表的头。这样两个指针走过的总路程相等,如果有交点,一定会在交点相遇。 + +- 指针 `pA` 从 `headA` 开始,指针 `pB` 从 `headB` 开始 +- 当 `pA` 走到末尾,切换到 `headB`;当 `pB` 走到末尾,切换到 `headA` +- 如果有交点,`pA` 和 `pB` 会在交点相遇;如果无交点,最终都会变成 `nil` + +```mermaid +graph LR + subgraph 链表A + A1["4"] --> A2["1"] + end + subgraph 链表B + B1["5"] --> B2["0"] --> B3["1"] + end + subgraph 公共部分 + C1["8"] --> C2["4"] --> C3["5"] + end + A2 --> C1 + B3 --> C1 +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 相交链表 +func getIntersectionNode(headA, headB *ListNode) *ListNode { + if headA == nil || headB == nil { + return nil + } + + pA := headA + pB := headB + + for pA != pB { + if pA == nil { + pA = headB // 走到末尾,切换到 headB + } else { + pA = pA.Next + } + if pB == nil { + pB = headA // 走到末尾,切换到 headA + } else { + pB = pB.Next + } + } + + return pA +} + +func main() { + // 读取两个链表和相交点 + // 为了简化输入,这里假设输入格式为: + // 链表A长度 链表B长度 相交点索引(-1表示不相交) + var nA, nB, intersectIdx int + fmt.Scan(&nA, &nB, &intersectIdx) + + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建公共部分(如果相交) + var intersectNode *ListNode + if intersectIdx >= 0 { + // 读取相交部分 + var commonLen int + fmt.Scan(&commonLen) + dummy := &ListNode{} + curr := dummy + nodes := make([]*ListNode, commonLen) + for i := 0; i < commonLen; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + nodes[i] = curr + } + intersectNode = nodes[intersectIdx] + } + + // 构建链表A + dummyA := &ListNode{} + currA := dummyA + for i := 0; i < nA; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + currA.Next = &ListNode{Val: val} + currA = currA.Next + } + if intersectNode != nil { + currA.Next = intersectNode + } + + // 构建链表B + dummyB := &ListNode{} + currB := dummyB + for i := 0; i < nB; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + currB.Next = &ListNode{Val: val} + currB = currB.Next + } + if intersectNode != nil { + currB.Next = intersectNode + } + + // 查找交点 + result := getIntersectionNode(dummyA.Next, dummyB.Next) + if result != nil { + fmt.Println(result.Val) + } else { + fmt.Println("null") + } +} +``` + +--- + +## 第 7 题:回文链表 + +!!! note "题目" + 给你一个单链表的头节点 `head`,请你判断该链表是否为回文链表。如果是,返回 `true`;否则,返回 `false`。(LeetCode 234) + +### 思路与操作 + +**快慢指针 + 反转后半部分**:找到中间节点,反转后半部分链表,然后比较前半部分和后半部分。 + +- 快慢指针找中间节点 +- 反转后半部分链表 +- 比较前半部分和反转后的后半部分 +- 恢复链表(可选) + +```mermaid +graph LR + subgraph 原始链表 + A["1"] --> B["2"] --> C["3"] --> D["2"] --> E["1"] + end + subgraph 反转后半部分 + A2["1"] --> B2["2"] --> C2["3"] --> D2["2"] --> E2["1"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 反转链表 +func reverseList(head *ListNode) *ListNode { + var prev *ListNode + curr := head + for curr != nil { + next := curr.Next + curr.Next = prev + prev = curr + curr = next + } + return prev +} + +// 找中间节点 +func findMiddle(head *ListNode) *ListNode { + slow := head + fast := head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + return slow +} + +// 回文链表 +func isPalindrome(head *ListNode) bool { + if head == nil || head.Next == nil { + return true + } + + // 1. 找中间节点 + mid := findMiddle(head) + + // 2. 反转后半部分 + secondHalf := reverseList(mid) + + // 3. 比较前半部分和后半部分 + p1 := head + p2 := secondHalf + result := true + for p2 != nil { + if p1.Val != p2.Val { + result = false + break + } + p1 = p1.Next + p2 = p2.Next + } + + // 4. 恢复链表(可选,但面试中可能加分) + reverseList(secondHalf) + + return result +} + +func main() { + var n int + fmt.Scan(&n) + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + } + + fmt.Println(isPalindrome(dummy.Next)) +} +``` + +--- + +## 第 8 题:环形链表 II(找环入口) + +!!! note "题目** + 给定一个链表的头节点 `head`,返回链表开始入环的第一个节点。如果链表无环,则返回 `null`。(LeetCode 142) + +### 思路与操作 + +**快慢指针 + 数学推导**: + +设: +- `a` = 头节点到环入口的距离 +- `b` = 环入口到相遇点的距离 +- `c` = 环的长度 - `b` + +快指针走过的距离:`a + b + c` +慢指针走过的距离:`a + b` + +因为快指针速度是慢指针的 2 倍,所以:`2(a + b) = a + b + c` → `a = c` + +这意味着:从头节点出发走 `a` 步,从相遇点出发走 `c` 步,两者会在环入口相遇。 + +```mermaid +graph TD + A["头节点"] --> B["环入口"] + B --> C["相遇点"] + C --> B +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 环形链表 II - 找环入口 +func detectCycle(head *ListNode) *ListNode { + if head == nil || head.Next == nil { + return nil + } + + // 1. 快慢指针找相遇点 + slow := head + fast := head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + if slow == fast { + break + } + } + + // 2. 如果无环,返回 nil + if fast == nil || fast.Next == nil { + return nil + } + + // 3. 从头节点和相遇点同时出发,相遇点即为环入口 + p1 := head + p2 := slow // 相遇点 + for p1 != p2 { + p1 = p1.Next + p2 = p2.Next + } + + return p1 +} + +func main() { + var n, pos int + fmt.Scan(&n, &pos) + + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + nodes := make([]*ListNode, n) + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + nodes[i] = curr + } + + // 创建环(如果 pos != -1) + if pos != -1 && pos < n { + nodes[n-1].Next = nodes[pos] + } + + // 查找环入口 + entry := detectCycle(dummy.Next) + if entry != nil { + fmt.Println(entry.Val) + } else { + fmt.Println("null") + } +} +``` + +--- + +## 第 9 题:两数相加 + +!!! note "题目** + 给你两个非空链表,代表两个非负整数。它们每位数字都是按逆序存储的,每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。(LeetCode 2) + +### 思路与操作 + +**模拟加法**:从个位开始逐位相加,处理进位。 + +- 两个指针分别遍历两个链表 +- 每次将两个节点的值加上进位,计算当前位和新进位 +- 如果一个链表遍历完,用 0 代替 +- 最后如果还有进位,需要新增一个节点 + +```mermaid +graph LR + subgraph 输入 + A1["2"] --> A2["4"] --> A3["3"] + B1["5"] --> B2["6"] --> B3["4"] + end + subgraph 输出 + C1["7"] --> C2["0"] --> C3["8"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// 两数相加 +func addTwoNumbers(l1, l2 *ListNode) *ListNode { + dummy := &ListNode{} + curr := dummy + carry := 0 // 进位 + + for l1 != nil || l2 != nil || carry > 0 { + sum := carry + if l1 != nil { + sum += l1.Val + l1 = l1.Next + } + if l2 != nil { + sum += l2.Val + l2 = l2.Next + } + carry = sum / 10 + curr.Next = &ListNode{Val: sum % 10} + curr = curr.Next + } + + return dummy.Next +} + +func main() { + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 读取第一个链表 + sc.Scan() + var n1 int + fmt.Sscan(sc.Text(), &n1) + dummy1 := &ListNode{} + curr1 := dummy1 + for i := 0; i < n1; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr1.Next = &ListNode{Val: val} + curr1 = curr1.Next + } + + // 读取第二个链表 + sc.Scan() + var n2 int + fmt.Sscan(sc.Text(), &n2) + dummy2 := &ListNode{} + curr2 := dummy2 + for i := 0; i < n2; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr2.Next = &ListNode{Val: val} + curr2 = curr2.Next + } + + // 相加并输出 + head := addTwoNumbers(dummy1.Next, dummy2.Next) + for head != nil { + if head.Next != nil { + fmt.Printf("%d ", head.Val) + } else { + fmt.Printf("%d", head.Val) + } + head = head.Next + } + fmt.Println() +} +``` + +--- + +## 第 10 题:K 个一组翻转链表 + +!!! note "题目** + 给你链表的头节点 `head`,每 `k` 个节点一组进行翻转,请你返回修改后的链表。(LeetCode 25) + +### 思路与操作 + +**分组翻转**:每 k 个节点为一组进行翻转,不足 k 个的保持原样。 + +- 用一个指针 `tail` 找到当前组的第 k 个节点 +- 如果不足 k 个,直接返回 +- 翻转当前组的 k 个节点 +- 递归处理剩余部分 +- 连接翻转后的部分和递归处理后的部分 + +```mermaid +graph LR + subgraph 输入 + A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"] + end + subgraph 输出(k=2) + A2["2"] --> B2["1"] --> C2["4"] --> D2["3"] --> E2["5"] + end +``` + +### ACM 题解(Go) + +```go +package main + +import ( + "bufio" + "fmt" + "os" +) + +// ListNode 单链表节点 +type ListNode struct { + Val int + Next *ListNode +} + +// K个一组翻转链表 +func reverseKGroup(head *ListNode, k int) *ListNode { + if head == nil || k == 1 { + return head + } + + // 1. 检查是否有 k 个节点 + tail := head + for i := 0; i < k; i++ { + if tail == nil { + return head // 不足 k 个,保持原样 + } + tail = tail.Next + } + + // 2. 翻转前 k 个节点 + var prev *ListNode + curr := head + for i := 0; i < k; i++ { + next := curr.Next + curr.Next = prev + prev = curr + curr = next + } + + // 3. 递归处理剩余部分,并连接 + head.Next = reverseKGroup(curr, k) + + return prev // prev 是翻转后的头节点 +} + +func main() { + var n, k int + fmt.Scan(&n, &k) + sc := bufio.NewScanner(os.Stdin) + sc.Split(bufio.ScanWords) + + // 构建链表 + dummy := &ListNode{} + curr := dummy + for i := 0; i < n; i++ { + sc.Scan() + var val int + fmt.Sscan(sc.Text(), &val) + curr.Next = &ListNode{Val: val} + curr = curr.Next + } + + // K个一组翻转并输出 + head := reverseKGroup(dummy.Next, k) + for head != nil { + if head.Next != nil { + fmt.Printf("%d ", head.Val) + } else { + fmt.Printf("%d", head.Val) + } + head = head.Next + } + fmt.Println() +} +``` + +--- + +## ⚠️ 常见陷阱 + +!!! warning "忘记处理空链表或单节点链表" + 很多链表题目在头节点为空或只有一个节点时需要特殊处理。写代码前先考虑边界情况。 + +!!! warning "断链前忘记保存 Next 指针" + 在修改 `curr.Next` 之前,必须用临时变量保存原来的 `Next`,否则链表一断就找不回来了。这是链表操作最常见的 bug。 + +!!! warning "虚拟头节点(dummy)使用不当" + 使用 dummy 节点时,最后要返回 `dummy.Next` 而不是 `head`。因为头节点可能已经改变。 + +!!! warning "快慢指针初始化错误" + - 检测环时:`slow` 和 `fast` 都从 `head` 开始,但循环条件是 `fast != nil && fast.Next != nil` + - 找中间节点时:如果要找第二个中间节点(偶数个节点时),`fast` 从 `head` 开始;如果要找第一个,`fast` 从 `head.Next` 开始 + +--- + +## 🏋️ 练习题 + +??? question "练习 1:LeetCode 24 — 两两交换链表节点" + 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。 + + ??? success "答案" + 用递归或迭代。迭代:`dummy -> 1 -> 2 -> 3 -> 4`,交换 1 和 2 时,让 `dummy.Next = 2`,`2.Next = 1`,`1.Next = 递归处理 3->4`。关键是画图理清指针指向。 + +??? question "练习 2:LeetCode 148 — 排序链表" + 给你链表的头结点 `head`,请将其按升序排列并返回排序后的链表。要求时间复杂度 O(n log n),空间复杂度 O(1)。 + + ??? success "答案" + 归并排序:找中间节点,递归排序左右两半,合并两个有序链表。时间 O(n log n),空间 O(log n)(递归栈)。如果要求 O(1) 空间,需要用自底向上的迭代归并。 + +??? question "练习 3:LeetCode 23 — 合并 K 个升序链表" + 给你一个链表数组,每个链表都已按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。 + + ??? success "答案" + 方法一:分治法,两两合并,时间 O(N log k),N 是所有节点总数。方法二:最小堆,每次取 k 个链表头节点中最小的,时间 O(N log k)。面试推荐分治法,代码简洁。 + +--- + +## 🔗 相关链接 + +- [LeetCode 206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) — 链表入门必会 +- [LeetCode 141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) — 快慢指针经典 +- [LeetCode 21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) — 双指针合并 +- [LeetCode 19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) — 快慢指针应用 +- [LeetCode 876. 链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/) — 快慢指针应用 +- [LeetCode 160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) — 双指针技巧 +- [LeetCode 234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) — 综合应用 +- [LeetCode 142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) — 数学推导 +- [LeetCode 2. 两数相加](https://leetcode.cn/problems/add-two-numbers/) — 模拟加法 +- [LeetCode 25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) — 高级操作 diff --git a/mkdocs.yml b/mkdocs.yml index 3b5a1b8..373f63f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -136,6 +136,7 @@ nav: - 二叉树的遍历: algorithm/binary-tree-traversal.md - 队列: algorithm/queue.md - 十大动态规划题: algorithm/dynamic-programming-top10.md + - 十大经典链表题: algorithm/linked-list-top10.md - JVM: - jvm/index.md - 垃圾回收基础: jvm/garbage-collection.md