移除数组里的指定元素,你学会了吗?

移除数组里的指定元素,你学会了吗?

大家好,今天我们来聊聊 LeetCode 上的一道基础题------第 27 题:移除元素

题目看起来很简单:给你一个数组 nums 和一个值 val,你需要原地 移除所有数值等于 val 的元素,然后返回新数组的长度。

而且不能使用额外的数组空间,只能使用 O(1) 的额外空间,也就是只能在原数组上动手脚。

另外还有两个小条件:

  • 元素的顺序可以改变
  • 新长度后面的元素是什么都无所谓,不用管

举个例子:

text

ini 复制代码
输入:nums = [3,2,2,3], val = 3
输出:新长度 2,并且 nums 的前两个元素应该是 2, 2

另一个例子:

text

ini 复制代码
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:新长度 5,nums 的前五个元素为 0,1,3,0,4(顺序无所谓,但必须是这几个数)

是不是感觉很简单?但真正写代码的时候,很多新手会掉进坑里。

先想想为什么要"原地"操作

如果你刚学编程,可能第一反应是:新建一个数组,把不等于 val 的元素放进去,然后复制回来。

但题目明确说了不能用额外数组,因为面试官想考察的是你对数组内存结构的理解。

数组在内存中是连续的一段空间,你没法像链表那样随意删除一个节点。所谓的"删除",本质上是覆盖------把后面的元素往前挪,盖掉要删的那个。

暴力解法:两层循环

最容易想到的办法就是:遍历数组,遇到等于 val 的元素,就把它后面的所有元素整体往前移动一位,覆盖掉它。

这个过程看起来就像这样:

text

ini 复制代码
初始:[3, 2, 2, 3],val = 3
i=0,发现 nums[0]=3,于是把下标 1~3 的元素依次往前挪:
    -> [2, 2, 3, 3],然后 i-- 回退一位,size 减 1
i=0,现在 nums[0]=2,不动
i=1,nums[1]=2,不动
i=2,nums[2]=3,再次触发删除,把下标 3 往前挪:
    -> [2, 2, 3, 3](但此时 size 已经是 3,实际只看前三个)
结束,新长度 = 2

代码写出来也很直接:

javascript

ini 复制代码
var removeElement = function(nums, val) {
    let size = nums.length;
    for (let i = 0; i < size; i++) {
        if (nums[i] === val) {
            for (let j = i + 1; j < size; j++) {
                nums[j - 1] = nums[j];
            }
            i--;   // 因为后面的元素往前挪了,所以 i 也要回退
            size--; // 数组有效长度减 1
        }
    }
    return size;
};

这个解法的时间复杂度是 O(n²),空间复杂度 O(1)。虽然 LeetCode 上也能过,但效率不高,尤其是当数组很长、要删的元素很多时,嵌套循环会非常慢。

双指针法(快慢指针):优雅又高效

其实我们完全可以用一个循环完成两件事:

  • 一个指针 fast 负责遍历原数组,找出"不该删"的元素
  • 另一个指针 slow 负责指向新数组的末尾,每找到一个"不该删"的元素,就把它放到 slow 的位置,然后 slow++

这样就避免了嵌套循环,时间复杂度直接降到 O(n)。

还是拿例子来看:nums = [0,1,2,2,3,0,4,2], val = 2

  • fast=0,nums[0]=0(不等于2) → nums[slow]=0,slow=1
  • fast=1,nums[1]=1 → nums[slow]=1,slow=2
  • fast=2,nums[2]=2(等于2) → 跳过,slow 不动
  • fast=3,nums[3]=2 → 跳过
  • fast=4,nums[4]=3 → nums[slow]=3,slow=3
  • fast=5,nums[5]=0 → nums[slow]=0,slow=4
  • fast=6,nums[6]=4 → nums[slow]=4,slow=5
  • fast=7,nums[7]=2 → 跳过

结束。新长度 = slow = 5,数组前五个元素是 [0,1,3,0,4]

代码非常简洁:

javascript

ini 复制代码
var removeElement = function(nums, val) {
    let slow = 0;
    for (let fast = 0; fast < nums.length; fast++) {
        if (nums[fast] !== val) {
            nums[slow] = nums[fast];
            slow++;
        }
    }
    return slow;
};

是不是比暴力解法清爽多了?而且因为只遍历一次,时间复杂度 O(n),空间复杂度 O(1)。

双指针的另一种写法:左右对撞

题目说了元素的顺序可以改变,那其实还有一个更高效的办法,尤其适用于要删的元素很少的情况。

我们可以用两个指针,一个从左边找等于 val 的元素,一个从右边找不等于 val 的元素,然后交换(或者说覆盖)。这样能减少移动的次数。

思路大概是:

  • 左指针 left 指向开头,右指针 right 指向末尾

  • left <= right 时循环:

    • 如果 nums[left] == val,就把 nums[right] 的值赋给 nums[left],然后 right--
    • 否则 left++

这样最终 left 就是新数组的长度。

代码:

javascript

sql 复制代码
var removeElement = function(nums, val) {
    let left = 0, right = nums.length - 1;
    while (left <= right) {
        if (nums[left] === val) {
            nums[left] = nums[right];
            right--;
        } else {
            left++;
        }
    }
    return left;
};

这种方法的好处是,当要删除的元素很少时,它不会像快慢指针那样把所有保留元素都复制一遍,而是直接把右边的幸存元素挪过来覆盖。不过代价是顺序会被打乱。

总结

这道题虽然简单,但很能体现对数组操作的理解。暴力法容易想到但效率低,双指针法才是面试官想看到的。

我建议大家先自己写一遍暴力法,再改成快慢指针,最后再看对撞指针的写法。这样对"原地修改数组"这件事的理解会更深刻。

如果你正在刷 LeetCode,可以把这道题当作双指针的入门练习,后面碰到类似的三数之和、移除重复元素之类的题目,思路其实是相通的。

好了,今天的分享就到这里。如果觉得有用,可以点个赞或者收藏一下,也欢迎在评论区讨论你的解法~

相关推荐
吃好睡好便好2 小时前
创建全0矩阵和全1矩阵
开发语言·学习·线性代数·算法·matlab·信息可视化·矩阵
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_12 :(值与单位的技能测试与深入理解)
前端·javascript·css·ui·交互
Maimai108082 小时前
TanStack Table 入门:为什么它是 React 表格开发里的“表格引擎”
前端·javascript·react.js·架构·前端框架·reactjs
happymaker06263 小时前
LeetCodeHot100——128.最长连续序列
算法
余生皆假期-3 小时前
配置 CodeX 环境的 Simlink AI 工具链
笔记·单片机·嵌入式硬件·算法
hef2883 小时前
Word里快速替换短语 省时省力不卡顿
javascript
qq_296553273 小时前
[特殊字符] 旋转排序数组中的高效搜索:从线性到二分查找的进阶之路
数据结构·算法·搜索引擎·分类·柔性数组
用户81071472820393 小时前
《我在 Vue 项目中用到的 JS 核心知识点》
javascript
葬送的代码人生3 小时前
从零到一:AI 全栈开发入门 —— 构建一个简单的用户聊天系统
前端·javascript·架构