LeetCode 27.移除元素:双指针解法

2 阅读4分钟

前言

作为算法入门的经典题型,LeetCode 27.移除元素看似简单,却藏着数组操作、双指针思想、边界判断等核心考点。很多新手(包括我)第一次写代码时都会踩逻辑坑,


一、题目再解读

原题题干

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变,不需要考虑数组中超出新长度后面的元素。

核心规则(抓重点)

原地修改:不能新建数组,只能在原数组上改,空间复杂度O(1)​

返回值:返回移除后的数组长度k,原数组前k个元素为有效元素​

顺序无关:有效元素顺序可以打乱,这是双指针优化的关键​

无需处理k之后的元素,系统不会校验​ 示例演示

输入:nums = [3,2,2,3], val = 3​

输出:2, nums = [2,2,,]​

解释:返回新长度2,数组前两位为有效元素,后续元素可忽略


二、新手踩坑实录(我的错误代码复盘)

第一次写这道题时,我想用首尾双指针思路,但因为逻辑判断失误,写出了有bug的代码,先复盘错误点,避免大家踩同样的坑。 错误代码展示

var removeElement = function (nums, val) {
    //创建左右指针
    let left = 0;
    let right = nums.length - 1;
    //计数k
    let k = 0;
    while((left<=nums.length-1)||(left<=right)){
        // 致命错误:判断下标而非数组元素
        if(left===val){
            if(nums[right]!=val){
                nums[left]=nums[right];
                nums[right]=val;
                left++;right--;k++;
            }else{
                right--;
            }
        }else{
            left++
            k++;
        }
    }
    return k;
}

核心错误分析​

1.判断条件写错:本意判断nums[left] === val,却写成left === val,混淆了数组下标和数组元素

2.循环条件冗余:只需left <= right即可,多余的判断会导致死循环或越界

3.计数逻辑混乱:双指针解法无需单独计数k,最终left下标就是有效长度,强行计数会导致结果偏差


三、最优解法:首尾双指针法(空间O(1),时间O(n))

解法思路

利用首尾双指针,从数组两端向中间遍历,核心逻辑:

左指针left:从头向后找等于val的待删除元素

右指针right:从尾向前找不等于val的有效元素

找到待删除元素时,用右指针指向的有效元素覆盖左指针位置,再收缩右指针

若当前元素有效,直接移动左指针

循环结束后,left下标即为有效数组长度

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function (nums, val) {
    // 左指针:寻找待删除元素
    let left = 0;
    // 右指针:寻找有效元素
    let right = nums.length - 1;

    // 双指针相遇时结束循环
    while (left <= right) {
        // 找到待删除元素,用右侧有效元素覆盖
        if (nums[left] === val) {
            nums[left] = nums[right];
            // 右指针左移,舍弃原右侧位置
            right--;
        } else {
            // 当前元素有效,左指针继续前进
            left++;
        }
    }
    // 左指针下标即为有效数组长度
    return left;
};

初始化指针:left从0开始(数组头部),right从nums.length-1开始(数组尾部)

循环条件:left <= right,保证双指针覆盖所有元素

核心判断:若左指针元素是val,用右指针元素覆盖,右指针左移(相当于删除当前元素)

有效元素:若左指针元素不是val,直接右移左指针,保留该元素

返回值:left最终停在有效元素末尾,即为新长度


四、复杂度分析

时间复杂度:O(n),数组仅遍历一次,双指针移动总次数不超过n

空间复杂度:O(1),仅用了两个指针变量,无额外数组空间开销,完全符合题目要求


五、举一反三:其他解法(快慢指针)

除了首尾双指针,这道题还能用快慢指针(同向双指针)解决,适合需要保留元素顺序的场景:

快指针:遍历整个数组,寻找有效元素

慢指针:记录有效元素的存放位置

快指针找到有效元素,就赋值给慢指针位置,慢指针右移

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;
};

结语

算法入门不要怕写错题,复盘踩坑过程反而比直接记答案更有效。移除元素这道题吃透双指针思路后,类似的数组去重、元素移动题型都能套用这个逻辑。