力扣解题-26. 删除有序数组中的重复项

3 阅读6分钟

力扣解题-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 已按 非递减 顺序排列。


第一次解答

解题思路

核心方法:哈希集合去重 + 原地覆盖,利用HashSet的“元素唯一性”特性筛选重复值,再将唯一元素依次写入数组前半部分,逻辑直观但存在额外内存开销,未利用数组“有序”的核心特性。

具体步骤:

  1. 初始化关键变量
    • writeIndex:写指针,初始为0,记录下一个唯一元素的写入位置;
    • sets:HashSet集合,用于存储已出现的元素,实现“去重”判断。
  2. 遍历数组筛选唯一元素
    • 遍历数组每个元素value = nums[m]
    • sets中不包含value(说明是首次出现的唯一元素):
      • value加入sets,标记为已出现;
      • value写入nums[writeIndex],并将writeIndex右移一位;
    • sets中包含value(说明是重复元素):跳过该元素,不做任何操作。
  3. 返回结果:遍历完成后,writeIndex的值即为数组中唯一元素的数量k,直接返回。

性能劣势说明

该解法耗时2ms仅击败9.05%用户,核心问题是未利用数组“有序”的特性,且引入额外内存开销

  1. 时间复杂度的隐性开销:虽然遍历数组是O(n),但HashSet的containsadd操作存在哈希冲突的可能(最坏情况O(n)),且集合的底层操作(如扩容、哈希计算)会增加额外耗时;
  2. 空间复杂度非最优:HashSet需要存储所有唯一元素,空间复杂度为O(n),违背了“原地操作”的最优空间要求(O(1));
  3. 对有序数组的浪费:数组本身是“非递减排列”,重复元素必然相邻,无需通过HashSet判断,仅需对比相邻元素即可实现去重,该解法完全忽略了这一关键条件。

执行耗时:2 ms,击败了9.05% 的Java用户 内存消耗:46.03 MB,击败了56.48% 的Java用户

public int removeDuplicates(int[] nums) {
        int wirteIndex=0;
        Set<Integer> sets=new HashSet<>();
        for(int m=0;m<nums.length;m++){
            int value=nums[m];
            if(!sets.contains(value)){
                sets.add(value);
                nums[wirteIndex]=value;
                wirteIndex++;
            }
        }
        return wirteIndex;
        
    }

第二次解答

解题思路

核心方法:快慢双指针(相邻对比去重),充分利用数组“非递减排列,重复元素必相邻”的特性,通过快慢指针原地覆盖重复元素,时间复杂度O(n)、空间复杂度O(1),是本题的最优解法。

核心原理铺垫

数组非递减排列 → 所有重复元素必然连续出现(如[0,0,1,1]),因此只需对比当前元素与前一个元素是否相等,即可判断是否为重复值:

  • 慢指针(slow):记录“下一个唯一元素的写入位置”,初始为1(数组第一个元素必然唯一,无需处理);
  • 快指针(fast):遍历数组,检查每个元素是否为新的唯一值,初始为1(从第二个元素开始检查)。
具体步骤
  1. 边界处理:获取数组长度n = nums.length(若n=0可直接返回0,代码中隐含处理);
  2. 初始化双指针
    • slow = 1:第一个元素(nums[0])天然唯一,慢指针从第二个位置开始写入;
    • fast = 1:从第二个元素开始遍历检查。
  3. 快指针遍历数组while(fast < n)):
    • 核心判断:nums[fast] != nums[fast-1](当前元素与前一个元素不同,说明是新的唯一元素);
    • 若满足条件:将nums[fast]写入慢指针位置nums[slow],慢指针右移一位(slow++);
    • 无论是否满足条件,快指针均右移一位(fast++),继续检查下一个元素。
  4. 返回结果:遍历完成后,slow的值即为唯一元素的数量k(慢指针的位置等于已写入的唯一元素个数)。
核心优化逻辑说明
  1. 时间复杂度最优:仅一次遍历数组(O(n)),每个元素仅被快指针访问一次,无任何冗余计算,因此耗时0ms击败100%用户;
  2. 空间复杂度极致:仅使用两个指针变量,无额外集合/数组创建,空间复杂度O(1),完全符合“原地操作”的要求;
  3. 利用有序特性的关键:放弃HashSet的通用去重思路,针对性利用“重复元素相邻”的特性,仅通过相邻元素对比实现去重,消除了集合的额外开销;
  4. 内存表现优化:内存消耗45.91MB击败83.74%用户,核心原因是无HashSet的底层存储开销(如数组+链表/红黑树),仅使用基础变量,内存占用降至最低。

执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:45.91 MB,击败了83.74% 的Java用户

public int removeDuplicates(int[] nums) {
        int n=nums.length;
        int slow=1;
        int fast=1;
        while(fast<n){
            if(nums[fast]!=nums[fast-1]){
                nums[slow]=nums[fast];
                slow++;
            }
            fast++;
        }
        return slow;
        
    }

总结

  1. 第一次解答的“HashSet去重”是通用思路,但未利用数组“有序”的特性,引入额外内存和时间开销,性能较差;
  2. 第二次解答的“快慢双指针”是本题的最优解:针对性利用“重复元素相邻”的特性,通过相邻对比实现原地去重,时间/空间复杂度均达到理论最优;
  3. 本题的核心解题技巧:
    • 有序数组的去重优先考虑“相邻对比”,而非通用的哈希去重;
    • 慢指针记录写入位置,快指针负责检查,是数组原地操作的经典双指针模式;
    • 无需处理k之后的元素,只需保证前k个元素为唯一值即可,简化逻辑。