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]
- 初始状态:
slow=0(指向 1),fast=1(指向 1)。 nums[fast] == nums[slow](1==1),快指针 ++,fast=2(指向 2)。nums[fast] != nums[slow](2!=1):slow++→slow=1。nums[1] = nums[2]→ 数组变为[1,2,2]。
- 循环结束,返回
slow+1=2,数组前 2 个元素是[1,2],符合要求。
为什么不用 set?
你最初想到的 set 确实能去重,但有两个问题:
- 无法原地修改:set 会创建新的集合,需要把 set 中的元素再拷贝回原数组,虽然能实现功能,但违背了 "原地修改" 的最优解思路(时间 / 空间复杂度更高)。
- 时间 / 空间复杂度:双指针法的时间复杂度是 O (n)(仅遍历一次数组),空间复杂度是 O (1)(只用到两个指针);而 set 的方法时间复杂度是 O (n log n)(插入 set 的时间),空间复杂度是 O (n)(存储 set),效率更低。
总结
- 核心解法是双指针法,利用数组 "有序" 的特性,通过慢指针标记唯一元素位置、快指针遍历找新元素,实现原地去重。
- 边界条件要处理:数组为空时直接返回 0。
- 最终返回值是
slow + 1(因为慢指针是索引,从 0 开始),数组前slow + 1个元素就是去重后的结果。
这个解法完全满足题目 "原地修改、保持相对顺序、返回唯一元素个数" 的所有要求,也是这道题的最优解。
3 题目
给你一个有序数组 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] <= 104nums已按升序排列
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])
- 初始:数组长度 = 6>2,
slow=1(指向第二个 1),fast=2(指向第三个 1)。 fast=2:nums[2] == nums[0](1==1)→ 重复超 2 次,fast++(变为 3)。fast=3:nums[3] (2) != nums[0] (1)→ 符合条件:slow++(变为 2);nums[2] = nums[3]→ 数组变为[1,1,2,2,2,3];fast++(变为 4)。
fast=4:nums[4] (2) != nums[1] (1)→ 符合条件:slow++(变为 3);nums[3] = nums[4]→ 数组仍为[1,1,2,2,2,3];fast++(变为 5)。
fast=5:nums[5] (3) != nums[2] (2)→ 符合条件:slow++(变为 4);nums[4] = nums[5]→ 数组变为[1,1,2,2,3,3];fast++(变为 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 等),导致结果错误。
总结
- 核心解法:双指针法,核心判断条件从 "和慢指针当前位置不同" 升级为 "和慢指针前一位位置不同",实现 "最多保留 2 次" 的逻辑。
- 边界处理:数组长度≤2 时直接返回原长度,无需处理。
- 关键逻辑 :利用数组有序的特性,通过
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]
- 初始:
nums.size()=6 > 2,跳过 if;slow=1,fast=2。 fast=2:nums[0] (1) == nums[2] (1)→ 不执行内部逻辑,fast++。fast=3:nums[0] (1) != nums[3] (2)→slow++(变为 2),nums[2] = 2→ 数组变为[1,1,2,2,2,3]。fast=4:nums[1] (1) != nums[4] (2)→slow++(变为 3),nums[3] = 2→ 数组不变。fast=5:nums[2] (2) != nums[5] (3)→slow++(变为 4),nums[4] = 3→ 数组变为[1,1,2,2,3,3]。- 循环结束,返回
slow+1=5,完全符合题目要求。
总结
- 核心错误 :混淆了
nums.empty()(判断是否为空)和nums.size()(获取长度)的用法,导致边界条件判断语法错误。 - 逻辑正确性 :你对双指针核心逻辑(
nums[slow-1] != nums[fast])的理解是对的,只是基础语法写错了。 - 关键提醒 :C++ 中
empty()返回 bool,size()返回无符号整数,这是容器(vector/string 等)的基础用法,一定要区分清楚。
修复后的代码可以直接提交,所有测试用例都会通过,完全满足题目 "原地修改、返回新长度、最多保留 2 次重复" 的要求。
5 小结
我学习到了双指针对于原地修改的做法,还有一些小细节比如empty的用法,为什么不用set去重,虽然我想起我之前第一次做的时候也想到用set...
加油,坚持!!加油(ง •_•)ง,多练会越来越好的。