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

题目链接

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

题目描述

给你一个有序数组 nums,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组并在使用 O(1) 额外空间的条件下完成。


说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

java 复制代码
// nums 是以"引用"方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例

示例 1:

输入: nums = [1,1,1,2,2,3]
输出: 5, nums = [1,1,2,2,3]
解释: 函数应返回新长度 length = 5,并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。不需要考虑数组中超出新长度后面的元素。


示例 2:

输入: nums = [0,0,1,1,1,1,2,3,3]
输出: 7, nums = [0,0,1,1,2,3,3]
解释: 函数应返回新长度 length = 7,并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。


提示

  • 1 <= nums.length <= 3 * 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按升序排列

题解

方法一:计数辅助法(空间 O(1))

思路

由于数组已经排序,相同的元素必然连续出现。我们可以使用快慢指针配合计数器来解决问题:

  • 快指针 fast:从索引 1 开始遍历数组,寻找不同的元素。
  • 慢指针 slow :从索引 1 开始,指向下一个有效元素应该存放的位置
  • 计数器 count:记录当前元素已经出现的次数。

遍历过程中:

  • 如果快指针找到的元素nums[fast]与前一个有效元素nums[slow-1]不同,说明遇到了新元素,将其放到慢指针nums[slow]位置,重置计数器count为 1;
  • 如果元素相同nums[slow-1] == nums[fast],且计数器count小于 2,说明当前元素还可以再出现一次,将其放到慢指针nums[slow]位置,计数器count加 1;
  • 如果计数器count已经达到 2,则跳过该元素。

这种方法逻辑清晰,但需要额外维护一个计数器变量。

复杂度分析

  • 时间复杂度: O(n),快指针遍历整个数组一次。
  • 空间复杂度: O(1),原地操作,仅需常数个额外变量。

代码

java 复制代码
class Solution {
    public int removeDuplicates(int[] nums) {
        // 数组长度不超过 2 时,直接返回原长度
        if (nums.length <= 2) {
            return nums.length;
        }

        // 慢指针:指向下一个有效元素应存放的位置
        int slow = 1;
        // 记录当前元素已出现的次数
        int count = 1;

        // 快指针:从索引 1 开始遍历
        for (int fast = 1; fast < nums.length; fast++) {
            // 如果遇到不同的元素,说明遇到了新元素
            if (nums[slow - 1] != nums[fast]) {
                // 将新元素放到慢指针位置
                nums[slow] = nums[fast];
                // 重置计数器
                count = 1;
                // 慢指针右移
                slow++;
            } else {
                // 元素相同,但出现次数少于 2 次,可以保留
                if (count < 2) {
                    nums[slow] = nums[fast];
                    // 计数器加 1
                    count++;
                    // 慢指针右移
                    slow++;
                }
                // 如果 count >= 2,则跳过该元素
            }
        }

        // 遍历结束后,slow 的值即为有效元素的个数
        return slow;
    }
}

图解示例

nums = [1,1,1,2,2,3] 为例:

text 复制代码
初始状态:[1,  1,  1,  2,  2,  3]   slow=1, fast=1, count=1
              ↑
          slow/fast

第1步:fast=1, nums[fast] == nums[slow-1]=1,相同,count=1<2 → 保留
       操作:nums[1]=1(不变), count=2, slow=2, fast=2
         [1,  1,  1,  2,  2,  3]
                  ↑
               slow/fast
       count=2

第2步:fast=2, nums[fast] == nums[slow-1]=1,相同,count=2 → 跳过
       操作:仅 fast++ → fast=3, slow 保持 2
         [1,  1,  1,  2,  2,  3]
                  ↑   ↑
                slow fast
       count=2

第3步:fast=3, nums[fast]=2 ≠ nums[slow-1]=1,新元素 → 保留
       操作:nums[2]=2 ~覆盖, count=1, slow=3, fast=4
         [1,  1, ~2,  2,  2,  3]   ← ~后为本次修改位置
                      ↑   ↑
                    slow  fast
       count=1

第4步:fast=4, nums[fast]=2 == nums[slow-1]=2,相同,count=1<2 → 保留
       操作:nums[3]=2(不变), count=2, slow=4, fast=5
         [1,  1,  2,  2,  2,  3]
                          ↑   ↑
                        slow fast
       count=2

第5步:fast=5, nums[fast]=3 ≠ nums[slow-1]=2,新元素 → 保留
       操作:nums[4]=3 ~覆盖, count=1, slow=5, fast=6(结束)
         [1,  1,  2,  2, ~3, 3]   ← 前5个为有效结果
                             ↑
                            slow(返回5)
       count=1

方法二:通用覆盖法(最优解,空间 O(1))

思路

利用数组已排序的特性,可以设计更简洁的覆盖逻辑:

由于每个元素最多允许出现两次,慢指针 slow 从索引 2 开始,快指针 fast 也从索引 2 开始遍历。对于快指针遇到的每个元素,只需判断它是否等于 nums[slow - 2]

  • 如果 nums[fast] != nums[slow - 2],说明当前元素可以安全保留(最多是第二次出现),将其赋值给 nums[slow],慢指针右移。
  • 如果 nums[fast] == nums[slow - 2],说明该元素已经出现了两次,跳过即可。

这种方法的核心洞察是:慢指针前方两位的元素,一定代表了当前"允许出现两次"的元素的边界。代码极其精简,是最优解法。


核心原理:为什么比较 slow-2 就能保证最多出现两次?

不变量nums[0...slow-1] 中每个元素最多出现 2 次。

证明思路(数学归纳法):

  1. 基础情况slow=2 时,前 2 个元素最多出现 2 次

  2. 归纳步骤 :假设 nums[0...slow-1] 已满足条件,判断 nums[fast] 能否写入:

    • 如果 nums[fast] == nums[slow-2]
      • 由于数组有序,nums[slow-2] == nums[slow-1](相同元素连续)
      • 说明该元素已出现 2 次,再写入会变成 3 次 → 跳过
    • 如果 nums[fast] != nums[slow-2]
      • 要么是新元素(出现 0 次),要么只出现了 1 次(在 slow-1 位置)
      • 写入后最多出现 2 次 → 保留
  3. 结论 :通过比较 slow-2,可以精确控制每个元素最多出现 2 次

直观理解slow-2 是「已保留序列」的倒数第 2 个元素,如果当前元素等于它,说明已经出现了 2 次(slow-2slow-1),不能再写入。


复杂度分析

  • 时间复杂度: O(n),快指针遍历整个数组一次。
  • 空间复杂度: O(1),原地操作,无需额外空间。

代码

java 复制代码
class Solution {
    public int removeDuplicates(int[] nums) {
        // 数组长度不超过 2 时,直接返回原长度
        if (nums.length <= 2) {
            return nums.length;
        }

        // 慢指针:从索引 2 开始,指向下一个有效元素应存放的位置
        int slow = 2;

        // 快指针:从索引 2 开始遍历
        for (int fast = 2; fast < nums.length; fast++) {
            // 如果当前元素与 slow 前方两位的元素不同,说明可以保留
            // nums[slow-2] 代表已保留的元素边界,保证每个元素最多出现两次
            if (nums[slow - 2] != nums[fast]) {
                // 将有效元素放到慢指针位置
                // 然后慢指针右移
                nums[slow++] = nums[fast];
            }
        }

        // 遍历结束后,slow 的值即为有效元素的个数
        return slow;
    }
}

图解示例

nums = [0,0,1,1,1,1,2,3,3] 为例:

text 复制代码
初始状态:[0,  0,  1,  1,  1,  1,  2,  3,  3]   slow=2, fast=2
                  ↑
              slow/fast

第1步:fast=2, nums[fast]=1 != nums[slow-2]=0,不同 → 保留
       操作:nums[2]=1(不变), slow=3, fast=3
         [0,  0,  1,  1,  1,  1,  2,  3,  3]
                      ↑
                  slow/fast

第2步:fast=3, nums[fast]=1 != nums[slow-2]=0,不同 → 保留
       操作:nums[3]=1(不变), slow=4, fast=4
         [0,  0,  1,  1,  1,  1,  2,  3,  3]
                          ↑
                      slow/fast

第3步:fast=4, nums[fast]=1 == nums[slow-2]=1,相同 → 跳过
       操作:仅 fast++ → fast=5, slow 保持 4
         [0,  0,  1,  1,  1,  1,  2,  3,  3]
                          ↑   ↑
                        slow fast

第4步:fast=5, nums[fast]=1 == nums[slow-2]=1,相同 → 跳过
       操作:仅 fast++ → fast=6, slow 保持 4
         [0,  0,  1,  1,  1,  1,  2,  3,  3]
                          ↑       ↑
                        slow     fast

第5步:fast=6, nums[fast]=2 != nums[slow-2]=1,不同 → 保留
       操作:nums[4]=2 ~覆盖, slow=5, fast=7
         [0,  0,  1,  1, ~2,  1,  2,  3,  3]   ← ~后为本次修改位置
                              ↑       ↑
                            slow     fast

第6步:fast=7, nums[fast]=3 != nums[slow-2]=1,不同 → 保留
       操作:nums[5]=3 ~覆盖, slow=6, fast=8
         [0,  0,  1,  1,  2, ~3,  2,  3,  3]   ← ~后为本次修改位置
                                  ↑       ↑
                                slow     fast

第7步:fast=8, nums[fast]=3 != nums[slow-2]=2,不同 → 保留
       操作:nums[6]=3 ~覆盖, slow=7, fast=9(结束)
         [0,  0,  1,  1,  2,  3, ~3,  3,  3]   ← 前7个为有效结果
                                      ↑
                                    slow(返回7)

最终结果:返回 slow=7,nums 前 7 个元素为 [0, 0, 1, 1, 2, 3, 3]

两种方法对比

方法 时间复杂度 空间复杂度 代码简洁度 适用场景
计数辅助法 O(n) O(1) 逻辑直观,适合初学者理解
通用覆盖法 O(n) O(1) 最优解,代码精简,可扩展至 k 次
相关推荐
pwn蒸鱼2 小时前
leetcode:21. 合并两个有序链表
算法·leetcode·链表
洛水水2 小时前
【力扣100题】15.删除链表的倒数第 N 个结点
算法·leetcode·链表
wfbcg2 小时前
每日算法练习:LeetCode 3. 无重复字符的最长子串 ✅
算法·leetcode·职场和发展
_日拱一卒2 小时前
LeetCode:矩阵置零
java·数据结构·线性代数·算法·leetcode·职场和发展·矩阵
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.10):三个相等元素之间的最小距离 I
算法·leetcode
玛丽莲茼蒿3 小时前
Leetcode hot100 【中等】括号生成
算法·leetcode·职场和发展
小欣加油3 小时前
leetcode 128 最长连续序列
c++·算法·leetcode·职场和发展
pwn蒸鱼3 小时前
leetcode:92. 反转链表 II
算法·leetcode·链表
ulias2123 小时前
leetcode热题 - 2
算法·leetcode·职场和发展