26. 删除有序数组中的重复项(Remove Duplicates from Sorted Array)

频次 ★★★ · 难度 🟢 · 高频:全厂

题目

给定一个有序数组,原地删除重复元素,使每个元素只出现一次,返回新长度。不能使用额外空间,必须 O(1) 额外内存修改输入数组。

示例

输入: nums = [1, 1, 2]
输出: length = 2, nums 前两位为 [1, 2]

思路

数组已排序,重复元素必然相邻。用快慢双指针:

  • slow 指向下一个不重复元素应放的位置
  • fast 遍历数组
  • nums[fast] != nums[fast-1] 时,说明遇到新元素,复制到 slow 位置

代码

public int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    int slow = 1;
    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[fast - 1]) {
            nums[slow] = nums[fast];
            slow++;
        }
    }
    return slow;
}

复杂度

  • 时间:O(n) — 快指针遍历一次
  • 空间:O(1) — 只用了两个指针

边界条件

  • 空数组:nums.length == 0 直接返回 0,代码开头已处理。
  • 只有一个元素:循环体不会执行(fast 从 1 开始且 < 1 不成立),直接返回 slow = 1,正确。
  • 全部重复(如 [1,1,1]):slow 始终不递增,最终返回 1。
  • 全部不重复(如 [1,2,3]):slow 每次都递增,等价于原样返回。

变式

  • 允许重复最多两次(LeetCode 80 题类型):判断条件从”是否等于前一个”改为”是否等于 slow-2 位置的值”,思路完全一样,只是比较的基准前移了一位。
  • 如果数组无序:快慢指针失效,重复元素不一定相邻,需要先排序(多花 O(n log n))或用 HashSet(多花 O(n) 空间)。

易错点

  • 题目要求”原地”修改并返回新长度,不需要slow 之后的元素清零或截断数组——调用方只会看前 slow 个元素,这一点容易和”移除元素”类题目的输出要求搞混。
  • 比较的是 nums[fast] != nums[fast-1](和前一个原始元素比较),而不是和 nums[slow-1](已写入的最后一个)比较——虽然对有序数组这两者其实等价,但理解错原理会在写变式题(如允许重复 2 次)时出错。

面试追问

  • 为什么这题不需要额外空间,而”无序数组去重”通常要用 HashSet? 因为数组有序保证了重复元素必然相邻,快慢指针能利用这个性质原地压缩;无序数组里重复元素可能相隔很远,只能借助额外的哈希结构或先排序。
  • 能不能不改变相对顺序、用双指针从两端向中间夹? 不行,双向夹逼适用于”按值不关心顺序”的场景(如移除特定值),去重需要保持原有相对顺序,只能单向扫描。

关联题

  • 同套路:27. 移除元素283. 移动零 —— 同一把快慢指针
  • 进阶:80. 删除有序数组中的重复项 II —— 最多保留两个:与 slow-2 位置比较,一行改动可推广到”保留 k 个”