Leetcode 35

1 题目

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

2 代码实现

cpp 复制代码
int removeDuplicates(int* nums, int numsSize) {
   if (numsSize == 0){
    return 0;
   }
   int slow = 0 ;
   for ( int fast = 1 ; fast < numsSize ; fast ++){
    if(nums[fast] != nums [slow]){
        slow ++;
        nums[slow] = nums [fast];
    }
   }
   return slow +1;
}

第一次写的错误百出

cpp 复制代码
int removeDuplicates(int* nums, int numsSize) {
    for(int i = 1 ; i < numsSize; i ++){
        for (int j = 0 ; j < numsSize - 1 ; j ++){
            if(nums[i]== nums[j]){
                nums [i] = nums [i+1] ;
            }
        }
           
    }
    return nums;
}

你提供的这段代码是想实现移除数组中重复元素的功能,但存在一些逻辑和语法问题,导致无法正确运行。以下是具体问题分析和修正方案:

问题分析

  1. 返回值错误 函数声明返回类型为 int,但代码中 return nums; 试图返回数组指针,类型不匹配,会导致编译错误。通常这类函数需要返回去重后数组的新长度。

  2. 去重逻辑错误嵌套循环的逻辑存在问题:

    • 当发现 nums[i] == nums[j] 时,仅用 nums[i+1] 覆盖 nums[i],但未处理后续元素的移动,也没有减少数组长度,会导致重复元素无法真正被移除。
    • 循环边界 j < numsSize - 1 可能越界(当 i 是最后一个元素时,i+1 会超出原数组范围)。
  3. 未处理重复元素后的长度变化 去重后数组长度会减小,但代码中始终使用原始 numsSize 作为循环边界,导致无效元素仍被处理。

修正方案(双指针法)

移除有序数组中的重复元素(假设题目是有序数组,否则需先排序),最优方法是用双指针

  • 慢指针 slow 指向去重后数组的末尾。
  • 快指针 fast 遍历整个数组,找到与 slow 指向不同的元素时,更新 slow 并赋值。
cpp 复制代码
int removeDuplicates(int* nums, int numsSize) {
    if (numsSize == 0) { // 处理空数组特殊情况
        return 0;
    }
    int slow = 0; // 慢指针,指向去重后最后一个元素
    for (int fast = 1; fast < numsSize; fast++) {
        if (nums[fast] != nums[slow]) { // 找到不重复的元素
            slow++; // 慢指针前移
            nums[slow] = nums[fast]; // 覆盖重复位置
        }
    }
    return slow + 1; // 新长度为慢指针索引+1
}

说明

  • 该方法仅适用于有序数组 (题目通常默认有序,如 LeetCode 第 26 题),如果数组无序,需先通过 qsort 排序再去重。
  • 时间复杂度 O(n),空间复杂度 O(1),效率远高于嵌套循环(嵌套循环时间复杂度 O(n²))。
  • 函数返回去重后的新长度,原数组前 slow+1 个元素即为去重后的结果。

3 题目

27. 移除元素

给你一个数组 nums和一个值 val,你需要 原地 移除所有数值等于 val的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

用户评测:

评测机将使用以下代码测试您的解决方案:

复制代码
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
                            // 它以不等于 val 的值排序。

int k = removeElement(nums, val); // 调用你的实现

assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < actualLength; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有的断言都通过,你的解决方案将会 通过

示例 1:

复制代码
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

复制代码
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

4 代码实现

cpp 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int count = 0 ;
    for (int  i = 0 ; i < numsSize ; i ++){
        if(nums [i] != val){
            nums[count] = nums[i]; 
            count++;
        }
    
    }
    return count;
}

错误百出

cpp 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int count = 0 ;
    for (int  i = 0 ; i < numsSize ; i ++){
        if(nums [i] == val){
            count++;
            nums[i] = nums[i + 1]; 
        }
        i++;
    }
    return count;
}

你提供的这段代码用于移除数组中等于目标值val的元素,但存在一些逻辑问题,可能无法正确实现功能。以下是具体分析和修正建议:

问题分析

  1. 循环变量重复自增 :代码中for循环已经包含i++,但循环体内又额外执行了i++,导致i每次循环实际增加 2,会跳过一半的元素,无法遍历整个数组。

  2. 元素覆盖错误 :当遇到nums[i] == val时,直接用nums[i+1]覆盖nums[i],但未考虑:

    • i是最后一个元素(i = numsSize - 1),nums[i+1]会访问越界。
    • 覆盖后未重新检查当前位置(可能新覆盖的元素仍是val)。
  3. 返回值错误:题目要求返回 "移除后数组的新长度",而代码返回的是 "被移除元素的个数",逻辑相反。

修正方案(双指针法)

推荐使用双指针高效解决,时间复杂度O(n),空间复杂度O(1)

cpp 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int left = 0; // 慢指针:指向新数组的末尾
    for (int right = 0; right < numsSize; right++) {
        if (nums[right] != val) {
            // 若当前元素不等于val,放入新数组
            nums[left] = nums[right];
            left++; // 新数组长度+1
        }
    }
    return left; // left即为新数组的长度
}

修正说明

  • 双指针逻辑
    • right(快指针)遍历整个原数组,寻找不等于val的元素。
    • left(慢指针)记录新数组的有效长度,只在right找到有效元素时移动,并将元素覆盖到left位置。
  • 返回值left最终等于新数组中有效元素的个数(即移除后数组的长度)。
  • 优势:避免了重复遍历和越界问题,高效完成元素移除(覆盖式)。

例如,对于输入nums = [3,2,2,3], val = 3,修正后的代码会返回2,且nums前 2 个元素为[2,2],符合预期。

【我发现这种原地修改的循环约束条件好像通常是!=的条件下?】

你观察得很对!在原地修改数组的场景中(比如移除元素、筛选元素等),循环里确实经常用 != 作为核心判断条件 ,这本质上是由 "保留有效元素" 的目标决定的。

为什么常用 != 条件?

原地修改的核心需求通常是:保留 "符合要求的元素",剔除 "不符合要求的元素" 。这里的 "符合要求" 往往就是 "不等于某个值"(比如移除特定值val时,"有效元素" 就是!= val的元素)。

以双指针法为例:

  • 快指针right负责遍历所有元素,一旦遇到!= val的元素(即 "有效元素") ,就需要把它 "保留" 到慢指针left的位置。
  • 慢指针left只在快指针找到有效元素时才移动,最终left的位置就是有效元素的个数。

这种逻辑下,!= 条件直接对应了 "需要保留该元素" 的判断,是最直接、高效的写法。

反过来想:如果用 == 会怎样?

如果用 == val 作为条件,逻辑就会变成 "处理需要移除的元素",但这样容易出现问题:

  • 比如你最初的代码,遇到== val时试图用后一个元素覆盖,但会漏掉连续的val(比如[2,val,val,3],第一个val被覆盖后,第二个val可能没被处理)。
  • 还可能导致数组越界(当val在数组末尾时)。

因此,与其 "盯着要移除的元素",不如 "盯着要保留的元素"(用!=),逻辑更清晰,也更少出错。

总结

原地修改的核心是 "保留有效元素 ",而有效元素的判定往往就是 "不等于某个值",因此 != 会成为最常用的条件。这种思路不仅适用于移除元素,还适用于很多类似场景(比如筛选偶数、提取特定范围的数等),本质上都是 "留下符合条件的,丢掉不符合的"。

【我感觉更像是一个新的数组在覆盖,这个left看到要留下的元素就覆盖了,原地修改核心应该还是因为没有开辟新的存储空间】

你说得非常准确!原地修改的核心本质确实是**"在不额外开辟新数组空间" 的前提下,通过覆盖原数组元素来模拟 "新数组" 的构建** ,而left指针就相当于这个 "新数组" 的索引。

具体来说:

  • 原数组的内存空间被重复利用,left指针之前的部分([0, left-1])就是 "新数组" 的有效内容,left之后的元素则是待处理或已废弃的部分。
  • right指针遍历到需要保留的元素(!= val)时,就把它 "复制" 到left的位置 ------ 这一步看似是 "覆盖" 原数组,实际是在原空间里构建新数组,left每增加 1,就意味着新数组的长度增加 1。

这种方式的优势很明显:

  • 空间复杂度降到O(1),不需要额外申请与原数组同规模的内存(尤其适合处理大规模数据时节省资源)。
  • 虽然原数组中left之后的元素可能还残留旧值,但题目通常只要求 "新数组长度" 和 "前left个元素正确",后续元素无需关心,因此不影响结果。

可以说,"覆盖" 是手段,"复用原空间" 是核心,leftright双指针的配合则是实现这一目标的高效方式 ------ 既满足了原地修改的约束,又达到了筛选元素的目的。

相关推荐
码农多耕地呗3 小时前
力扣101.对称二叉树(java)
算法·leetcode
小龙报3 小时前
《算法通关指南之C++编程篇(5)----- 条件判断与循环(下)》
c语言·开发语言·c++·算法·visualstudio·学习方法·visual studio
郝学胜-神的一滴3 小时前
C++ STL(标准模板库)深度解析:从基础到实践
linux·服务器·开发语言·c++·算法
Fortunate Chen4 小时前
初识C语言12. 结构体(自定义类型的核心工具)
c语言·开发语言·笔记
刚入坑的新人编程4 小时前
算法训练.17
开发语言·数据结构·c++·算法
米饭不加菜4 小时前
typora的基本用法
笔记
白羊无名小猪4 小时前
排序算法总结
算法·排序算法
piggy侠4 小时前
百度PaddleOCR-VL:基于0.9B超紧凑视觉语言模型,支持109种语言,性能超越GPT-4o等大模型
人工智能·算法·机器学习
xiaoxiaoxiaolll4 小时前
封面论文丨薄膜铌酸锂平台实现强耦合电光调制,《Light Sci. Appl. 》报道机器学习优化新范式
人工智能·学习