LeetCode 26 · 删除有序数组中的重复项:快慢指针的经典模板

这道题和上一题 LeetCode 27(移除元素)是亲兄弟,连代码结构都几乎一模一样——都是快慢指针原地修改数组。区别在于:LeetCode 27 是"等于某个值就删",这道题是"和前一个一样就删"。掌握了这个模板,一类题全通了。


题目长什么样

给你一个非严格递增排列的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。

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

说人话就是:数组已经排好序了,把重复的挤掉,只留一个,剩下的元素往前挪。返回不重复的元素有几个。

有两个关键条件值得注意:

  • “非严格递增”:意味着相等的元素一定挨在一起,不会散落在数组各处。这是能用双指针的前提。
  • “保持相对顺序”:说明只能从前往后扫,不能用左右指针交换。

一把就够:快慢指针

这道题只有一种最优解法——快慢指针。因为要保持顺序,左右指针的交换方案直接出局。

思路和 LeetCode 27 完全一致:慢指针 k 标记"已去重区域的末尾",快指针 i 扫描数组。遇到和前一个不一样的元素就写入 k 位置。

唯一的区别是判断条件:

  • LeetCode 27:nums[i] != val(不等于目标值就保留)
  • LeetCode 26:nums[i] != nums[i-1](和前一个不一样就保留)
class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0
        k = 1
        for i in range(1, len(nums)):
            if nums[i] != nums[i - 1]:
                nums[k] = nums[i]
                k += 1
        return k

注意 k 从 1 开始——因为第一个元素(nums[0])肯定是唯一的,前面没有和它重复的。i 也从 1 开始,因为我们要拿 nums[i]nums[i-1] 比。

跑一遍示例 2 看看:

nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
                   ↑ 已确定唯一

i=1: nums[1]=0 = nums[0]=0 → 重复,跳过
i=2: nums[2]=1 ≠ nums[1]=0 → 新元素! nums[1]=1, k=2
i=3: nums[3]=1 = nums[2]=1 → 重复,跳过
i=4: nums[4]=1 = nums[3]=1 → 重复,跳过
i=5: nums[5]=2 ≠ nums[4]=1 → 新元素! nums[2]=2, k=3
i=6: nums[6]=2 = nums[5]=2 → 重复,跳过
i=7: nums[7]=3 ≠ nums[6]=2 → 新元素! nums[3]=3, k=4
i=8: nums[8]=3 = nums[7]=3 → 重复,跳过
i=9: nums[9]=4 ≠ nums[8]=3 → 新元素! nums[4]=4, k=5

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

和 LeetCode 27 放在一起看

这两道题的代码几乎一样,放在一起对比就清楚了:

LeetCode 27(移除元素)         LeetCode 26(删除重复项)
─────────────────────         ─────────────────────
k = 0                         k = 1
for i in range(len(nums)):    for i in range(1, len(nums)):
    if nums[i] != val:            if nums[i] != nums[i-1]:
        nums[k] = nums[i]             nums[k] = nums[i]
        k += 1                         k += 1
return k                      return k

三处区别:

区别LeetCode 27LeetCode 26
起始值k = 0(第一个元素也要判断)k = 1(第一个元素直接保留)
遍历起点i = 0i = 1(需要和前一个比较)
判断条件nums[i] != valnums[i] != nums[i-1]

本质上是同一个模板,只是"保留条件"不同。LeetCode 27 的保留条件是"不等于某个值",这道题的保留条件是"和前一个不一样"。


这道题教会我什么

"有序"是最大的提示

题目说"非严格递增",这意味着相等的元素一定连续排列。如果数组是无序的,nums[i] != nums[i-1] 这个判断就废了——不相邻的重复元素会漏掉。所以看到"有序数组",第一反应就应该是:重复元素一定相邻,可以用双指针。

快慢指针是一个万能模板

k = 起始位置
for i in range(遍历范围):
    if 满足保留条件:
        nums[k] = nums[i]
        k += 1
return k

这个模板能解决所有"原地过滤数组"类的问题,只需要改三个地方:起始值、遍历范围、保留条件。LeetCode 27、LeetCode 26、LeetCode 283(移动零)都是这个套路。

边界条件不能忘

if not nums: return 0 这一行很容易漏掉。当数组为空时,k = 1 会导致越界。LeetCode 27 没有这个问题,因为空数组的 range(len(nums)) 直接不执行循环,k = 0 自然返回。但 LeetCode 26 的 k 从 1 开始,空数组必须单独处理。


完整测试代码

from typing import List


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


if __name__ == "__main__":
    s = Solution()

    nums = [1, 1, 2]
    k = s.removeDuplicates(nums)
    print(f"k={k}, nums={nums[:k]}")

    nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
    k = s.removeDuplicates(nums)
    print(f"k={k}, nums={nums[:k]}")

    nums = [1]
    k = s.removeDuplicates(nums)
    print(f"k={k}, nums={nums[:k]}")

    nums = [1, 1, 1, 1]
    k = s.removeDuplicates(nums)
    print(f"k={k}, nums={nums[:k]}")

    nums = [1, 2, 3, 4]
    k = s.removeDuplicates(nums)
    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、付费专栏及课程。

余额充值