LeetCode100天Day12-删除重复项与删除重复项II:双指针去重与原地修改
摘要:本文详细解析了LeetCode中两道经典数组去重题目------"删除有序数组中的重复项"和"删除有序数组中的重复项II"。通过双指针实现原地去重,以及处理允许重复两次的情况,帮助读者掌握数组原地修改的技巧。
目录
文章目录
- LeetCode100天Day12-删除重复项与删除重复项II:双指针去重与原地修改
-
- 目录
- [1. 删除有序数组中的重复项(Remove Duplicates from Sorted Array)](#1. 删除有序数组中的重复项(Remove Duplicates from Sorted Array))
-
- [1.1 题目描述](#1.1 题目描述)
- [1.2 解题思路](#1.2 解题思路)
- [1.3 代码实现](#1.3 代码实现)
- [1.4 代码逐行解释](#1.4 代码逐行解释)
- [1.5 执行流程详解](#1.5 执行流程详解)
- [1.6 算法图解](#1.6 算法图解)
- [1.7 复杂度分析](#1.7 复杂度分析)
- [1.8 边界情况](#1.8 边界情况)
- [2. 删除有序数组中的重复项II(Remove Duplicates from Sorted Array II)](#2. 删除有序数组中的重复项II(Remove Duplicates from Sorted Array II))
- [3. 两题对比与总结](#3. 两题对比与总结)
-
- [3.1 算法对比](#3.1 算法对比)
- [3.2 双指针去重模板](#3.2 双指针去重模板)
- [3.3 原地修改的技巧](#3.3 原地修改的技巧)
- [3.4 数组元素移动](#3.4 数组元素移动)
- [4. 总结](#4. 总结)
- 参考资源
- 文章标签
1. 删除有序数组中的重复项(Remove Duplicates from Sorted Array)
1.1 题目描述
给你一个 非严格递增排列 的数组 nums,请你 原地 删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k,去重后,返回唯一元素的数量 k。
nums 的前 k 个元素应包含排序后的唯一数字。下标 k - 1 之后的剩余元素可以忽略。
示例 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.2 解题思路
这道题使用双指针的方法:
- 使用慢指针k指向下一个不重复元素应该放置的位置
- 使用快指针i遍历数组
- 当遇到不重复的元素时,将其放到k的位置,然后k++
- 返回k
解题步骤:
- 边界检查:如果数组为空,返回0
- 初始化k=1,从第二个元素开始检查
- 遍历数组,比较当前元素与前一个元素
- 如果不重复,将当前元素放到nums[k],k++
- 返回k
1.3 代码实现
java
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length == 0){
return 0;
}
int k = 1;
for(int i = 1;i < nums.length;i++){
if(nums[i] != nums [i-1]){
nums[k] = nums[i];
k++;
}
}
return k;
}
}
1.4 代码逐行解释
第一部分:边界检查
java
if(nums.length == 0){
return 0;
}
功能:处理空数组的情况
| 输入 | 说明 | 输出 |
|---|---|---|
[] |
空数组 | 0 |
[1] |
单元素数组 | 1 |
第二部分:双指针去重
java
int k = 1;
for(int i = 1;i < nums.length;i++){
if(nums[i] != nums [i-1]){
nums[k] = nums[i];
k++;
}
}
指针说明:
| 指针 | 初始值 | 作用 |
|---|---|---|
k |
1 | 慢指针,指向下一个唯一元素的位置 |
i |
1 | 快指针,遍历数组 |
为什么k从1开始:
数组: [1, 1, 2, 2, 3]
索引: 0 1 2 3 4
nums[0] = 1,第一个元素总是保留的
k从1开始,表示下一个唯一元素应该放在索引1
不重复判断:
java
if(nums[i] != nums [i-1])
| 条件 | 含义 |
|---|---|
nums[i] != nums[i-1] |
当前元素与前一个元素不同 |
1.5 执行流程详解
示例1 :nums = [1,1,2]
初始状态:
nums = [1, 1, 2]
k = 1
i=1:
nums[1] = 1, nums[0] = 1
1 != 1? 否,不操作
k = 1
i=2:
nums[2] = 2, nums[1] = 1
2 != 1? 是
nums[1] = nums[2] = 2
nums = [1, 2, 2]
k = 2
循环结束,返回 2
最终数组前2个元素: [1, 2]
示例2 :nums = [0,0,1,1,1,2,2,3,3,4]
初始状态:
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
k = 1
i=1: nums[1]=0, nums[0]=0
0 != 0? 否
k = 1
i=2: nums[2]=1, nums[1]=0
1 != 0? 是
nums[1] = 1
nums = [0, 1, 1, 1, 1, 2, 2, 3, 3, 4]
k = 2
i=3: nums[3]=1, nums[2]=1
1 != 1? 否
k = 2
i=4: nums[4]=1, nums[3]=1
1 != 1? 否
k = 2
i=5: nums[5]=2, nums[4]=1
2 != 1? 是
nums[2] = 2
nums = [0, 1, 2, 1, 1, 2, 2, 3, 3, 4]
k = 3
i=6: nums[6]=2, nums[5]=2
2 != 2? 否
k = 3
i=7: nums[7]=3, nums[6]=2
3 != 2? 是
nums[3] = 3
nums = [0, 1, 2, 3, 1, 2, 2, 3, 3, 4]
k = 4
i=8: nums[8]=3, nums[7]=3
3 != 3? 否
k = 4
i=9: nums[9]=4, nums[8]=3
4 != 3? 是
nums[4] = 4
nums = [0, 1, 2, 3, 4, 2, 2, 3, 3, 4]
k = 5
循环结束,返回 5
最终数组前5个元素: [0, 1, 2, 3, 4]
1.6 算法图解
初始数组: [1, 1, 2, 2, 3]
索引: 0 1 2 3 4
步骤1: k=1, i=1
数组: [1, 1, 2, 2, 3]
索引: ↑ ↑
k=1 i=1
nums[1] = nums[0]? 1 == 1,重复,跳过
步骤2: k=1, i=2
数组: [1, 1, 2, 2, 3]
索引: ↑ ↑
k=1 i=2
nums[2] = 2, nums[1] = 1
2 != 1,不重复,复制
nums[1] = 2
数组: [1, 2, 2, 2, 3]
k = 2
步骤3: k=2, i=3
数组: [1, 2, 2, 2, 3]
索引: ↑ ↑
k=2 i=3
nums[3] = nums[2]? 2 == 2,重复,跳过
步骤4: k=2, i=4
数组: [1, 2, 2, 2, 3]
索引: ↑ ↑
k=2 i=4
nums[4] = 3, nums[3] = 2
3 != 2,不重复,复制
nums[2] = 3
数组: [1, 2, 3, 2, 3]
k = 3
最终结果: k = 3
数组前3个元素: [1, 2, 3]
1.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 遍历数组一次 |
| 空间复杂度 | O(1) | 只使用常数空间 |
1.8 边界情况
| nums | 说明 | 输出 |
|---|---|---|
[] |
空数组 | 0 |
[1] |
单元素 | 1 |
[1,1,1] |
全部重复 | 1 |
[1,2,3] |
无重复 | 3 |
2. 删除有序数组中的重复项II(Remove Duplicates from Sorted Array II)
2.1 题目描述
给你一个有序数组 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。不需要考虑数组中超出新长度后面的元素。
2.2 解题思路
这道题使用原地删除的方法:
- 遍历数组,检查连续三个元素是否相同
- 如果相同,删除第三个元素(移动后面的元素)
- 继续检查,直到没有三个连续相同的元素
解题步骤:
- 边界检查:如果数组长度≤2,直接返回长度
- 维护当前有效长度len
- 遍历数组,检查是否有三个连续相同元素
- 如果有,移动元素并减少len
- 返回len
2.3 代码实现
java
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length <= 2) return nums.length;
int len = nums.length;
int i = 0;
while (i < len - 2) {
if (nums[i] == nums[i+1] && nums[i] == nums[i+2]) {
// 发现三个连续相同元素,移动后面的元素
for (int k = i+2; k < len-1; k++) {
nums[k] = nums[k+1];
}
len--; // 数组有效长度减1
} else {
i++; // 只有当前三个元素不重复时,才移动指针
}
}
return len;
}
}
2.4 代码逐行解释
第一部分:边界检查
java
if (nums.length <= 2) return nums.length;
功能:数组长度≤2时,最多每个元素出现2次,无需处理
| nums | 长度 | 说明 | 返回值 |
|---|---|---|---|
[] |
0 | 空数组 | 0 |
[1] |
1 | 单元素 | 1 |
[1,1] |
2 | 两元素 | 2 |
[1,1,1] |
3 | 需要处理 | - |
第二部分:遍历检查
java
int len = nums.length;
int i = 0;
while (i < len - 2) {
if (nums[i] == nums[i+1] && nums[i] == nums[i+2]) {
// 处理重复
for (int k = i+2; k < len-1; k++) {
nums[k] = nums[k+1];
}
len--;
} else {
i++;
}
}
变量说明:
| 变量 | 作用 |
|---|---|
len |
当前有效长度 |
i |
当前检查位置 |
为什么是i < len - 2:
数组: [1, 1, 1, 2, 2, 3]
索引: 0 1 2 3 4 5
len = 6
检查i, i+1, i+2
最大i = 3 (检查3, 4, 5)
所以条件是 i < len - 2 (i < 4)
重复检查:
java
if (nums[i] == nums[i+1] && nums[i] == nums[i+2])
| 条件 | 含义 |
|---|---|
nums[i] == nums[i+1] |
第一个和第二个相同 |
nums[i] == nums[i+2] |
第一个和第三个相同 |
| 两者都成立 | 三个连续相同 |
第三部分:移动元素
java
for (int k = i+2; k < len-1; k++) {
nums[k] = nums[k+1];
}
len--;
移动逻辑:
删除nums[i+2],移动后面的元素
原始: [1, 1, 1, 2, 2, 3]
索引: 0 1 2 3 4 5
↑
i+2
删除索引2的元素:
k=2: nums[2] = nums[3] = 2
k=3: nums[3] = nums[4] = 2
k=4: nums[4] = nums[5] = 3
结果: [1, 1, 2, 2, 3, 3]
len = 5 (有效长度)
2.5 执行流程详解
示例1 :nums = [1,1,1,2,2,3]
初始状态:
nums = [1, 1, 1, 2, 2, 3]
len = 6, i = 0
i=0:
nums[0]=1, nums[1]=1, nums[2]=1
1 == 1 && 1 == 1? 是,三个连续相同
删除nums[2]
k=2: nums[2] = nums[3] = 2
k=3: nums[3] = nums[4] = 2
k=4: nums[4] = nums[5] = 3
nums = [1, 1, 2, 2, 3, 3]
len = 5
i不增加
i=0:
nums[0]=1, nums[1]=1, nums[2]=2
1 == 1 && 1 == 2? 否
i = 1
i=1:
nums[1]=1, nums[2]=2, nums[3]=2
1 == 2 && 1 == 2? 否
i = 2
i=2:
nums[2]=2, nums[3]=2, nums[4]=3
2 == 2 && 2 == 3? 否
i = 3
i=3:
i < len - 2? 3 < 3? 否,退出循环
返回 len = 5
最终数组前5个元素: [1, 1, 2, 2, 3]
示例2 :nums = [0,0,1,1,1,1,2,3,3]
初始状态:
nums = [0, 0, 1, 1, 1, 1, 2, 3, 3]
len = 9, i = 0
i=0:
nums[0]=0, nums[1]=0, nums[2]=1
0 == 0 && 0 == 1? 否
i = 1
i=1:
nums[1]=0, nums[2]=1, nums[3]=1
0 == 1 && 0 == 1? 否
i = 2
i=2:
nums[2]=1, nums[3]=1, nums[4]=1
1 == 1 && 1 == 1? 是
删除nums[4]
移动: nums[4]=nums[5]=1, nums[5]=nums[6]=2,
nums[6]=nums[7]=3, nums[7]=nums[8]=3
nums = [0, 0, 1, 1, 1, 2, 3, 3, 3]
len = 8
i=2:
nums[2]=1, nums[3]=1, nums[4]=1
1 == 1 && 1 == 1? 是
删除nums[4]
移动: nums[4]=nums[5]=2, nums[5]=nums[6]=3,
nums[6]=nums[7]=3
nums = [0, 0, 1, 1, 2, 3, 3, 3, 3]
len = 7
i=2:
nums[2]=1, nums[3]=1, nums[4]=2
1 == 1 && 1 == 2? 否
i = 3
i=3:
nums[3]=1, nums[4]=2, nums[5]=3
1 == 2 && 1 == 3? 否
i = 4
i=4:
i < len - 2? 4 < 5? 否,退出循环
返回 len = 7
最终数组前7个元素: [0, 0, 1, 1, 2, 3, 3]
2.6 算法图解
初始数组: [1, 1, 1, 2, 2, 3]
索引: 0 1 2 3 4 5
步骤1: i=0, 检查索引0,1,2
数组: [1, 1, 1, 2, 2, 3]
索引: ↑ ↑ ↑
i=0 i+1 i+2
nums[0] == nums[1] == nums[2]? 1==1==1,是
删除nums[2]
步骤2: 移动元素
原: [1, 1, 1, 2, 2, 3]
0 1 2 3 4 5
删除索引2:
nums[2] = nums[3] = 2
nums[3] = nums[4] = 2
nums[4] = nums[5] = 3
新: [1, 1, 2, 2, 3, 3]
0 1 2 3 4 5
len = 5
步骤3: 继续检查i=0
数组: [1, 1, 2, 2, 3, 3]
索引: ↑ ↑ ↑
i=0 i+1 i+2
nums[0] == nums[1] == nums[2]? 1==1==2,否
i = 1
步骤4: 继续检查i=1
数组: [1, 1, 2, 2, 3, 3]
索引: ↑ ↑ ↑
i=1 i+1 i+2
nums[1] == nums[2] == nums[3]? 1==2==2,否
i = 2
最终结果: len = 5
数组前5个元素: [1, 1, 2, 2, 3]
2.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 最坏情况每次都要移动元素 |
| 空间复杂度 | O(1) | 只使用常数空间 |
优化思路:可以使用双指针优化到O(n)
java
// 优化版本:双指针
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length <= 2) return nums.length;
int k = 2; // 前两个元素总是保留
for (int i = 2; i < nums.length; i++) {
if (nums[i] != nums[k-2]) {
nums[k++] = nums[i];
}
}
return k;
}
}
2.8 边界情况
| nums | 说明 | 输出 |
|---|---|---|
[] |
空数组 | 0 |
[1] |
单元素 | 1 |
[1,1] |
两元素 | 2 |
[1,1,1] |
三个重复 | 2 |
[1,1,1,1] |
四个重复 | 2 |
3. 两题对比与总结
3.1 算法对比
| 对比项 | 删除重复项 | 删除重复项II |
|---|---|---|
| 允许重复次数 | 1次 | 2次 |
| 核心算法 | 双指针 | 原地删除 |
| 时间复杂度 | O(n) | O(n²) |
| 空间复杂度 | O(1) | O(1) |
| 实现难度 | 简单 | 中等 |
3.2 双指针去重模板
java
// 双指针去重模板(允许重复k次)
int k = k; // 前k个元素总是保留
for (int i = k; i < nums.length; i++) {
if (nums[i] != nums[k-k]) { // 比较nums[i]和nums[k-k]
nums[k++] = nums[i];
}
}
return k;
3.3 原地修改的技巧
原地修改的原则:
- 使用双指针:一个读,一个写
- 写指针永远不超过读指针
- 不需要额外空间
java
// 标准双指针模板
int write = 0; // 写指针
for (int read = 0; read < length; read++) { // 读指针
if (满足条件) {
nums[write++] = nums[read];
}
}
return write;
3.4 数组元素移动
java
// 删除指定位置的元素
public void remove(int[] nums, int index, int len) {
for (int i = index; i < len - 1; i++) {
nums[i] = nums[i + 1]; // 后面的元素前移
}
// len--; // 有效长度减1
}
移动示意图:
删除索引2的元素:
原始: [A, B, C, D, E]
0 1 2 3 4
移动后:
[A, B, D, E, E]
0 1 2 3 4
有效长度: 4
4. 总结
今天我们学习了两道数组去重题目:
- 删除有序数组中的重复项:掌握双指针去重,理解原地修改的技巧
- 删除有序数组中的重复项II:掌握处理重复次数限制,理解元素移动的方法
核心收获:
- 双指针是处理有序数组去重的高效方法
- 原地修改可以节省空间,但需要小心操作
- 比较当前元素和写入位置的元素可以判断是否重复
- 元素移动的时间复杂度是O(n),嵌套循环会导致O(n²)
练习建议:
- 尝试用双指针优化删除重复项II到O(n)
- 思考如何处理允许重复k次的情况
- 尝试不使用双指针,只用循环实现
参考资源
文章标签
#LeetCode #算法 #Java #数组 #双指针
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!