删除有序数组中的重复项 II
问题描述
给定一个有序数组 nums
,要求你原地 删除重复出现的元素,使得每个元素最多出现两次 ,并返回删除后数组的新长度 k
。要求满足以下条件:
- 原地修改数组:不能使用额外的数组空间,必须在原数组上进行修改。
- 保持元素的相对顺序一致:删除多余的重复元素后,剩下的元素的顺序必须与原数组中元素的顺序一致。
- 返回值 :新的数组长度
k
,其中前k
个元素为处理后的数组元素。
示例
示例 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。
解题思路
由于数组是有序的,重复的元素会连续出现。我们需要允许每个元素最多出现两次。因此,可以使用双指针的方法遍历数组,控制元素的出现次数。
- 慢指针(k):指向数组中下一个要放置元素的位置,也即新数组的长度。
- 快指针(i):用于遍历整个数组。
算法步骤:
-
初始化指针:
k = 0
,用于记录新数组的长度。
-
遍历数组:
- 对于每个元素
nums[i]
,执行以下操作:- 如果
k < 2
,说明前面少于两个元素,可以直接复制当前元素。 - 或者
nums[i] != nums[k - 2]
,表示当前元素不等于新数组中倒数第二个元素,说明当前元素出现次数未超过两次,可以复制。 - 否则,跳过当前元素。
- 如果
- 对于每个元素
-
复制元素:
- 将符合条件的
nums[i]
复制到nums[k]
。 - 增加
k
的值。
- 将符合条件的
-
返回结果:
- 遍历结束后,
k
即为新数组的长度。 - 数组
nums
的前k
个元素即为处理后的数组。
- 遍历结束后,
代码解释
typescript
function removeDuplicates(nums: number[]): number {
let k = 0; // 初始化新数组的长度
for (let i = 0; i < nums.length; i++) {
if (k < 2 || nums[i] !== nums[k - 2]) {
nums[k] = nums[i]; // 将当前元素放到新数组中
k++; // 增加新数组的长度
}
// 如果当前元素等于 nums[k - 2],则跳过,不复制
}
return k; // 返回新数组的长度
}
详细解释:
-
变量初始化:
let k = 0;
:初始化新数组的长度为 0。
-
遍历数组:
for (let i = 0; i < nums.length; i++) { ... }
:遍历数组中的每个元素。
-
条件判断:
-
if (k < 2 || nums[i] !== nums[k - 2]) { ... }
:k < 2
:前面少于两个元素,可以直接复制。nums[i] !== nums[k - 2]
:当前元素不等于新数组中倒数第二个元素,说明未出现超过两次。
-
-
复制元素:
-
nums[k] = nums[i];
:将符合条件的元素复制到位置k
。 -
k++;
:增加新数组的长度。
-
-
跳过元素:
- 如果上述条件不满足,说明当前元素已经出现超过两次,直接跳过。
-
返回结果:
return k;
:返回新数组的长度。
示例演示
以示例 1 为例,nums = [1,1,1,2,2,3]
:
-
初始状态:
nums = [1,1,1,2,2,3]
k = 0
-
遍历过程:
-
i = 0
:k = 0 < 2
,条件满足。nums[k] = nums[0];
⇒nums[0] = 1
k = 1
-
i = 1
:k = 1 < 2
,条件满足。nums[k] = nums[1];
⇒nums[1] = 1
k = 2
-
i = 2
:k = 2 >= 2
,nums[2] = 1
,nums[k - 2] = nums[0] = 1
nums[2] === nums[k - 2]
,条件不满足,跳过。
-
i = 3
:nums[3] = 2
,nums[k - 2] = nums[0] = 1
nums[3] !== nums[k - 2]
,条件满足。nums[k] = nums[3];
⇒nums[2] = 2
k = 3
-
i = 4
:nums[4] = 2
,nums[k - 2] = nums[1] = 1
nums[4] !== nums[k - 2]
,条件满足。nums[k] = nums[4];
⇒nums[3] = 2
k = 4
-
i = 5
:nums[5] = 3
,nums[k - 2] = nums[2] = 2
nums[5] !== nums[k - 2]
,条件满足。nums[k] = nums[5];
⇒nums[4] = 3
k = 5
-
-
结果:
- 返回
k = 5
nums
的前5
个元素为[1,1,2,2,3]
- 返回
时间和空间复杂度分析
-
时间复杂度 :O(n),其中 n 是数组
nums
的长度。需要遍历一次数组。 -
空间复杂度:O(1),只使用了常数级别的额外空间。
总结
-
利用数组有序的特点:因为数组是有序的,相同的元素必然连续出现。
-
双指针法 :使用慢指针
k
控制新数组的长度,快指针i
遍历原数组。 -
控制元素出现次数 :通过检查
nums[i]
是否等于nums[k - 2]
,来判断当前元素是否已经出现两次。 -
原地修改数组 :在满足条件的情况下,将元素复制到位置
k
,并增加k
的值。
注意事项
-
边界条件 :当
k < 2
时,无需检查,直接复制元素。 -
元素比较:始终将当前元素与新数组中倒数第二个元素进行比较,以控制出现次数不超过两次。
扩展
如果题目要求每个元素最多出现 k 次,该如何修改算法?
- 将条件
k < 2
改为k < n
,并将比较条件改为nums[i] !== nums[k - n]
。
测试代码
以下是测试代码,用于验证函数的正确性:
typescript
function testRemoveDuplicates() {
const nums1 = [1,1,1,2,2,3];
const expectedNums1 = [1,1,2,2,3];
const k1 = removeDuplicates(nums1);
console.assert(k1 === expectedNums1.length, `测试 1 失败:期望长度 ${expectedNums1.length},实际长度 ${k1}`);
for (let i = 0; i < k1; i++) {
console.assert(nums1[i] === expectedNums1[i], `测试 1 失败:索引 ${i},期望值 ${expectedNums1[i]},实际值 ${nums1[i]}`);
}
console.log("测试 1 通过!");
const nums2 = [0,0,1,1,1,1,2,3,3];
const expectedNums2 = [0,0,1,1,2,3,3];
const k2 = removeDuplicates(nums2);
console.assert(k2 === expectedNums2.length, `测试 2 失败:期望长度 ${expectedNums2.length},实际长度 ${k2}`);
for (let i = 0; i < k2; i++) {
console.assert(nums2[i] === expectedNums2[i], `测试 2 失败:索引 ${i},期望值 ${expectedNums2[i]},实际值 ${nums2[i]}`);
}
console.log("测试 2 通过!");
}
testRemoveDuplicates();
结论
该方法利用了数组有序的特性,使用双指针高效地完成了任务,时间复杂度为 O(n),空间复杂度为 O(1)。