Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II

1 题目

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

考虑 nums 的唯一元素的数量为 k。去重后,返回唯一元素的数量 k。

nums 的前 k 个元素应包含 排序后 的唯一数字。下标 k - 1 之后的剩余元素可以忽略。

判题标准:

系统会用下面的代码来测试你的题解:

复制代码
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有断言都通过,那么您的题解将被 通过

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 3 * 104
  • -100 <= nums[i] <= 100
  • nums 已按 非递减 顺序排列。

2 代码实现

c++

cpp 复制代码
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if (nums.empty()){
            return 0 ;
        }

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

js

javascript 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if (nums.length === 0){
        return 0 ;
    }
    let slow = 0 ;
    for (let fast = 1 ; fast < nums.length ; fast ++ ){
        if (nums[slow ] !== nums [fast]){
            slow ++ ;
            nums[slow] = nums[fast ];
        }
    }
    return slow + 1 ;
};

思考

这题是之前做过的,然后我现在的思路就是把元素存在set集合里面,如果之前已经存过了,就。。。啊,怎么原地修改,初始化一个null数组吗,原来的长度数组里面全部都是null吗?

题解

你现在卡在了如何原地删除有序数组的重复项,虽然想到了用 set 去重,但不知道怎么结合 "原地修改" 这个核心要求来实现,我会帮你把这个思路理清楚,并给出非常详细的题解。

核心思路分析

首先要明确题目中的原地修改 是关键:不能创建新数组(包括你提到的 null 数组),必须直接在原数组上操作,通过覆盖的方式把重复元素去掉。

对于有序数组(非严格递增),重复元素一定是连续的,这是解题的核心突破口。(。。我居然没看到这个条件

我们可以用双指针法

  • 慢指针 slow:指向当前已经处理好的最后一个唯一元素的位置(初始为 0)。
  • 快指针 fast:遍历整个数组,寻找和慢指针位置不同的元素(初始为 1)。
  • nums[fast] != nums[slow] 时,说明找到了新的唯一元素,把慢指针后移一位,再将 nums[fast] 覆盖到 nums[slow] 位置。
  • 遍历结束后,slow + 1 就是唯一元素的个数(因为索引从 0 开始)。

完整代码实现(详细注释版)

cpp 复制代码
#include <vector>
using namespace std;

int removeDuplicates(vector<int>& nums) {
    // 处理边界情况:如果数组为空,直接返回0
    if (nums.empty()) {
        return 0;
    }

    // 慢指针:初始指向第一个元素(第一个元素一定是唯一的)
    int slow = 0;
    // 快指针:从第二个元素开始遍历
    for (int fast = 1; fast < nums.size(); ++fast) {
        // 找到和慢指针位置不同的元素(新的唯一元素)
        if (nums[fast] != nums[slow]) {
            // 慢指针后移,指向新的唯一元素要存放的位置
            slow++;
            // 覆盖:把新的唯一元素放到慢指针位置
            nums[slow] = nums[fast];
        }
        // 如果相等,说明是重复元素,快指针直接跳过,慢指针不动
    }

    // 慢指针的索引+1 就是唯一元素的个数(因为索引从0开始)
    return slow + 1;
}

// 测试代码(可以直接运行验证)
#include <iostream>
int main() {
    // 示例1测试
    vector<int> nums1 = {1, 1, 2};
    int k1 = removeDuplicates(nums1);
    cout << "示例1结果:长度=" << k1 << ",数组前" << k1 << "个元素:";
    for (int i = 0; i < k1; ++i) {
        cout << nums1[i] << " "; // 输出:1 2
    }
    cout << endl;

    // 示例2测试
    vector<int> nums2 = {0,0,1,1,1,2,2,3,3,4};
    int k2 = removeDuplicates(nums2);
    cout << "示例2结果:长度=" << k2 << ",数组前" << k2 << "个元素:";
    for (int i = 0; i < k2; ++i) {
        cout << nums2[i] << " "; // 输出:0 1 2 3 4
    }
    cout << endl;

    return 0;
}

代码执行过程拆解(以示例 1 为例)

示例 1:nums = [1,1,2]

  1. 初始状态:slow=0(指向 1),fast=1(指向 1)。
  2. nums[fast] == nums[slow](1==1),快指针 ++,fast=2(指向 2)。
  3. nums[fast] != nums[slow](2!=1):
    • slow++slow=1
    • nums[1] = nums[2] → 数组变为 [1,2,2]
  4. 循环结束,返回 slow+1=2,数组前 2 个元素是[1,2],符合要求。

为什么不用 set?

你最初想到的 set 确实能去重,但有两个问题:

  1. 无法原地修改:set 会创建新的集合,需要把 set 中的元素再拷贝回原数组,虽然能实现功能,但违背了 "原地修改" 的最优解思路(时间 / 空间复杂度更高)。
  2. 时间 / 空间复杂度:双指针法的时间复杂度是 O (n)(仅遍历一次数组),空间复杂度是 O (1)(只用到两个指针);而 set 的方法时间复杂度是 O (n log n)(插入 set 的时间),空间复杂度是 O (n)(存储 set),效率更低。

总结

  1. 核心解法是双指针法,利用数组 "有序" 的特性,通过慢指针标记唯一元素位置、快指针遍历找新元素,实现原地去重。
  2. 边界条件要处理:数组为空时直接返回 0。
  3. 最终返回值是slow + 1(因为慢指针是索引,从 0 开始),数组前slow + 1个元素就是去重后的结果。

这个解法完全满足题目 "原地修改、保持相对顺序、返回唯一元素个数" 的所有要求,也是这道题的最优解。

3 题目

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

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

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

说明:

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

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

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

复制代码
// 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 * 104
  • -104 <= nums[i] <= 104
  • nums 已按升序排列

4 代码实现

c++

cpp 复制代码
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if (nums.size() <= 2 ){
            return nums.size() ;
        }

        int slow = 1 ;
        for (int fast = 2  ; fast < nums.size() ; fast++ ){
            if (nums[slow - 1] != nums [fast]){
                slow ++ ;
                nums[slow] = nums[fast] ;
            }
        }
        return slow +1 ;
    }
};

js

javascript 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if (nums.length <= 2 ){
        return nums.length ;
    }
    let slow = 1 ;
    for (let fast = 2 ; fast < nums.length ; fast ++){
        if (nums [slow -1 ] != nums[fast ]){
            slow ++ ;
            nums[slow] = nums[fast];
        }
    }
    return slow +1 ;
};

思考

这一题是上一题的升级,区别就是判断的条件多了一个元素出现的次数之前是1,现在是1或者2。

我以为只是简单的fast ++ 改成fast = fast + 2 。。。完全不行,不知道怎么写。

不是简单的修改步长。

题解

你现在要解决的是 "删除有序数组中的重复项 II" 问题,核心难点是从 "每个元素只留 1 个" 升级到 "每个元素最多留 2 个",你尝试直接改 fast 指针步长的思路不对,我会先帮你理清核心逻辑,再给出详细的 C++ 题解。

核心思路分析

这道题依然可以用双指针法(原地修改的最优解),核心是调整 "判断重复" 的条件:

  • 慢指针 slow:指向已处理好的最后一个元素的位置(初始为 1,因为前 2 个元素天然可以保留)。
  • 快指针 fast:遍历数组寻找可保留的元素(初始为 2,从第 3 个元素开始判断)。
  • 判断条件:nums[fast] != nums[slow-1]。因为数组是有序的,只要当前元素和 "慢指针前一位" 的元素不同,说明它最多只出现 2 次,可保留;反之则是重复超过 2 次,需跳过。

举个例子:数组 [1,1,1,2,2,3]

  • slow=1(指向第二个 1),fast=2(指向第三个 1):nums[2] == nums[0](1==1)→ 重复超 2 次,跳过。
  • fast=3(指向 2):nums[3] != nums[1](2≠1)→ 保留,慢指针后移并覆盖。

完整 C++ 代码(详细注释版)

cpp 复制代码
#include <vector>
using namespace std;

int removeDuplicates(vector<int>& nums) {
    // 边界条件:数组长度≤2时,所有元素都可保留,直接返回原长度
    if (nums.size() <= 2) {
        return nums.size();
    }

    // 慢指针:初始为1(前2个元素默认保留)
    int slow = 1;
    // 快指针:从第3个元素开始遍历(索引2)
    for (int fast = 2; fast < nums.size(); ++fast) {
        // 核心判断:当前元素 和 慢指针前一位的元素不同 → 说明最多出现2次,可保留
        // (因为数组有序,若nums[fast] == nums[slow-1],则nums[fast] == nums[slow] == nums[slow-1],即重复超2次)
        if (nums[fast] != nums[slow - 1]) {
            // 慢指针后移,指向新的可保留位置
            slow++;
            // 覆盖:将当前元素放到慢指针位置
            nums[slow] = nums[fast];
        }
        // 若相等,说明重复超2次,快指针直接跳过,慢指针不动
    }

    // 慢指针是索引,返回长度需+1(索引从0开始)
    return slow + 1;
}

// 测试代码(可直接运行验证)
#include <iostream>
int main() {
    // 示例1测试
    vector<int> nums1 = {1,1,1,2,2,3};
    int len1 = removeDuplicates(nums1);
    cout << "示例1结果:长度=" << len1 << ",数组前" << len1 << "个元素:";
    for (int i = 0; i < len1; ++i) {
        cout << nums1[i] << " "; // 输出:1 1 2 2 3
    }
    cout << endl;

    // 示例2测试
    vector<int> nums2 = {0,0,1,1,1,1,2,3,3};
    int len2 = removeDuplicates(nums2);
    cout << "示例2结果:长度=" << len2 << ",数组前" << len2 << "个元素:";
    for (int i = 0; i < len2; ++i) {
        cout << nums2[i] << " "; // 输出:0 0 1 1 2 3 3
    }
    cout << endl;

    return 0;
}

核心判断条件详解(为什么用 nums[fast] != nums[slow-1]

数组是有序的,所以重复元素必然连续,我们可以通过 "慢指针前一位" 来判断当前元素是否重复超 2 次:

  • slow 指向已处理的最后一个元素时,slow-1 就是前一个元素的位置。
  • 如果 nums[fast] == nums[slow-1]:说明 nums[fast]nums[slow]nums[slow-1] 三者相等,即该元素已出现 2 次以上,需跳过。
  • 如果 nums[fast] != nums[slow-1]:说明该元素最多出现 2 次,可保留。

代码执行过程拆解(示例 1:nums = [1,1,1,2,2,3]

  1. 初始:数组长度 = 6>2,slow=1(指向第二个 1),fast=2(指向第三个 1)。
  2. fast=2nums[2] == nums[0](1==1)→ 重复超 2 次,fast++(变为 3)。
  3. fast=3nums[3] (2) != nums[0] (1) → 符合条件:
    • slow++(变为 2);
    • nums[2] = nums[3] → 数组变为 [1,1,2,2,2,3]
    • fast++(变为 4)。
  4. fast=4nums[4] (2) != nums[1] (1) → 符合条件:
    • slow++(变为 3);
    • nums[3] = nums[4] → 数组仍为 [1,1,2,2,2,3]
    • fast++(变为 5)。
  5. fast=5nums[5] (3) != nums[2] (2) → 符合条件:
    • slow++(变为 4);
    • nums[4] = nums[5] → 数组变为 [1,1,2,2,3,3]
    • fast++(变为 6),循环结束。
  6. 返回 slow+1=5,数组前 5 个元素为 [1,1,2,2,3],符合要求。

为什么不能直接改 fast += 2

  • 数组中元素的重复次数是不确定的(可能 1 次、2 次、3 次及以上),固定步长会漏掉需要保留的元素。
  • 比如示例 2 中的 [0,0,1,1,1,1,2,3,3],若 fast 每次 + 2,会跳过中间的有效元素(如第二个 1、2 等),导致结果错误。

总结

  1. 核心解法:双指针法,核心判断条件从 "和慢指针当前位置不同" 升级为 "和慢指针前一位位置不同",实现 "最多保留 2 次" 的逻辑。
  2. 边界处理:数组长度≤2 时直接返回原长度,无需处理。
  3. 关键逻辑 :利用数组有序的特性,通过 nums[fast] != nums[slow-1] 判断元素是否可保留,避免重复超 2 次。

这个解法的时间复杂度是 O (n)(仅遍历一次数组),空间复杂度是 O (1)(仅用两个指针),完全符合题目 "原地修改、O (1) 额外空间" 的要求。你可以直接复制代码测试,所有示例用例都会通过。

自己乱写又错了

cpp 复制代码
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if (nums.empty() <= 2 ){
            return nums.size() ;
        }

        int slow = 1 ;
        for (int fast = 2  ; fast < nums.size() ; fast++ ){
            if (nums[slow - 1] != nums [fast]){
                slow ++ ;
                nums[slow] = nums[fast] ;
            }
        }
        return slow +1 ;
    }
};

你这段代码出错的核心原因是边界条件的判断语法写错了,还有一些小的格式问题,我会逐行帮你分析错误,然后给出修复后的完整代码,并解释每一处修改的原因。

第一步:逐行分析错误点

1. 最致命的错误:边界条件判断逻辑错误
复制代码
if (nums.empty() <= 2 ){  // 错误!
  • nums.empty() 返回的是 bool 类型(true/false),而不是数组长度。bool 类型和整数 2 比较是完全错误的逻辑。
  • 你的本意是 "如果数组长度 ≤ 2,直接返回原长度",但写成了 "判断数组是否为空(bool 值)≤ 2",这会导致:
    • 当数组为空时,nums.empty()true(等价于整数 1),1 <= 2 成立,返回 0(正确);
    • 当数组长度为 1/2 时,nums.empty()false(等价于整数 0),0 <= 2 成立,返回 1/2(正确);
    • 但这个写法是语法逻辑错误,只是巧合结果对了,本质是错的,且不符合编程规范。
2. 次要格式问题(不影响运行但影响可读性)
  • nums [fast] 中间多了空格,规范写法是 nums[fast]
  • 代码缩进可以更统一,提升可读性。

第二步:修复后的完整正确代码

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        // 修复:正确判断数组长度 ≤ 2 的情况
        if (nums.size() <= 2) {
            return nums.size();
        }

        int slow = 1;
        // 统一格式,去掉多余空格
        for (int fast = 2; fast < nums.size(); fast++) {
            // 修复空格格式,保持代码整洁
            if (nums[slow - 1] != nums[fast]) {
                slow++;
                nums[slow] = nums[fast];
            }
        }
        return slow + 1;
    }
};

// 测试代码(可直接运行验证)
#include <iostream>
int main() {
    // 示例1测试
    vector<int> nums1 = {1,1,1,2,2,3};
    Solution s;
    int len1 = s.removeDuplicates(nums1);
    cout << "示例1结果:长度=" << len1 << ",数组前" << len1 << "个元素:";
    for (int i = 0; i < len1; ++i) {
        cout << nums1[i] << " "; // 输出:1 1 2 2 3
    }
    cout << endl;

    // 示例2测试
    vector<int> nums2 = {0,0,1,1,1,1,2,3,3};
    int len2 = s.removeDuplicates(nums2);
    cout << "示例2结果:长度=" << len2 << ",数组前" << len2 << "个元素:";
    for (int i = 0; i < len2; ++i) {
        cout << nums2[i] << " "; // 输出:0 0 1 1 2 3 3
    }
    cout << endl;

    return 0;
}

第三步:错误修复的详细解释

错误位置 错误代码 修复后代码 修复原因
边界判断 nums.empty() <= 2 nums.size() <= 2 nums.empty() 是判断数组是否为空(返回 bool),nums.size() 才是获取数组长度(返回整数),这是 C++ 容器的基础用法,你的核心逻辑是判断长度≤2,所以必须用size()
格式问题 nums [fast] nums[fast] 去掉多余空格,符合 C++ 代码书写规范,提升可读性。

第四步:验证修复后的代码逻辑(以示例 1 为例)

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

  1. 初始:nums.size()=6 > 2,跳过 if;slow=1fast=2
  2. fast=2nums[0] (1) == nums[2] (1) → 不执行内部逻辑,fast++
  3. fast=3nums[0] (1) != nums[3] (2)slow++(变为 2),nums[2] = 2 → 数组变为[1,1,2,2,2,3]
  4. fast=4nums[1] (1) != nums[4] (2)slow++(变为 3),nums[3] = 2 → 数组不变。
  5. fast=5nums[2] (2) != nums[5] (3)slow++(变为 4),nums[4] = 3 → 数组变为[1,1,2,2,3,3]
  6. 循环结束,返回slow+1=5,完全符合题目要求。

总结

  1. 核心错误 :混淆了 nums.empty()(判断是否为空)和 nums.size()(获取长度)的用法,导致边界条件判断语法错误。
  2. 逻辑正确性 :你对双指针核心逻辑(nums[slow-1] != nums[fast])的理解是对的,只是基础语法写错了。
  3. 关键提醒 :C++ 中empty()返回 bool,size()返回无符号整数,这是容器(vector/string 等)的基础用法,一定要区分清楚。

修复后的代码可以直接提交,所有测试用例都会通过,完全满足题目 "原地修改、返回新长度、最多保留 2 次重复" 的要求。

5 小结

我学习到了双指针对于原地修改的做法,还有一些小细节比如empty的用法,为什么不用set去重,虽然我想起我之前第一次做的时候也想到用set...

加油,坚持!!加油(ง •_•)ง,多练会越来越好的。

相关推荐
Polaris北1 小时前
第二十九天打卡
算法
CodeJourney_J1 小时前
从“Hello World“ 开始 C++
c语言·c++·学习
样例过了就是过了1 小时前
LeetCode热题100 环形链表 II
数据结构·算法·leetcode·链表
码农幻想梦1 小时前
3472. 八皇后(北京大学考研机试题目)
考研·算法·深度优先
匠心网络科技2 小时前
JavaScript进阶-ES6 带来的高效编程新体验
开发语言·前端·javascript·学习·面试
岛雨QA2 小时前
递归「Java数据结构与算法学习笔记5」
数据结构·算法
kebijuelun2 小时前
Learning Personalized Agents from Human Feedback:用人类反馈训练可持续个性化智能体
人工智能·深度学习·算法·transformer
Eloudy3 小时前
稀疏矩阵的 CSR 格式(Compressed Sparse Row)
人工智能·算法·arch·hpc
岛雨QA3 小时前
栈「Java数据结构与算法学习笔记4」
数据结构·算法