LeetCode 27. 移除元素(双指针详解 + Java Python 多解法对比)

题目链接

27. 移除元素 - 力扣(LeetCode)


题目描述

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

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

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

用户评测

评测机将使用以下代码测试您的解决方案:

java 复制代码
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

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

示例 2

text 复制代码
输入: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 个元素之外留下了什么并不重要(因此它们并不计入评测)。

解题思路

这道题是数组操作中的经典题目,核心要求是 原地移除 指定元素。与第 26 题不同,这道题的数组 不一定有序,而且元素的顺序可以发生改变。这个条件给了我们更多的解题思路。

本文将介绍三种解法,从暴力解法到两种双指针解法,逐步优化。


方法一:暴力解法(不推荐)

思路

遍历数组,当遇到等于 val 的元素时,向后寻找一个不等于 val 的元素,将两者交换。这样可以保证前面的元素都不是 val,同时统计不等于 val 的元素个数。

Java 代码

java 复制代码
class Solution {
    public int removeElement(int[] nums, int val) {
        if (nums.length == 0) {
            return 0;
        }
        int k = 0;
        for (int index = 0; index < nums.length; index++) {
            if (nums[index] == val) {
                // 向后寻找一个非val元素交换过来
                for (int i = index + 1; i < nums.length; i++) {
                    if (nums[i] != val) {
                        nums[index] = nums[i];
                        nums[i] = val;
                        break;
                    }
                }
            }
            if (nums[index] != val) {
                k++;
            }
        }
        return k;
    }
}

复杂度分析

  • 时间复杂度O(n²),因为每遇到一个 val 元素,都需要向后遍历寻找非 val 元素。
  • 空间复杂度O(1),只使用了常数级别的额外空间。

缺点

虽然空间复杂度满足要求,但时间复杂度太高,在数据规模较大时会超时,不推荐使用。


方法二:同向双指针(快慢指针)

思路

这种解法和第 26 题的思路类似,使用两个指针 slowfast

  1. fast 指针用于遍历整个数组。
  2. slow 指针指向下一个应该被覆盖的位置。
  3. nums[fast] != val 时,说明这个元素需要保留,将其赋值给 nums[slow],然后 slow 向后移动。
  4. nums[fast] == val 时,跳过这个元素,fast 继续向前移动。
  5. 最终 slow 的值就是不等于 val 的元素个数。

Java 代码

java 复制代码
class Solution {
    public int removeElement(int[] nums, int val) {
        int slow = 0;
        for (int fast = 0; fast < nums.length; fast++) {
            if (nums[fast] != val) {
                nums[slow] = nums[fast];
                slow++;
            }
        }
        return slow;
    }
}

代码说明

  1. slow 初始化为 0,表示第一个需要保留的元素应该放在位置 0
  2. fast0 开始遍历数组。
  3. 如果 nums[fast] != val,说明当前元素需要保留,将其赋值给 nums[slow],然后 slow++
  4. 如果 nums[fast] == val,说明当前元素需要移除,直接跳过。
  5. 最终 slow 的值就是保留的元素个数。

复杂度分析

  • 时间复杂度O(n),只需要遍历一次数组。
  • 空间复杂度O(1),只使用了常数级别的额外空间。

优点

  • 简单直观,代码量少。
  • 保持了元素的相对顺序。
  • 适用于需要保持原顺序的场景。

方法三:双向双指针(首尾指针)

思路

题目明确说明 元素的顺序可能发生改变 ,这意味着我们可以用更巧妙的方法。使用两个指针 leftright

  1. left 从数组开头向右移动。
  2. right 从数组末尾向左移动。
  3. nums[left] == val 时,将 nums[right] 的值赋给 nums[left],然后 right--。这样就把 val 元素"扔"到了数组末尾。
  4. nums[left] != val 时,left++
  5. left > right 时,循环结束,left 的值就是保留的元素个数。

Java 代码

java 复制代码
class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            if (nums[left] == val) {
                nums[left] = nums[right];
                right--;
            } else {
                left++;
            }
        }
        return left;
    }
}

Python 代码

python 复制代码
class Solution(object):
    def removeElement(self, nums, val):
        """
        :type nums: List[int]
        :type val: int
        :rtype: int
        """
        left = 0
        right = len(nums) - 1
        while left <= right:
            if nums[left] == val:
                nums[left] = nums[right]
                right -= 1
            else:
                left += 1
        return left

代码说明

  1. left 指向数组开头,right 指向数组末尾。
  2. 循环条件为 left <= right
  3. 如果 nums[left] == val,说明当前元素需要移除,将 nums[right] 的值赋给 nums[left],然后 right--。注意此时 left 不移动,因为新的 nums[left] 可能仍然是 val,需要继续判断。
  4. 如果 nums[left] != val,说明当前元素需要保留,left++
  5. 最终 left 的值就是保留的元素个数。

复杂度分析

  • 时间复杂度O(n),每个元素最多被访问一次。
  • 空间复杂度O(1),只使用了常数级别的额外空间。

优点

  • 效率高,减少了不必要的赋值操作。
  • 当数组中 val 元素较少时,速度会比同向双指针更快。

方法对比

方法 时间复杂度 空间复杂度 相对顺序 适用场景
暴力解法 O(n²) O(1) 保持 数据规模小
同向双指针 O(n) O(1) 保持 需要保持顺序
双向双指针 O(n) O(1) 可能改变 顺序无关紧要

总结

这道题是双指针技巧的又一经典应用。通过对比三种解法,我们可以看到:

  • 暴力解法虽然思路简单,但效率太低,不推荐使用。
  • 同向双指针是最直观的解法,保持了元素的相对顺序,代码简洁易懂。
  • 双向双指针利用了题目中"顺序可以改变"的条件,在某些场景下效率更高。

关键点回顾:

  1. 同向双指针slowfast 都从 0 开始,遇到非 val 元素就赋值给 slow 位置。
  2. 双向双指针left 从开头,right 从末尾,遇到 val 就用 right 元素覆盖。
  3. 两种双指针解法的时间复杂度都是 O(n),空间复杂度都是 O(1)

掌握这两种双指针技巧,对于解决类似的数组原地操作问题非常有帮助。建议读者多做几道相关题目,加深理解。

相关推荐
路溪非溪1 小时前
Python语言的执行流程总结
开发语言·python
宋均浩2 小时前
AI 生成代码质量防线实战:5 个 CI/CD 配置,把 8% 的逻辑错拦在线上之前0
python
kisbad2 小时前
Day 036|OpenAI Agents SDK 快速开始:今天跑通第一个 Agent
java·前端·javascript
憧憬成为java架构高手的小白2 小时前
langchain4j框架学习
java
树码小子2 小时前
Pycharm解释器配置问题解决
ide·python·pycharm
国雪2 小时前
python-协程
python
0x532 小时前
Java网络编程(四)
java·网络
一木 之林2 小时前
Python.六.(一)--1.标准库、常用工具与基础操作(进阶)
python·rpc·dubbo
ly76892 小时前
Java 设计模式详解:从原则到 23 种经典模式
java·开发语言·设计模式