LeetCode 27 · 移除元素:双指针的两种打开方式

这道题和 LeetCode 26(删除有序数组中的重复项)是一对姐妹题,核心都是"原地修改数组"。有意思的是,这道题的双指针有两种完全不同的玩法——一个从前往后,一个两头往中间。两种思路都值得掌握,因为它们代表了双指针最经典的两种模式。


题目长什么样

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

输入nums = [0,1,2,2,3,0,4,2], val = 2
输出5, nums = [0,1,4,0,3,_,_,_]

说人话就是:把数组里所有等于 val 的元素删掉,把剩下的元素挤到数组前面,返回剩下几个。后面留了啥无所谓,评测机不看。


第一反应:快慢指针,一个读一个写

最直观的想法——用一个指针 i 扫描整个数组,另一个指针 k 记录"有效区域的末尾"。遇到不是 val 的元素就写入 k 的位置,然后 k 往前推一步。

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

跑一遍示例 2 看看过程:

nums = [0, 1, 2, 2, 3, 0, 4, 2], val = 2
        i=0              k=0

i=0: nums[0]=0 ≠ 2 → nums[0]=0, k=1
i=1: nums[1]=1 ≠ 2 → nums[1]=1, k=2
i=2: nums[2]=2 = 2 → 跳过
i=3: nums[3]=2 = 2 → 跳过
i=4: nums[4]=3 ≠ 2 → nums[2]=3, k=3
i=5: nums[5]=0 ≠ 2 → nums[3]=0, k=4
i=6: nums[6]=4 ≠ 2 → nums[4]=4, k=5
i=7: nums[7]=2 = 2 → 跳过

结果: nums = [0,1,3,0,4,_,_,_], k = 5
维度说明
时间O(n)每个元素只看一次
空间O(1)只用了两个变量

这就是最标准的解法。代码简洁,思路清晰,面试写这个完全够用。

但仔细想想——当要删除的元素很少时,比如数组有 1000 个元素,只有最后 1 个等于 val,这个方法还是会把前面 999 个元素都复制一遍(虽然是复制到自己的位置)。能不能更聪明一点?


换个思路:左右指针,遇到了就换掉

如果题目说"顺序可以变"——那事情就好办了。等于 val 的元素我们根本不需要保留,直接从数组末尾拿一个元素来覆盖它就行。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            if nums[left] == val:
                nums[left] = nums[right]
                right -= 1
            else:
                left += 1
        return left

跑一遍示例 1 看看:

nums = [3, 2, 2, 3], val = 3
        ↑left      ↑right

Step 1: nums[0]=3 = 3 → 用 nums[3]=3 覆盖 → [3,2,2,3], right=2
Step 2: nums[0]=3 = 3 → 用 nums[2]=2 覆盖 → [2,2,2,3], right=1
Step 3: nums[0]=2 ≠ 3 → left=1
Step 4: nums[1]=2 ≠ 3 → left=2
Step 5: left=2 > right=1 → 结束

结果: k=2, nums前2个 = [2,2]

再跑一遍示例 2:

nums = [0,1,2,2,3,0,4,2], val = 2
        ↑left            ↑right

Step 1: nums[0]=0 ≠ 2 → left=1
Step 2: nums[1]=1 ≠ 2 → left=2
Step 3: nums[2]=2 = 2 → 用 nums[7]=2 覆盖 → [0,1,2,2,3,0,4,2], right=6
Step 4: nums[2]=2 = 2 → 用 nums[6]=4 覆盖 → [0,1,4,2,3,0,4,2], right=5
Step 5: nums[2]=4 ≠ 2 → left=3
Step 6: nums[3]=2 = 2 → 用 nums[5]=0 覆盖 → [0,1,4,0,3,0,4,2], right=4
Step 7: nums[3]=0 ≠ 2 → left=4
Step 8: nums[4]=3 ≠ 2 → left=5
Step 9: left=5 > right=4 → 结束

结果: k=5, nums前5个 = [0,1,4,0,3]
维度说明
时间O(n)每个元素最多访问一次
空间O(1)只用了两个变量
赋值次数最多 n 次比快慢指针最多 2n 次更少

两种解法放在一起看

解法时间空间赋值次数保持顺序
快慢指针O(n)O(1)最多 2n✅ 保持
左右双指针O(n)O(1)最多 n❌ 不保证
  • 快慢指针:万能解法,适用于所有"原地移除/过滤"类问题,而且保持元素原始顺序。
  • 左右双指针:当 val 很少时优势明显(赋值次数更少),但会打乱顺序。

面试时先写快慢指针,然后主动提"如果顺序不重要,还可以用左右指针减少赋值次数"——这种递进思考就是面试官想看到的。


这道题教会我什么

同一个技巧,不同的打开方式

双指针不是一种固定写法,而是一种思想——用两个标记位置来解决问题。快慢指针是"一个读一个写",左右指针是"两头往中间收敛"。两种模式在很多题目里都能见到:

  • 快慢指针:删除重复元素、移动零、滑动窗口
  • 左右指针:两数之和(有序数组)、盛水容器、回文判断

"顺序可以变"不是废话

题目特意说"元素的顺序可能发生改变",这个条件不是白给的——它直接打开了左右指针的解法。和 LeetCode 88 一样,题目里的每一个条件都值得多想一步。

O(n) 也能继续优化

两种解法时间复杂度都是 O(n),但实际赋值次数差了一倍。面试中如果能说出"虽然都是 O(n),但赋值操作次数不同",说明你对复杂度的理解已经不局限于大 O 符号了。


完整测试代码

from typing import List


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


class Solution2:
    def removeElement(self, nums: List[int], val: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            if nums[left] == val:
                nums[left] = nums[right]
                right -= 1
            else:
                left += 1
        return left


if __name__ == "__main__":
    s1 = Solution()
    s2 = Solution2()

    nums = [3, 2, 2, 3]
    k = s1.removeElement(nums, 3)
    print(f"解法一: k={k}, nums={nums[:k]}")

    nums = [0, 1, 2, 2, 3, 0, 4, 2]
    k = s1.removeElement(nums, 2)
    print(f"解法一: k={k}, nums={nums[:k]}")

    nums = [3, 2, 2, 3]
    k = s2.removeElement(nums, 3)
    print(f"解法二: k={k}, nums={nums[:k]}")

    nums = [0, 1, 2, 2, 3, 0, 4, 2]
    k = s2.removeElement(nums, 2)
    print(f"解法二: k={k}, nums={nums[:k]}")

    nums = []
    k = s1.removeElement(nums, 0)
    print(f"解法一: k={k}, nums={nums[:k]}")

    nums = [1]
    k = s1.removeElement(nums, 1)
    print(f"解法一: k={k}, nums={nums[:k]}")

相关题目推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开源Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值