LeetCode-Go 题解:206. Reverse Linked List 反转单链表的迭代实现与源码剖析
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
本篇文章围绕 LeetCode 第 206 题 "Reverse Linked List"(反转单链表),以 LeetCode-Go 仓库中该题目的官方题解文档与 Go 源码为主线,讲解其迭代式反转算法的核心思路、逐行实现细节、时间复杂度分析,并结合仓库内的测试用例与structures工具包,展示如何在本地验证算法正确性。读完本文,你将掌握单链表反转这一高频基础操作的 Go 实现范式,以及 LeetCode-Go 仓库的代码组织与测试风格。
题目背景:Reverse Linked List
LeetCode 206. Reverse Linked List 是链表类问题中最基础、最高频的一道题,其题目描述非常简洁:
Reverse a singly linked list.(反转一个单链表。)
原题文档位于仓库的 website/content.en/ChapterFour/0200~0299/0206.Reverse-Linked-List.md,与之对应的还有中文说明文档 leetcode/0206.Reverse-Linked-List/README.md,其中用一句话概括了题意:翻转单链表。
例如输入链表1 -> 2 -> 3 -> 4 -> 5 -> NULL,要求输出5 -> 4 -> 3 -> 2 -> 1 -> NULL。这道题虽然简单,却是后续大量复杂链表题目(如两两交换、k 个一组翻转、回文链表判断等)的基础,因此掌握其两种主流实现(迭代与递归)非常重要。
解题思路:按题意直接迭代翻转
原文档给出的解题思路只有一句话:"Just follow the problem statement."(按照题意做即可)。这句话背后对应的,正是链表反转最经典、最直接的迭代三指针法:
- 使用三个指针完成就地翻转:
behind(前驱,初始为nil)、head(当前节点,初始为链表头)、以及每轮循环中暂存的next(当前节点的下一个节点)。 - 每一轮迭代只做三件事:先保存
head.Next到next,再把head.Next指向前驱behind(完成当前节点的"掉头"),最后让behind和head各自前进一位。 - 当
head走到链表末尾的nil时,behind恰好停在新链表的头节点上,直接返回behind即可。
整个过程不申请任何额外空间,仅靠修改指针指向完成反转,属于 O(1) 空间复杂度的就地算法。
源码实现:LeetCode-Go 中的 reverseList
仓库中的实际实现位于 leetcode/0206.Reverse-Linked-List/206. Reverse Linked List.go,与题解文档中的代码完全一致:
package leetcode import ( "github.com/halfrost/LeetCode-Go/structures" ) // ListNode define type ListNode = structures.ListNode /** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func reverseList(head *ListNode) *ListNode { var behind *ListNode for head != nil { next := head.Next head.Next = behind behind = head head = next } return behind }逐行拆解核心循环
var behind *ListNode:声明前驱指针,零值初始化为nil,这正好作为反转后链表的尾部终止符。for head != nil:遍历直到原链表末尾。next := head.Next:先保存后继。因为下一步会覆盖head.Next,若不先暂存,就会丢失尚未处理的链表剩余部分。head.Next = behind:把当前节点的指针掉转方向,指向前驱。behind = head:前驱指针前进到当前节点。head = next:当前指针前进到原先的后继节点,继续下一轮。return behind:循环结束时behind指向原链表的尾节点,也就是新链表的头节点。
以输入1 -> 2 -> 3为例,模拟过程如下:
| 轮次 | head | 操作后 head.Next | behind | 说明 |
|---|---|---|---|---|
| 初始 | 1 | — | nil | — |
| 第 1 轮 | 1 | nil(原为 2) | 1 | 1 变成新链表尾 |
| 第 2 轮 | 2 | 1(原为 3) | 2 | 2 指向 1 |
| 第 3 轮 | 3 | 2(原为 nil) | 3 | 3 指向 2 |
| 结束 | nil | — | 3 | 返回 3,即3 -> 2 -> 1 |
关于 ListNode 类型别名的说明
值得注意的是,仓库在实现文件中对链表节点做了类型别名处理:
type ListNode = structures.ListNode真正的节点定义统一放在独立工具包 structures/ListNode.go 中:
// ListNode 是链接节点 type ListNode struct { Val int Next *ListNode }从源码结构看,这是 LeetCode-Go 仓库的一种工程化设计:所有链表类题目复用同一个ListNode定义,避免在每题目录里重复声明结构体;题解文档中的代码则保留了一份完整的ListNode定义,便于单独阅读时自洽。这种"文档中自包含、源码中统一复用"的组织方式,正是仓库代码整洁度的体现。
复杂度分析
- 时间复杂度:O(n)。每个节点恰好被访问一次,循环次数等于链表长度 n。
- 空间复杂度:O(1)。仅使用
behind、next两个指针变量,无额外数据结构开销。
该实现满足题目"原地反转"的要求,是链表反转的空间最优解。
测试验证:仓库如何保证 100% 覆盖率
LeetCode-Go 仓库的口号是 "100% test coverage",206 题的测试用例位于 leetcode/0206.Reverse-Linked-List/206. Reverse Linked List_test.go:
package leetcode import ( "fmt" "testing" "github.com/halfrost/LeetCode-Go/structures" ) type question206 struct { para206 ans206 } // para 是参数 // one 代表第一个参数 type para206 struct { one []int } // ans 是答案 // one 代表第一个答案 type ans206 struct { one []int } func Test_Problem206(t *testing.T) { qs := []question206{ { para206{[]int{1, 2, 3, 4, 5}}, ans206{[]int{5, 4, 3, 2, 1}}, }, } fmt.Printf("------------------------Leetcode Problem 206------------------------\n") for _, q := range qs { _, p := q.ans206, q.para206 fmt.Printf("【input】:%v 【output】:%v\n", p, structures.List2Ints(reverseList(structures.Ints2List(p.one)))) } }该测试用例的设计体现了仓库的通用测试套路:
- 用数组描述输入输出:
para206和ans206分别用[]int表示输入链表与期望的反转结果,可读性好且便于扩展更多用例。 - 借助 structures 工具做类型转换:
structures.Ints2List(p.one)把[]int构造成链表,structures.List2Ints(...)把结果链表再转回[]int用于对比输出。这两个工具的实现在 structures/ListNode.go:
// List2Ints convert List to []int func List2Ints(head *ListNode) []int { // 链条深度限制,链条深度超出此限制,会 panic limit := 100 times := 0 res := []int{} for head != nil { times++ if times > limit { msg := fmt.Sprintf("链条深度超过%d,可能出现环状链条。请检查错误,或者放宽 l2s 函数中 limit 的限制。", limit) panic(msg) } res = append(res, head.Val) head = head.Next } return res } // Ints2List convert []int to List func Ints2List(nums []int) *ListNode { if len(nums) == 0 { return nil } l := &ListNode{} t := l for _, v := range nums { t.Next = &ListNode{Val: v} t = t.Next } return l.Next }其中List2Ints内置了 100 层深度的防环保护:一旦遍历次数超过 100 次即 panic,提示"链条深度超过 100,可能出现环状链条"。这一设计既防止了环形链表导致死循环,也间接验证了reverseList不会产生环。
本地运行测试
仓库根目录的 gotest.sh 提供了全量覆盖率测试脚本:
go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...若只单独验证本题,可在仓库根目录执行:
go test -v -run Test_Problem206 ./leetcode/0206.Reverse-Linked-List/运行后可以看到形如【input】:[1 2 3 4 5] 【output】:[5 4 3 2 1]的调试输出,且通过go test的 PASS 判定。需要注意的是,运行测试依赖 go.mod 中声明的本地模块替换(replace github.com/halfrost/LeetCode-Go/structures => ./structures),因此请在仓库根目录下执行命令,以保证structures包能正确解析。
拓展思考:递归解法与边界情况
递归版本
除了迭代,链表反转还有经典的递归实现,与迭代法对比能加深理解:
func reverseListRecursive(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } newHead := reverseListRecursive(head.Next) head.Next.Next = head head.Next = nil return newHead }递归思路是:先反转以head.Next为头的子链表,拿到新的头newHead;然后让原后继节点的Next指回当前节点(head.Next.Next = head),再断开head.Next并返回newHead。递归版代码更简洁,但栈深度为 O(n),在链表极长时存在栈溢出风险,这也是 LeetCode-Go 仓库选择迭代实现作为标准答案的合理解释。
边界情况
- 空链表(head 为 nil):循环体一次都不执行,直接返回
behind(即 nil),正确。 - 单节点链表:只执行一轮循环,
behind指向该节点本身,正确。 - 两个节点:第二轮循环后顺序互换,正确。
从代码逻辑看,迭代实现天然覆盖上述所有边界,无需额外特判,这也是其简洁性的体现。
总结
LeetCode 206 "Reverse Linked List" 是链表题目的入门基石。通过 LeetCode-Go 仓库的题解文档、源码与测试用例,本文完整梳理了迭代反转的"三指针"实现:保存后继、掉转指针、双指针前进,最终以 O(n) 时间、O(1) 空间的代价完成原地反转。仓库通过 structures/ListNode.go 统一封装节点定义与数组/链表互转工具,配合 206. Reverse Linked List_test.go 的用例驱动方式,为读者提供了一套可直接运行、可复用的标准范式。掌握本题后,再面对链表相关的进阶题目时,你就有了最坚实的底子。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考