☰
doocs/leetcode 题解精讲:LeetCode 27「移除元素」——一次遍历双指针覆盖法
2026/10/16 1:59:14 网站建设 项目流程
  • 示例工程

【免费下载链接】leetcode

🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解

项目地址:https://gitcode.com/doocs/leetcode
点击查看免费下载

本篇技术指南以开源仓库 doocs/leetcode 中 0027.Remove Element 题解文档 为核心,完整讲解 LeetCode 27「移除元素」的题目机制、评测逻辑与最优解法,并结合仓库中 Java、Python、C++、Go、TypeScript、Rust、JavaScript、C#、PHP 共 9 种语言的源码实现,剖析「一次遍历覆盖法」的底层原理。读完本文,你将掌握原地数组改写(In-place)的核心套路——用写指针覆盖前部、以返回值代替真实删除,并能举一反三应用到去重、移动零等同类双指针问题。

一、题目回顾:移除元素到底要做什么

题目给出一个整数数组nums和一个值val,要求原地(In-place)移除所有数值等于val的元素。注意,题目明确说明元素的顺序可能发生改变,然后返回nums中与val不同的元素的数量。

设nums中不等于val的元素数量为k,要通过此题,需要执行以下操作:

  • 更改nums数组,使nums的前k个元素包含不等于val的元素;nums的其余元素和nums的大小并不重要。
  • 返回k。

约束条件(来自原题文档):

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

示例 1:

输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2,_,_] 解释:你的函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3,_,_,_] 解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。 注意这五个元素可以任意顺序返回。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

注意示例 2 中前五个元素是[0,1,4,0,3],与输入中[0,1,2,2,3,0,4,2]的顺序并不完全一致(0与4的相对位置发生了交换),这印证了"元素的顺序可能发生改变"这一关键条件。

二、评测机制:判定标准决定了解法方向

这道题自带一个自定义评测机(Custom Judge),其判定逻辑直接决定了我们写代码的方式。原题文档给出了评测机使用的测试代码:

int[] nums = [...]; // 输入数组 int val = ...; // 要移除的值 int[] expectedNums = [...]; // 长度正确的预期答案。 // 它以不等于 val 的值排序。 int k = removeElement(nums, val); // 调用你的实现 assert k == expectedNums.length; sort(nums, 0, k); // 排序 nums 的前 k 个元素 for (int i = 0; i < k; i++) { assert nums[i] == expectedNums[i]; }

从这段评测代码可以提炼出三条关键信息:

  1. 只检查前k个位置:评测机只对nums[0..k-1]进行排序并逐一断言,k之后的内容完全被忽略。这意味着我们**根本不需要真正"删除"**数组尾部元素,只要把该保留的值"搬运"到前部即可。
  2. 相对顺序可以改变:评测机在断言前会对前k个元素调用sort,因此返回值与预期值不必保持输入时的相对顺序。这是这道题与第 26 题「删除有序数组中的重复项」的本质区别——26 题要求保留有序数组的相对顺序,而本题允许任意顺序。
  3. 必须原地修改:评测机直接读取修改后的nums,因此不能简单地返回一个新建数组的长度。

理解了这三点,"如何以最低成本通过评测"就有了明确的优化方向。

三、三种思路的对比:为什么最终选择覆盖法

原题文档的"思考"部分对三种思路做了权衡,这里展开说明:

思路一:另开一个数组,拷贝所有不等于val的元素

时间复杂度为O(n),当n <= 100时完全可以通过评测。但题目明确要求原地改写,额外的数组分配并不必要,还会让空间复杂度变成O(n),与本题的考察意图(原地算法)相悖。

思路二:每次删除时把后面的元素整体前移(模拟erase)

如果采用"找到等于val的元素就把它删掉、后面的元素逐个前移"的朴素做法,最坏情况下每次前移都要搬运O(n)个元素,整体退化为O(n²)的时间复杂度。虽然n <= 100时也能跑完,但这不是最优解,也偏离了出题人想考察的算法素养。

思路三:一次遍历覆盖法(本题标准解)

题目只要求前k个位置覆盖全部非val元素,且顺序可以改变。因此不必真正"删除":用k记录已写下的个数,遇到x != val就写入nums[k]并将k加一。一遍扫描即可完成,额外空间为O(1)。

三种思路的量化对比:

思路时间复杂度空间复杂度是否满足原地要求
另开数组拷贝O(n)O(n)否
逐次删除前移O(n²)O(1)是
一次遍历覆盖O(n)O(1)是

四、一次遍历覆盖法:原理、正确性与边界

4.1 算法流程

用变量k记录当前不等于val的元素个数,它同时充当写指针:

  1. 遍历数组nums,读指针即循环变量;
  2. 如果当前元素x不等于val,则将x赋值给nums[k],并将k自增1;
  3. 遍历结束后返回k。

4.2 不变式与正确性

算法的核心不变式(invariant)是:在循环的任意时刻,nums[0..k-1]已经保存了扫描过的所有不等于val的元素,且k等于这些元素的个数。

证明思路:

  • 初始:k = 0,前缀为空,不变式成立;
  • 归纳:当读指针扫到元素x时,若x != val,把它写入nums[k]并k++,前缀仍恰好包含全部已扫描的非val元素;若x == val,跳过且k不变,前缀不受影响;
  • 终止:扫描完整数组后,nums[0..k-1]恰好包含全部非val元素,即k就是要返回的答案。

由于写指针k永远不大于读指针(写指针只有在写入时才前进,且每次写入位置k都已被读指针扫描过),覆盖不会破坏尚未读到的数据,这是该算法不需要额外数组的前提。

4.3 边界情况

  • 空数组:nums.length = 0时循环体不执行,直接返回0,正确。
  • val不存在:每个元素都满足x != val,nums[k]原地自覆盖(写入位置与读出位置相同),返回原数组长度,正确。
  • 全等于val:k始终为0,不产生任何写入,返回0,评测机只检查空的前缀,正确。

五、多语言实现:doocs/leetcode 仓库源码全览

doocs/leetcode 仓库为本题提供了 9 种语言的实现,README 中的代码与目录下独立 Solution 文件一一对应(如 Solution.java、Solution.py、Solution.rs 等),可直接复制运行。以下完整给出各语言实现。

Python3(Solution.py):

class Solution: def removeElement(self, nums: List[int], val: int) -> int: k = 0 for x in nums: if x != val: nums[k] = x k += 1 return k

Java(Solution.java):

class Solution { public int removeElement(int[] nums, int val) { int k = 0; for (int x : nums) { if (x != val) { nums[k++] = x; } } return k; } }

C++(Solution.cpp):

class Solution { public: int removeElement(vector<int>& nums, int val) { int k = 0; for (int x : nums) { if (x != val) { nums[k++] = x; } } return k; } };

Go(Solution.go):

func removeElement(nums []int, val int) int { k := 0 for _, x := range nums { if x != val { nums[k] = x k++ } } return k }

TypeScript(Solution.ts):

function removeElement(nums: number[], val: number): number { let k: number = 0; for (const x of nums) { if (x !== val) { nums[k++] = x; } } return k; }

Rust(Solution.rs):

impl Solution { pub fn remove_element(nums: &mut Vec<i32>, val: i32) -> i32 { let mut k = 0; for i in 0..nums.len() { if nums[i] != val { nums[k] = nums[i]; k += 1; } } k as i32 } }

JavaScript(Solution.js):

/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function (nums, val) { let k = 0; for (const x of nums) { if (x !== val) { nums[k++] = x; } } return k; };

C#(Solution.cs):

public class Solution { public int RemoveElement(int[] nums, int val) { int k = 0; foreach (int x in nums) { if (x != val) { nums[k++] = x; } } return k; } }

PHP(Solution.php):

class Solution { /** * @param Integer[] $nums * @param Integer $val * @return Integer */ function removeElement(&$nums, $val) { $k = 0; foreach ($nums as $x) { if ($x != $val) { $nums[$k++] = $x; } } return $k; } }

5.1 各语言实现的共性观察

  • 逻辑完全同构:9 种语言都是"写指针k+ 读指针(循环变量)"的结构,证明该算法的语言无关性,适合作为双指针模板记忆;
  • 签名差异源于语言约定:PHP 通过&$nums显式按引用传参、Rust 使用&mut Vec<i32>可变借用,都是对"原地修改"这一要求的语言级表达;C++ 使用vector<int>&引用传参同理;
  • 返回类型统一:所有实现都返回int/i32,与题目要求的"返回 k"一致。

六、复杂度分析与延伸思考

6.1 复杂度结论

  • 时间复杂度 O(n):对数组做一次线性扫描,每个元素只访问一次;
  • 空间复杂度 O(1):只使用常数个变量(k与循环变量),不依赖数组长度。

其中n为数组nums的长度。这是该问题可达的最优复杂度,因为至少要扫描一遍数组才能知道哪些元素需要保留。

6.2 从"移除元素"到双指针家族

理解了 27 题,可以顺藤摸瓜掌握一系列双指针数组题,这些在 doocs/leetcode 仓库中均有对应题解:

  • 26. 删除有序数组中的重复项(题解):同样是"前 k 个位置保留有效值"的覆盖思路,但要求保留相对顺序且只保留不重复元素,写指针的比较对象从val变为"上一个已写入的值";
  • 283. 移动零:把"保留非零值、其余补零"等价转化为"先覆盖非零值,再对尾部补 0";
  • 80. 删除有序数组中的重复项 II:放宽为"每个元素最多出现两次",写指针需要参考前两位已写入的值。

这些题目共享同一个思维模型:用一个慢指针维护"结果前缀",用一个快指针扫描原数组,27 题是其中最简单的入门样例。

七、在 doocs/leetcode 仓库中查阅与进一步学习

本题在仓库中的完整目录为 solution/0000-0099/0027.Remove Element/,包含:

  • README.md:中文题解文档(题目描述、评测机制、解法与多语言代码);
  • README_EN.md:英文版题解文档;
  • Solution.*:9 种语言的独立可提交源码文件。

仓库按题号区间组织题解(如0000-0099、0100-0199……),每道题一个目录;solution/README.md 提供全部题目的索引表,可在其中快速检索题号、题解、标签与难度。此外,仓库还收录了《剑指 Offer(第 2 版)》(lcof/README.md)、《程序员面试金典(第 6 版)》(lcci/README.md)等系列题解,其中同样包含大量双指针、数组类题目,可与本题对照学习。

小结

LeetCode 27「移除元素」的价值不在题目本身,而在它揭示的原地算法范式:当评测只关心"结果前缀"且顺序可以改变时,用慢指针覆盖 + 快指针扫描,即可在 O(n) 时间、O(1) 空间内完成任务。doocs/leetcode 仓库以 9 种语言完整呈现了这一解法,既可作为复习模板,也是理解后续所有"前缀覆盖类双指针"问题的起点。

  • 示例工程

【免费下载链接】leetcode

🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解

项目地址:https://gitcode.com/doocs/leetcode
点击查看免费下载

相关推荐

上一篇:实时系统编程终极指南:lowlevelprogramming-university项目实战解析
下一篇:llama2-webui CPU部署优化:让普通电脑也能流畅运行大模型

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询