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 代码实现
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;
}
第一次写的错误百出
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
并赋值。
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 代码实现
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;
}
错误百出
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)
:
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
双指针的配合则是实现这一目标的高效方式 —— 既满足了原地修改的约束,又达到了筛选元素的目的。