LeetCode100天Day12-删除重复项与删除重复项II

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))
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 解题思路](#2.2 解题思路)
      • [2.3 代码实现](#2.3 代码实现)
      • [2.4 代码逐行解释](#2.4 代码逐行解释)
      • [2.5 执行流程详解](#2.5 执行流程详解)
      • [2.6 算法图解](#2.6 算法图解)
      • [2.7 复杂度分析](#2.7 复杂度分析)
      • [2.8 边界情况](#2.8 边界情况)
    • [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 解题思路

这道题使用双指针的方法:

  1. 使用慢指针k指向下一个不重复元素应该放置的位置
  2. 使用快指针i遍历数组
  3. 当遇到不重复的元素时,将其放到k的位置,然后k++
  4. 返回k

解题步骤

  1. 边界检查:如果数组为空,返回0
  2. 初始化k=1,从第二个元素开始检查
  3. 遍历数组,比较当前元素与前一个元素
  4. 如果不重复,将当前元素放到nums[k],k++
  5. 返回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 执行流程详解

示例1nums = [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]

示例2nums = [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 解题思路

这道题使用原地删除的方法:

  1. 遍历数组,检查连续三个元素是否相同
  2. 如果相同,删除第三个元素(移动后面的元素)
  3. 继续检查,直到没有三个连续相同的元素

解题步骤

  1. 边界检查:如果数组长度≤2,直接返回长度
  2. 维护当前有效长度len
  3. 遍历数组,检查是否有三个连续相同元素
  4. 如果有,移动元素并减少len
  5. 返回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 执行流程详解

示例1nums = [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]

示例2nums = [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 原地修改的技巧

原地修改的原则

  1. 使用双指针:一个读,一个写
  2. 写指针永远不超过读指针
  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. 总结

今天我们学习了两道数组去重题目:

  1. 删除有序数组中的重复项:掌握双指针去重,理解原地修改的技巧
  2. 删除有序数组中的重复项II:掌握处理重复次数限制,理解元素移动的方法

核心收获

  • 双指针是处理有序数组去重的高效方法
  • 原地修改可以节省空间,但需要小心操作
  • 比较当前元素和写入位置的元素可以判断是否重复
  • 元素移动的时间复杂度是O(n),嵌套循环会导致O(n²)

练习建议

  1. 尝试用双指针优化删除重复项II到O(n)
  2. 思考如何处理允许重复k次的情况
  3. 尝试不使用双指针,只用循环实现

参考资源

文章标签

#LeetCode #算法 #Java #数组 #双指针

喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!

相关推荐
乘风归趣9 小时前
idea、maven问题
java·maven·intellij-idea
人道领域9 小时前
【零基础学java】(多线程)
java·开发语言
小O的算法实验室9 小时前
2023年IEEE TITS SCI2区TOP,增强遗传算法+分布式随机多无人机协同区域搜索路径规划,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
幽络源小助理9 小时前
springboot基于Java的教学辅助平台源码 – SpringBoot+Vue项目免费下载 | 幽络源
java·vue.js·spring boot
星辰烈龙9 小时前
黑马程序员JavaSE基础加强d6
java·开发语言
亓才孓9 小时前
JUnit--Before,After,Test标签
java·junit·log4j
susu10830189119 小时前
maven-3.9.12的conf配置settings.xml
xml·java·maven
一直都在5729 小时前
MyBatis入门:CRUD、参数处理与防 SQL 注入
java·sql·mybatis
Allen_LVyingbo9 小时前
病历生成与质控编码的工程化范式研究:从模型驱动到系统治理的范式转变
前端·javascript·算法·前端框架·知识图谱·健康医疗·easyui