目录

题目描述

80. 删除有序数组中的重复项 II

题意分析

给定一个非递减排列的整数数组,要求原地改写它,使得每个不同的值最多保留两份,并返回改写后有效部分的长度。判题只看返回长度 k 以及数组前 k 个位置的内容,后面剩下什么都不重要。

约束里有两个信号。数组已经有序,意味着值相同的元素必然连成一段,判断「这个值已经保留了几个」不需要任何计数结构,只看邻近的几个位置就够。题目要求原地完成、额外空间 $O(1)$,等于直接排除了「新开一个数组再拷回去」的写法。

边界主要有三处:数组长度小于等于 2 时任何元素都不用删;某个值恰好出现两次时要全保留而不是删一个;某个值出现很多次时要连续跳过一长串。返回值是长度而不是下标,差一就会整体错位。

解法:慢指针保留最多两个重复项

核心思路

题目只要求数组前 k 个位置正确,因此不必真的删除并搬移元素,只需用读写双指针原地重写有效前缀。

write 既表示有效前缀长度,也表示下一个写入位置。遍历当前元素 num 时,write < 2 表示前两个元素一定可以保留;否则,仅当 num != nums[write - 2] 时写入。

循环不变量是:nums[0..write-1] 始终是已读元素的合法结果,有序且每个值最多出现两次。

为什么只比较倒数第二个元素?结果前缀仍然有序。若 num == nums[write - 2],那么 nums[write - 2]nums[write - 1]num 必然相等,当前元素是第三份;若不相等,前缀中最多只有一份 num,可以安全追加。写入次数不会超过读取次数,所以覆盖的都是已经读取过的位置。

解题步骤

  1. 初始化 write = 0
  2. 从左到右读取每个 num
  3. write < 2num != nums[write - 2],将 num 写到 nums[write],再令 write++
  4. 否则跳过当前元素。
  5. 遍历结束后返回 write,无需处理数组尾部。

[1,1,1,2,2,3],前三个 1 只写入两个;两个 2 都与结果前缀倒数第二位不同,均被保留;最终前缀为 [1,1,2,2,3],返回 5

代码实现

class Solution {
    public int removeDuplicates(int[] nums) {
        int write = 0;
        for (int num : nums) {
            if (write < 2 || num != nums[write - 2]) {
                nums[write++] = num;
            }
        }
        return write;
    }
}
func removeDuplicates(nums []int) int {
    write := 0
    for _, num := range nums {
        if write < 2 || num != nums[write-2] {
            nums[write] = num
            write++
        }
    }
    return write
}

复杂度分析

  • 时间复杂度:$O(n)$,每个元素只读取一次。
  • 空间复杂度:$O(1)$,直接改写原数组。

关键点总结

  • 原地删除应转化为覆盖写入,数组尾部残留内容不影响答案。
  • 判断对象是已经整理好的结果前缀,而不是原数组中的固定偏移位置。
  • 比较 nums[write - 2] 对应“最多保留两份”;推广到最多保留 m 份时比较 nums[write - m]
  • write 同时表示有效长度和下一写入位置,返回时无需再加减一。

易错点总结

  • 比较 nums[write - 1]:会退化成每个值最多保留一份。
  • 直接访问 nums[write - 2] 而不判断 write < 2:短数组会出现负下标。
  • 比较原数组的 nums[i - 2]:发生覆盖或跳过后,它不再代表结果前缀中的保留数量。
  • 返回 write - 1write 是长度,不是最后一个有效下标。
  • 清理数组尾部:题目只检查前 write 个元素,这一步没有必要。

相似题目

题目 难度 考察点
26. 删除有序数组中的重复项 简单 每个值只保留一份的基础版本
27. 移除元素 简单 按值过滤,与前缀内容无关
283. 移动零 简单 保序前移后还需回填尾部
443. 压缩字符串 中等 重写内容与原内容长度不等
75. 颜色分类 中等 无序输入下的三向双指针划分