bigywater.tech
算法与数据结构 · LeetCode 27

LeetCode 27. 移除元素

给定一个数组和一个值 val,原地移除数组中所有等于 val 的元素,返回剩下的元素个数 k;前 k 个位置放剩下的元素,顺序可以任意,后面的内容不重要。这是双指针的入门题:一个指针 i 扫描,一个指针 k 指向下一个要写入的位置。本文除了给出五种语言的实现,还比较了另一种"用最后一个元素补位"的做法,用真实的写入次数展示两种做法在不同数据上各有优劣,并把双指针做成分步演示。

10,000
组随机对拍,与暴力解的结果全部一致
10 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。评测只检查返回的 k 和前 k 个元素(不要求顺序),本文的用例输出是 [k, 前 k 个元素],比较时不区分前 k 个元素的先后。基准解是列表推导式过滤,与原地覆盖是两种独立的写法。一共 10 组用例,覆盖:官方示例、空数组、单个元素、没有元素等于 val、全部等于 val、val 在头或尾:

真实实测 · 本机执行输出
输入 [[3,2,2,3],3]
输出 [2,[2,2]]
输入 [[0,1,2,2,3,0,4,2],2]
输出 [5,[0,1,3,0,4]]
输入 [[],0]
输出 [0,[]]
输入 [[1],1]
输出 [0,[]]
输入 [[1],2]
输出 [1,[1]]
输入 [[2,2,2],2]
输出 [0,[]]

双指针:五种语言的实现

k 指向下一个要写入的位置。从左到右扫描,遇到不等于 val 的元素就写到 nums[k] 并让 k 加一,等于 val 的直接跳过。扫描结束时 nums[0..k) 就是所有保留的元素,并且保留了它们原来的相对顺序。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(nums, val): k = 0 # nums[0:k] 是已经留下的、不等于 val 的元素 for x in nums: if x != val: nums[k] = x k += 1 return [k, nums[:k]]
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[3,2,2,3],3]  →  [2,[2,2]]
[[0,1,2,2,3,0,4,2],2]  →  [5,[0,1,3,0,4]]
[[],0]  →  [0,[]]
[[1],1]  →  [0,[]]
[[1],2]  →  [1,[1]]
[[2,2,2],2]  →  [0,[]]
Java · 解法代码class Solution { public int removeElement(int[] nums, int val) { int k = 0; // nums[0..k) 是已经留下的、不等于 val 的元素 for (int x : nums) { if (x != val) nums[k++] = x; } return k; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[3,2,2,3],3]  →  [2,[2,2]]
[[0,1,2,2,3,0,4,2],2]  →  [5,[0,1,3,0,4]]
[[],0]  →  [0,[]]
[[1],1]  →  [0,[]]
[[1],2]  →  [1,[1]]
[[2,2,2],2]  →  [0,[]]
Go · 解法代码func removeElement(nums []int, val int) int { k := 0 // nums[0:k] 是已经留下的、不等于 val 的元素 for _, x := range nums { if x != val { nums[k] = x k++ } } return k }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[3,2,2,3],3]  →  [2,[2,2]]
[[0,1,2,2,3,0,4,2],2]  →  [5,[0,1,3,0,4]]
[[],0]  →  [0,[]]
[[1],1]  →  [0,[]]
[[1],2]  →  [1,[1]]
[[2,2,2],2]  →  [0,[]]
JavaScript · 解法代码function removeElement(nums, val) { let k = 0; // nums[0..k) 是已经留下的、不等于 val 的元素 for (const x of nums) { if (x !== val) nums[k++] = x; } return k; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[3,2,2,3],3]  →  [2,[2,2]]
[[0,1,2,2,3,0,4,2],2]  →  [5,[0,1,3,0,4]]
[[],0]  →  [0,[]]
[[1],1]  →  [0,[]]
[[1],2]  →  [1,[1]]
[[2,2,2],2]  →  [0,[]]
C++ · 解法代码#include <vector> using namespace std; class Solution { public: int removeElement(vector<int>& nums, int val) { int k = 0; // nums[0..k) 是已经留下的、不等于 val 的元素 for (int x : nums) { if (x != val) nums[k++] = x; } return k; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[3,2,2,3],3]  →  [2,[2,2]]
[[0,1,2,2,3,0,4,2],2]  →  [5,[0,1,3,0,4]]
[[],0]  →  [0,[]]
[[1],1]  →  [0,[]]
[[1],2]  →  [1,[1]]
[[2,2,2],2]  →  [0,[]]
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

另一种做法:从后往前补位

题目允许改变元素顺序,所以还有另一种做法:遇到等于 val 的元素,就用数组最后一个元素覆盖它,然后数组长度减一,并且这个位置要再检查一次(因为换过来的元素也可能等于 val)。这种做法在 val 很少的时候只需要很少的写入。下面是长度 100000 的数组,val 占不同比例时,两种做法真实的写入次数:

真实实测 · 本机执行输出
写入次数对比:长度 100000,val 在数组里占不同比例
val 占  1%: 正向覆盖写入  99000 次,从后往前补位写入   1000 次,保留个数 99000/99000
val 占 10%: 正向覆盖写入  90000 次,从后往前补位写入  10000 次,保留个数 90000/90000
val 占 50%: 正向覆盖写入  50000 次,从后往前补位写入  50000 次,保留个数 50000/50000
val 占 99%: 正向覆盖写入   1000 次,从后往前补位写入  99000 次,保留个数 1000/1000

val 占 1% 时,正向覆盖写了 99000 次,从后往前补位只写了 1000 次;val 占 50% 时两者相同(都是 50000 次);val 占 99% 时反过来:正向覆盖只写 1000 次,从后往前补位写了 99000 次。哪种更省写入取决于 val 在数据中的比例。补位法的代价是会改变保留元素的相对顺序,而正向覆盖保持顺序。这个实验只数了写入次数,没有测量实际耗时。

随机对拍

Python 版本另外和列表推导式做了随机对拍,比较个数和前 k 个元素(不区分顺序):

真实实测 · 本机执行输出
随机对拍 10000 组(长度 0 到 15、取值 -4 到 4 的随机数组,val 取 -4 到 4;比较返回的个数和前 k 个元素(不区分顺序)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。这道题是一次线性扫描,所以没有做规模对比实验。

分步演示:两个指针怎么走

下面的演示用 [0, 1, 2, 2, 3, 0, 4, 2] 和 val = 2。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。淡色的是等于 val、被跳过的位置,绿色是已经保留的部分。

移除元素:[0, 1, 2, 2, 3, 0, 4, 2],val = 2
保留的部分 nums[0:k]正在看的 nums[i]刚写入的位置等于 val,被跳过

    参考与说明

    • 官方题目页:LeetCode 27. 移除元素。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:从后往前补位法在五种语言里的实现(只在 Python 里数了写入次数);写入次数与实际耗时的关系。
    京ICP备2026067502号-1京公网安备11010502063487号