十大经典链表题(Go · ACM 模式)¶
💡 一句话概述
精选 10 道由易到难的 LeetCode 经典链表题,每道题重点讲解**核心思路、指针操作和边界处理**,并给出完整的 Go ACM 模式(可编译运行的含 main 输入输出)题解。
🔑 核心概念¶
- 虚拟头节点(dummy node) — 链表操作的第一法宝:在头节点前加一个哨兵节点,统一处理头节点被删除/替换的情况,避免大量
if判断 - 快慢指针 — 链表操作的第二法宝:两个指针以不同速度遍历,常用于找中间节点、检测环、找倒数第 k 个节点
- 原地修改 vs 新建链表 — 明确题目要求:是修改原链表的
Next指针,还是创建新节点组成新链表 - 指针断链前先保存 — 修改
Next指针前,务必用临时变量保存原来的Next,否则链表一断就找不回来了 - 递归 vs 迭代 — 链表天然具有递归结构(头 + 剩余链表),很多问题递归写法更简洁,但要注意栈深度
📝 详细说明¶
链表节点定义¶
本文所有题目使用以下链表节点定义:
链表操作通用技巧¶
| 技巧 | 适用场景 | 说明 |
|---|---|---|
| dummy node | 头节点可能变化 | 返回 dummy.Next 即可 |
| 快慢指针 | 中间节点、倒数第 k 个 | 快指针先走 k 步,或快指针速度是慢指针两倍 |
| 三指针(prev, curr, next) | 反转、插入、删除 | 保存三个状态,防止断链 |
| 哈希表辅助 | 检测环、相交 | 空间换时间 |
| 递归 | 反转、合并、回文 | 利用链表递归结构 |
第 1 题:反转链表¶
题目
给你单链表的头节点 head,请你反转链表,并返回反转后的链表头。(LeetCode 206)
思路与操作¶
这是链表最基础的操作,核心是**三指针迭代**:prev(已反转部分的头)、curr(当前节点)、next(暂存下一个节点)。
- 初始:
prev = nil,curr = head - 每次循环:保存
next = curr.Next,将curr.Next指向prev,然后prev = curr,curr = next - 结束条件:
curr == nil,此时prev就是新的头节点
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)¶
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()
}
递归解法
递归解法更简洁,但要注意递归深度。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 题:检测环形链表¶
题目
给你一个链表的头节点 head,判断链表中是否有环。如果链表中存在环,则返回 true;否则返回 false。(LeetCode 141)
思路与操作¶
快慢指针法:快指针每次走 2 步,慢指针每次走 1 步。如果链表有环,快慢指针一定会在环内相遇(就像在操场跑步,跑得快的人一定会追上跑得慢的人)。
- 慢指针
slow:每次走 1 步 - 快指针
fast:每次走 2 步 - 如果
fast或fast.Next变成nil,说明无环 - 如果
fast == slow,说明有环
graph LR
subgraph 有环链表
A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
E --> C
end
ACM 题解(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 题:合并两个有序链表¶
题目
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(LeetCode 21)
思路与操作¶
双指针比较法:用两个指针分别遍历两个链表,每次选择较小的节点接到结果链表后面。
- 创建
dummy节点作为结果链表的头 - 比较
l1.Val和l2.Val,将较小的节点接到curr后面 - 当一个链表遍历完,直接将另一个链表剩余部分接到
curr后面
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)¶
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 个节点¶
题目
给你一个链表,删除链表的倒数第 n 个节点,并且返回链表的头节点。(LeetCode 19)
思路与操作¶
快慢指针法:让快指针先走 n 步,然后快慢指针一起走,当快指针到达末尾时,慢指针正好指向倒数第 n 个节点的前一个节点。
- 快指针
fast先走n步 - 快慢指针一起走,直到
fast.Next == nil - 此时
slow.Next就是倒数第n个节点,执行删除操作
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)¶
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 题:链表的中间节点¶
题目
给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。(LeetCode 876)
思路与操作¶
快慢指针法:快指针每次走 2 步,慢指针每次走 1 步。当快指针到达末尾时,慢指针正好在中间。
- 快指针
fast:每次走 2 步 - 慢指针
slow:每次走 1 步 - 当
fast == nil || fast.Next == nil时,slow就是中间节点
graph LR
subgraph 奇数个节点
A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
end
subgraph 中间节点
C2["3"]
end
ACM 题解(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 题:相交链表¶
题目
给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点。如果不存在相交节点,返回 null。(LeetCode 160)
思路与操作¶
双指针法:让两个指针分别从两个链表头开始走,当走到末尾时切换到另一个链表的头。这样两个指针走过的总路程相等,如果有交点,一定会在交点相遇。
- 指针
pA从headA开始,指针pB从headB开始 - 当
pA走到末尾,切换到headB;当pB走到末尾,切换到headA - 如果有交点,
pA和pB会在交点相遇;如果无交点,最终都会变成nil
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)¶
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 题:回文链表¶
题目
给你一个单链表的头节点 head,请你判断该链表是否为回文链表。如果是,返回 true;否则,返回 false。(LeetCode 234)
思路与操作¶
快慢指针 + 反转后半部分:找到中间节点,反转后半部分链表,然后比较前半部分和后半部分。
- 快慢指针找中间节点
- 反转后半部分链表
- 比较前半部分和反转后的后半部分
- 恢复链表(可选)
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)¶
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 步,两者会在环入口相遇。
graph TD
A["头节点"] --> B["环入口"]
B --> C["相遇点"]
C --> B
ACM 题解(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 代替
- 最后如果还有进位,需要新增一个节点
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)¶
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 个节点
- 递归处理剩余部分
- 连接翻转后的部分和递归处理后的部分
graph LR
subgraph Input
A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
end
subgraph "Output k=2"
A2["2"] --> B2["1"] --> C2["4"] --> D2["3"] --> E2["5"]
end
ACM 题解(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()
}
⚠️ 常见陷阱¶
忘记处理空链表或单节点链表
很多链表题目在头节点为空或只有一个节点时需要特殊处理。写代码前先考虑边界情况。
断链前忘记保存 Next 指针
在修改 curr.Next 之前,必须用临时变量保存原来的 Next,否则链表一断就找不回来了。这是链表操作最常见的 bug。
虚拟头节点(dummy)使用不当
使用 dummy 节点时,最后要返回 dummy.Next 而不是 head。因为头节点可能已经改变。
快慢指针初始化错误
- 检测环时:
slow和fast都从head开始,但循环条件是fast != nil && fast.Next != nil - 找中间节点时:如果要找第二个中间节点(偶数个节点时),
fast从head开始;如果要找第一个,fast从head.Next开始
🏋️ 练习题¶
练习 1:LeetCode 24 — 两两交换链表节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。
答案
用递归或迭代。迭代:dummy -> 1 -> 2 -> 3 -> 4,交换 1 和 2 时,让 dummy.Next = 2,2.Next = 1,1.Next = 递归处理 3->4。关键是画图理清指针指向。
练习 2:LeetCode 148 — 排序链表
给你链表的头结点 head,请将其按升序排列并返回排序后的链表。要求时间复杂度 O(n log n),空间复杂度 O(1)。
答案
归并排序:找中间节点,递归排序左右两半,合并两个有序链表。时间 O(n log n),空间 O(log n)(递归栈)。如果要求 O(1) 空间,需要用自底向上的迭代归并。
练习 3:LeetCode 23 — 合并 K 个升序链表
给你一个链表数组,每个链表都已按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
答案
方法一:分治法,两两合并,时间 O(N log k),N 是所有节点总数。方法二:最小堆,每次取 k 个链表头节点中最小的,时间 O(N log k)。面试推荐分治法,代码简洁。
🔗 相关链接¶
- LeetCode 206. 反转链表 — 链表入门必会
- LeetCode 141. 环形链表 — 快慢指针经典
- LeetCode 21. 合并两个有序链表 — 双指针合并
- LeetCode 19. 删除链表的倒数第 N 个结点 — 快慢指针应用
- LeetCode 876. 链表的中间结点 — 快慢指针应用
- LeetCode 160. 相交链表 — 双指针技巧
- LeetCode 234. 回文链表 — 综合应用
- LeetCode 142. 环形链表 II — 数学推导
- LeetCode 2. 两数相加 — 模拟加法
- LeetCode 25. K 个一组翻转链表 — 高级操作