LeetCode-Go 题解:203. Remove Linked List Elements 删除链表中指定值的结点
导读
本文围绕 LeetCode 第 203 题《Remove Linked List Elements》展开,讲解如何用 Go 语言在一个单链表中删除所有值为 val 的结点。文章以仓库 leetcode/0203.Remove-Linked-List-Elements/README.md 为主体,结合仓库内真实源码 203. Remove Linked List Elements.go 与测试用例 203. Remove Linked List Elements_test.go 进行纵深讲解。读完本文,你将掌握单链表删除结点的两种核心思路(哨兵结点法 / 头结点特判法)、Go 语言中的指针操作细节,以及本仓库统一的链表工具函数与测试运行方式。
题目
原文描述如下:
Remove all elements from a linked list of integers that have value val.
即:给定一个整数链表和一个目标值 val,删除链表中所有值为 val 的结点,并返回处理后的链表头。
题目给出的示例:
Input: 1->2->6->3->4->5->6, val = 6
Output: 1->2->3->4->5
题目大意
删除链表中所有指定值的结点。注意是"所有"而非"第一个",且题目没有限定 val 在链表中出现的位置,因此头结点、中间结点、尾结点以及连续重复结点都需要被正确处理。
解题思路
官方题解的思路只有一句话:"按照题意做即可。"但这道题真正的考察点在于单链表删除操作对前驱指针的依赖:
- 单链表结点只有
Next指针,删除当前结点cur的唯一方式是让它的前驱pre.Next直接跳过它,指向cur.Next; - 头结点没有前驱,如果头结点的值恰好等于
val,直接删除会丢失链表入口,因此必须特殊处理。
解决这一矛盾有两条经典路径:
- 哨兵结点(dummy head)法:人为构造一个虚拟头结点,让真正的头结点也有统一的前驱,从而把"删头"降级为"删普通结点";
- 头结点特判法:先循环删除头部连续等于
val的结点,再对剩余链表做统一删除。
本仓库采用第一种方案,代码更简洁、无需重复分支。
Go 源码实现详解
仓库的实现位于 203. Remove Linked List Elements.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 removeElements(head *ListNode, val int) *ListNode {
if head == nil {
return head
}
newHead := &ListNode{Val: 0, Next: head}
pre := newHead
cur := head
for cur != nil {
if cur.Val == val {
pre.Next = cur.Next
} else {
pre = cur
}
cur = cur.Next
}
return newHead.Next
}
关键点一:类型别名复用公共结构
文件开头通过 type ListNode = structures.ListNode 将仓库公共模块 structures/ListNode.go 中定义的单链表结点类型直接别名到 leetcode 包内使用:
type ListNode struct {
Val int
Next *ListNode
}
这种写法让所有链表题共用同一份结点定义,避免每个题解重复声明,也使测试辅助函数(如 Ints2List、List2Ints)可以在全部题目中复用。
关键点二:哨兵结点统一删除逻辑
newHead := &ListNode{Val: 0, Next: head}
pre := newHead
cur := head
newHead 是值为 0 的虚拟头结点,它的 Next 指向真正的头结点。此后 pre 永远指向"当前遍历到的最后一个未被删除的结点",也就是 cur 的合法前驱,删除逻辑对头结点与普通结点完全一致:
- 若
cur.Val == val:执行pre.Next = cur.Next,跳过当前结点,pre不动(因为被删结点不能成为新前驱); - 否则:
pre = cur,前驱正常后移。
最后 return newHead.Next 直接返回新链表的头。这里也顺带处理了两种极端情况:链表为空(head == nil 提前返回)以及整条链表全部等于 val(此时 newHead.Next 为 nil,返回空链表)。
关键点三:一次遍历,O(1) 额外空间
整段代码只做一次 for 循环遍历,没有任何额外容器,时间复杂度 O(n)、空间复杂度 O(1),与 LeetCode 官方最优解一致。
边界情况梳理
结合仓库测试用例 203. Remove Linked List Elements_test.go 中的 8 组输入,实现需要覆盖的边界包括:
| 场景 | 输入 | 期望输出 |
|---|---|---|
| 删除头结点 | [1,2,3,4,5], val=1 |
[2,3,4,5] |
| 删除中间结点 | [1,2,3,4,5], val=2 |
[1,3,4,5] |
| 全链表同值 | [1,1,1,1,1], val=1 |
[] |
| 值连续重复出现 | [1,2,3,2,3,2,3,2], val=2 |
[1,3,3,3] |
| 删除尾结点 | [1,2,3,4,5], val=5 |
[1,2,3,4] |
| 空链表 | [], val=5 |
[] |
| 目标值不存在 | [1,2,3,4,5], val=10 |
[1,2,3,4,5] |
| 单结点被删 | [1], val=1 |
[] |
哨兵结点方案天然覆盖以上全部场景:全链表同值不会造成死循环(cur 始终后移,pre 停在哨兵处);连续重复值场景中,前一个被删结点不会成为 pre,因此不会"跳过"后续应删结点。
仓库测试与工具函数佐证
测试用例的组织方式
测试文件 203. Remove Linked List Elements_test.go 沿用了本仓库统一的"para/ans 表格驱动测试"风格:question203 把输入 para203{one []int, n int} 与期望 ans203{one []int} 成对组织,Test_Problem203 中循环遍历 8 组用例,并通过公共工具函数完成数组与链表的互转:
structures.List2Ints(removeElements(structures.Ints2List(p.one), p.n))
这里 Ints2List 把 []int 数组转换为链表输入,List2Ints 把删除后的链表还原为数组以便与期望值断言,两个函数的实现都位于 structures/ListNode.go。
测试工具函数的实现细节
从 structures/ListNode.go 可以看到:
Ints2List(nums []int) *ListNode:顺序构造链表,空数组返回nil;List2Ints(head *ListNode) []int:顺序遍历还原数组,并内置limit := 100的链条深度保护——若链表深度超过 100 会直接panic提示"链条深度超过 100,可能出现环状链条",避免测试中误入环而死循环。
如何运行本题测试
本仓库为 Go module 结构(见根目录 go.mod),且 gotest.sh 中给出了统一的测试命令:
go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...
针对本题,可以在仓库根目录执行:
go test -v -run Test_Problem203 ./leetcode/0203.Remove-Linked-List-Elements/
输出会包含每组用例的 【input】 与 【output】 打印(来自测试中 fmt.Printf 的调试信息),配合表格断言即可验证实现正确性。仓库的 coverage.txt 与持续集成配置也表明该项目要求题解保持 100% 测试覆盖率,因此每个题目目录都配套了完整的 _test.go 文件。
同类题横向参考
链表的删除/反转/合并是 LeetCode 高频考点,本仓库的以下题目与本题共享 ListNode 数据结构与工具函数,可作为延伸练习:
- 203. Remove Linked List Elements:删除所有指定值结点(本题,哨兵结点法);
- 237. Delete Node in a Linked List:已知结点直接删除(无前驱的"值覆盖"技巧),其测试文件同样复用了
removeElements辅助删除过程; - 83. Remove Duplicates from Sorted List 与 82. Remove Duplicates from Sorted List II:有序链表去重,是删除类操作在"有序"约束下的变体。
总结
LeetCode 203 题的核心价值在于:单链表删除必须借助前驱指针,而哨兵结点让头结点不再特殊。本仓库的 Go 实现用不到 20 行代码一次遍历完成删除,配合 structures/ListNode.go 提供的构造/还原工具与表格驱动测试,既保证了正确性,也保持了代码的可读性与可复用性。掌握这一"哨兵结点"模式后,几乎所有涉及链表头删除的题目(如 82、83、19 等)都可以直接套用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00