LeetCode第80题:删除有序数组中的重复项 II
题目描述
给你一个有序数组 nums ,请你原地删除重复出现的元素,使得每个元素最多出现两次,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
难度
中等
问题链接
示例
示例 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^4nums已按升序排列
解题思路
这道题要求我们删除有序数组中的重复项,使得每个元素最多出现两次,并且要求在原地修改数组,不使用额外的数组空间。
方法:双指针法
- 使用两个指针:慢指针
slow和快指针fast - 慢指针
slow指向当前可以放置元素的位置 - 快指针
fast用于遍历数组 - 对于每个元素,我们需要判断是否应该保留它:
- 如果当前元素是第一个或第二个出现,则保留
- 如果当前元素是第三个或更多次出现,则跳过
- 判断当前元素是否应该保留的方法:
- 如果
slow < 2或者nums[fast] != nums[slow-2],则保留当前元素 - 否则,跳过当前元素
- 如果
关键点
- 利用数组已排序的特性,相同的元素一定相邻
- 使用双指针技巧,一个指针用于遍历,一个指针用于放置元素
- 判断元素是否应该保留的关键是比较当前元素与
slow-2位置的元素
算法步骤分析
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化慢指针 slow = 0 | 慢指针指向当前可以放置元素的位置 |
| 2 | 遍历数组,快指针 fast 从 0 到 n-1 | 使用快指针遍历数组中的每个元素 |
| 3 | 判断当前元素是否应该保留 | 如果 slow < 2 或者 nums[fast] != nums[slow-2],则保留 |
| 4 | 如果保留,则将当前元素放置到 slow 位置,并将 slow 加 1 | 更新数组并移动慢指针 |
| 5 | 返回 slow 作为新数组的长度 | slow 即为删除重复项后的数组长度 |
算法可视化
以示例 1 为例,nums = [1,1,1,2,2,3]:
| 步骤 | fast | slow | nums | 说明 |
|---|---|---|---|---|
| 初始 | 0 | 0 | [1,1,1,2,2,3] | 初始状态 |
| 1 | 0 | 0 | [1,1,1,2,2,3] | slow < 2,保留 nums[0] = 1,slow = 1 |
| 2 | 1 | 1 | [1,1,1,2,2,3] | slow < 2,保留 nums[1] = 1,slow = 2 |
| 3 | 2 | 2 | [1,1,1,2,2,3] | nums[2] = 1 与 nums[slow-2] = 1 相同,不保留,slow 不变 |
| 4 | 3 | 2 | [1,1,2,2,2,3] | nums[3] = 2 与 nums[slow-2] = 1 不同,保留,nums[slow] = 2,slow = 3 |
| 5 | 4 | 3 | [1,1,2,2,2,3] | nums[4] = 2 与 nums[slow-2] = 1 不同,保留,nums[slow] = 2,slow = 4 |
| 6 | 5 | 4 | [1,1,2,2,3,3] | nums[5] = 3 与 nums[slow-2] = 2 不同,保留,nums[slow] = 3,slow = 5 |
| 结束 | - | 5 | [1,1,2,2,3,_] | 返回 slow = 5 作为新数组的长度 |
代码实现
C# 实现
public class Solution {
public int RemoveDuplicates(int[] nums) {
// 处理边界情况
if (nums.Length <= 2) {
return nums.Length;
}
// 初始化慢指针
int slow = 2;
// 从第三个元素开始遍历
for (int fast = 2; fast < nums.Length; fast++) {
// 如果当前元素与 slow-2 位置的元素不同,则保留
if (nums[fast] != nums[slow - 2]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}
Python 实现
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# 处理边界情况
if len(nums) <= 2:
return len(nums)
# 初始化慢指针
slow = 2
# 从第三个元素开始遍历
for fast in range(2, len(nums)):
# 如果当前元素与 slow-2 位置的元素不同,则保留
if nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
return slow
C++ 实现
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
// 处理边界情况
if (nums.size() <= 2) {
return nums.size();
}
// 初始化慢指针
int slow = 2;
// 从第三个元素开始遍历
for (int fast = 2; fast < nums.size(); fast++) {
// 如果当前元素与 slow-2 位置的元素不同,则保留
if (nums[fast] != nums[slow - 2]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
};
执行结果
C# 执行结果
- 执行用时:132 ms,击败了 94.12% 的 C# 提交
- 内存消耗:42.1 MB,击败了 88.24% 的 C# 提交
Python 执行结果
- 执行用时:36 ms,击败了 93.75% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 90.63% 的 Python3 提交
C++ 执行结果
- 执行用时:4 ms,击败了 95.24% 的 C++ 提交
- 内存消耗:10.6 MB,击败了 91.67% 的 C++ 提交
代码亮点
- 双指针技巧:使用双指针技巧,一个指针用于遍历,一个指针用于放置元素,实现了 O(n) 的时间复杂度和 O(1) 的空间复杂度。
- 巧妙的判断条件:通过比较当前元素与
slow-2位置的元素,可以判断当前元素是否是第三个或更多次出现。 - 原地修改:不使用额外的数组空间,直接在原数组上进行修改。
- 边界情况处理:对于长度小于等于 2 的数组,直接返回数组长度,避免了不必要的处理。
- 代码简洁:实现简洁明了,易于理解和维护。
常见错误分析
- 忽略边界情况:对于长度小于等于 2 的数组,应该直接返回数组长度,否则可能会导致索引越界。
- 判断条件错误:判断元素是否应该保留的条件是关键,需要正确比较当前元素与
slow-2位置的元素。 - 指针初始化错误:慢指针应该初始化为 2,因为前两个元素一定会保留。
- 返回值错误:返回值应该是慢指针
slow,而不是数组的长度。 - 没有更新数组:在保留元素时,需要将当前元素放置到
slow位置,并将slow加 1。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 双指针法 | O(n) | O(1) | 原地修改,不使用额外空间 | 无 |
| 计数法 | O(n) | O(n) | 实现简单 | 需要额外的空间 |
| 通用解法(k次) | O(n) | O(1) | 可以扩展到每个元素最多出现k次 | 实现稍复杂 |