LeetCode-Go 第 27 题 Remove Element:原地删除数组元素的交换双指针解法与全量测试验证
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
本篇基于 LeetCode-Go 仓库中 leetcode/0027.Remove-Element/README.md 的题解文档展开,完整讲解"删除数组中所有等于给定值 val 的元素(原地、O(1) 额外空间)"这一经典题目的题目语义、交换式双指针解法的 Go 源码实现,以及配套测试用例如何覆盖空数组、全匹配等边界场景。读完本篇,你能掌握原地删除类问题的通用指针框架,并能在本仓库中运行该题的测试与覆盖率验证。
题目与约束
给定一个数组nums和一个数值val,将数组中所有等于val的元素删除,并返回剩余的元素个数。题目有两点硬约束(引自 题目文档):
- 原地修改:不能为另一个数组分配额外空间,必须通过修改输入数组来完成,额外内存为 O(1);
- 元素顺序可以改变:返回长度之后的数组内容是什么都无所谓,评判时只读取前
len个元素。
原始文档给出的两个示例需要完整保留,它们是理解评判机制的关键:
示例 1:
Given nums = [3,2,2,3], val = 3, Your function should return length = 2, with the first two elements of nums being 2. It doesn't matter what you leave beyond the returned length.示例 2:
Given nums = [0,1,2,2,3,0,4,2], val = 2, Your function should return length = 5, with the first five elements of nums containing 0, 1, 3, 0, and 4. Note that the order of those five elements can be arbitrary. It doesn't matter what values are set beyond the returned length.返回值为什么是整数而不是数组?
原始文档的 Clarification 部分解释了一个常见困惑:函数返回的是长度(整数),但答案却是一个数组。原因是输入数组按引用(pass by reference)传入,函数内对数组的任何修改调用方都能感知;调用方借助函数返回的长度,打印前len个元素即可:
// nums is passed in by reference. (i.e., without making a copy) int len = removeElement(nums, val); // any modification to nums in your function would be known by the caller. // using the length returned by your function, it prints the first len elements. for (int i = 0; i < len; i++) { print(nums[i]); }因此本题的本质不是"真的删除元素",而是文档原话所概括的:把属于删除对象的元素挪到返回长度之后的空间,然后返回实际剩余的元素个数,OJ 读取剩余个数的元素进行输出。这一点在 Go 中同样成立——Go 的切片头包含指针,函数内通过切片下标写nums[i]会直接修改底层数组,调用方可见。
解题思路:与第 283 题同构
README.md 的解题思路部分指出:这道题和第 283 题(Move Zeroes)基本一致——283 题是删除 0,这一题是删除给定的val,实质相同。本仓库中 leetcode/0283.Move-Zeroes/283. Move Zeroes.go 的实现印证了这一点,它与 27 题解法逐行同构,只是判定条件固定为nums[i] != 0:
func moveZeroes(nums []int) { if len(nums) == 0 { return } j := 0 for i := 0; i < len(nums); i++ { if nums[i] != 0 { if i != j { nums[i], nums[j] = nums[j], nums[i] } j++ } } }所以 27 题的通用框架可以归纳为:把"0"这个魔法数字参数化为val,并额外返回指针j的位置作为新长度。
Go 源码实现逐行解析
本仓库的解法位于 leetcode/0027.Remove-Element/27. Remove Element.go,全文如下:
package leetcode func removeElement(nums []int, val int) int { if len(nums) == 0 { return 0 } j := 0 for i := 0; i < len(nums); i++ { if nums[i] != val { if i != j { nums[i], nums[j] = nums[j], nums[i] } j++ } } return j }实现采用交换式双指针,各部分职责如下:
- 空数组短路(L4-L6):
len(nums) == 0时直接返回 0。虽然对空切片循环体一次都不会执行、结果也正确,但显式短路使边界意图清晰,也对应测试中的空数组用例。 - 双指针分工(L7):
j标记"下一块非 val 元素应写入的位置"(即当前已保留元素区间的右边界),i负责扫描整个数组。j永远不大于i,因此只需一趟扫描。 - 交换归位(L9-L14):当
nums[i] != val时,说明该元素要保留。若i != j,则将nums[i]与nums[j]交换——这一步把"要保留的元素"前移到j位置,同时把"要删除的元素"(原来的nums[j])后移到了扫描指针i之后,即文档所说的"将删除的元素移动到数组后面的空间内"。随后j++收缩保留区间。 - 返回新长度(L16):循环结束时
j恰好等于非 val 元素的总个数,返回j即新数组长度。
用示例 1 走一遍:nums = [3,2,2,3], val = 3。
| 步骤 | i | j | 判断 | 动作 | nums |
|---|---|---|---|---|---|
| 初态 | 0 | 0 | - | - | [3,2,2,3] |
| 1 | 0 | 0 | nums[0]==3 | 跳过 | [3,2,2,3] |
| 2 | 1 | 0 | nums[1]==2 | 交换 1、0,j=1 | [2,3,2,3] |
| 3 | 2 | 1 | nums[2]==2 | 交换 2、1,j=2 | [2,2,3,3] |
| 4 | 3 | 2 | nums[3]==3 | 跳过 | [2,2,3,3] |
最终返回j = 2,前两个元素为[2, 2],与题目示例一致(示例 2 返回 5,前五个元素含 0, 1, 3, 0, 4 的任意顺序,交换式写法恰好给出[0,1,3,0,4]的某种归位排列)。
if i != j这个判断不是正确性必需的(交换相同下标是幂等操作),而是避免"自己和自己交换"的无意义写操作,属于实现上的小优化。
复杂度
- 时间 O(n):单趟扫描,每个元素至多参与一次交换;
- 空间 O(1):除
i、j两个指针外没有额外数据结构,满足题目 O(1) 额外内存的约束。
测试用例与边界覆盖
配套测试文件为 leetcode/0027.Remove-Element/27. Remove Element_test.go,采用本仓库统一的"参数结构体 + 答案结构体 + 用例切片"风格:para27{one []int, two int}承载输入(数组与 val),ans27{one int}承载期望长度,Test_Problem27遍历用例打印输入与实际输出。共 7 个用例,覆盖的边界与典型场景如下:
| 输入 nums | val | 期望长度 | 考察点 |
|---|---|---|---|
[1, 0, 1] | 1 | 1 | 首尾均为目标值 |
[0, 1, 0, 3, 0, 12] | 0 | 3 | 目标值散布全数组 |
[0, 1, 0, 3, 0, 0, 0, 0, 1, 12] | 0 | 4 | 长数组中目标值成块出现 |
[0, 0, 0, 0, 0] | 0 | 0 | 全部元素都是目标值 |
[1] | 1 | 0 | 单元素数组且就是目标值 |
[0, 1, 2, 2, 3, 0, 4, 2] | 2 | 5 | 题目官方示例 2 |
[] | 0 | 0 | 空数组 |
可以看到测试有意压满了原地删除类问题最容易出错的场景:空数组、单元素、全匹配(返回 0)以及目标值密集分布(大量交换发生)。
在本仓库中运行验证
仓库根目录的 go.mod 声明模块为github.com/halfrost/LeetCode-Go,Go 版本 1.19。题解与测试同属leetcode包,可直接运行该目录的测试:
go test ./leetcode/0027.Remove-Element/ -run Test_Problem27 -v测试会打印如下格式的输入输出对照(见测试文件中的fmt.Printf):
------------------------Leetcode Problem 27------------------------ 【input】:[1 0 1] 【output】:1 【input】:[0 1 0 3 0 12] 【output】:3 ...仓库还提供了一键生成覆盖率文件的脚本 gotest.sh,其核心命令对全部题解包一次性生成合法的coverage.txt(脚本注释说明了旧版"分包 cat 追加"写法会被新版 Codecov 解析器判为 0% 的原因):
go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...小结
- 27 题与 283 题同构:交换式双指针一趟扫描,把保留元素前移、目标值后移,返回保留区长度;
- "原地删除"的评判语义是调用方只读取返回长度之前的元素,返回长度之后的内容不做要求,Go 切片共享底层数组的特性使这一语义天然成立;
- 实现为 O(n) 时间、O(1) 空间,
i != j判断仅用于省去自交换; - 本仓库配套 7 个测试用例覆盖空数组、全匹配、单元素等边界,可通过
go test与 gotest.sh 直接复现验证。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考