1 题目
给你一个 非严格递增排列 的数组 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;
}
你提供的这段代码是想实现移除数组中重复元素的功能,但存在一些逻辑和语法问题,导致无法正确运行。以下是具体问题分析和修正方案:
问题分析
-
返回值错误 函数声明返回类型为
int,但代码中return nums;试图返回数组指针,类型不匹配,会导致编译错误。通常这类函数需要返回去重后数组的新长度。 -
去重逻辑错误嵌套循环的逻辑存在问题:
- 当发现
nums[i] == nums[j]时,仅用nums[i+1]覆盖nums[i],但未处理后续元素的移动,也没有减少数组长度,会导致重复元素无法真正被移除。 - 循环边界
j < numsSize - 1可能越界(当i是最后一个元素时,i+1会超出原数组范围)。
- 当发现
-
未处理重复元素后的长度变化 去重后数组长度会减小,但代码中始终使用原始
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 题目
给你一个数组 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 <= 1000 <= nums[i] <= 500 <= 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的元素,但存在一些逻辑问题,可能无法正确实现功能。以下是具体分析和修正建议:
问题分析
-
循环变量重复自增 :代码中
for循环已经包含i++,但循环体内又额外执行了i++,导致i每次循环实际增加 2,会跳过一半的元素,无法遍历整个数组。 -
元素覆盖错误 :当遇到
nums[i] == val时,直接用nums[i+1]覆盖nums[i],但未考虑:- 若
i是最后一个元素(i = numsSize - 1),nums[i+1]会访问越界。 - 覆盖后未重新检查当前位置(可能新覆盖的元素仍是
val)。
- 若
-
返回值错误:题目要求返回 "移除后数组的新长度",而代码返回的是 "被移除元素的个数",逻辑相反。
修正方案(双指针法)
推荐使用双指针高效解决,时间复杂度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个元素正确",后续元素无需关心,因此不影响结果。
可以说,"覆盖" 是手段,"复用原空间" 是核心,left和right双指针的配合则是实现这一目标的高效方式 ------ 既满足了原地修改的约束,又达到了筛选元素的目的。