在上一篇解决"移除元素"问题时,我们从左右指针过渡到了更简洁的快慢指针解法。而这道"删除有序数组中的重复项",因为数组是非严格递增排列的特性,快慢指针几乎可以直接复用思路,只需要微调判断条件即可。如果你已经掌握了"移除元素"的快慢指针解法,这道题会让你感受到"举一反三"的乐趣。今天就带大家拆解这道题,看看快慢指针是如何顺势解决的。
一、题目回顾:删除有序数组中的重复项
先明确题目核心要求,避免理解偏差:
给你一个非严格递增排列的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。
元素的相对顺序应该保持一致。nums 的前 k 个元素应包含排序后的唯一数字(k 为唯一元素个数),下标 k-1 之后的元素可忽略。
举个例子:
css
输入:nums = [1,1,2] → 输出:2,且 nums 前 2 个元素为 [1,2]
输入:nums = [0,0,1,1,1,2,2,3,3,4] → 输出:5,且 nums 前 5 个元素为 [0,1,2,3,4]
关键提示:数组是非严格递增的,这意味着重复的元素一定是相邻的------这是解题的核心突破口!
二、解题思路:复用快慢指针,微调判断条件
还记得"移除元素"的快慢指针思路吗?慢指针维护有效区域,快指针遍历找有效元素。这道题的核心逻辑完全可以复用这个框架,只是"有效元素"的定义变了:
在"移除元素"中,有效元素是"不等于 val 的元素";而在这道题中,有效元素是"不等于慢指针指向元素的元素"(因为数组有序,重复元素相邻,只要和慢指针位置不同,就是新的唯一元素)。
1. 快慢指针的角色定义
-
slow(慢指针):维护"唯一元素区域"的边界,指向当前唯一元素区域的最后一个元素(注意:这里和"移除元素"的慢指针定义有细微差别,需要重点关注)。
-
fast(快指针):遍历整个数组,负责寻找"新的唯一元素"(即和 slow 指向元素不同的元素)。
2. 核心逻辑拆解
因为数组是有序的,重复元素必相邻,所以我们只需要比较快指针和慢指针指向的元素:
-
初始时,slow 和 fast 都指向数组第一个元素(索引 0)------因为第一个元素一定是唯一的,直接纳入有效区域。
-
快指针 fast 开始遍历数组:
-
如果 nums[fast] !== nums[slow]:说明找到了新的唯一元素,先将 slow 右移一位(因为 slow 原本指向最后一个唯一元素,新元素要放到它后面),再把 nums[fast] 放到 slow 位置。
-
如果 nums[fast] === nums[slow]:说明是重复元素,直接跳过,fast 继续右移。
-
-
遍历结束后,slow 指向的是最后一个唯一元素的索引,所以唯一元素的个数是 slow + 1(因为索引从 0 开始)。
三、代码实现:简洁到只需 5 行核心逻辑
结合上面的思路,我们可以写出非常简洁的代码,几乎是"移除元素"代码的微调:
typescript
function removeDuplicates(nums: number[]): number {
// 因为是递增数组,重复元素必相邻,直接用快慢指针
let slow: number = 0; // 慢指针:指向唯一元素区域的最后一个元素
for (let fast: number = 0; fast < nums.length; fast++) {
// 找到新的唯一元素:和慢指针指向元素不同
if (nums[fast] !== nums[slow]) {
nums[++slow] = nums[fast]; // 先移慢指针,再赋值
}
}
// slow是最后一个唯一元素的索引,个数=索引+1
return slow + 1;
};
3. 代码执行过程演示
我们以「nums = [0,0,1,1,1,2,2,3,3,4]」为例,一步步看代码如何工作:
-
初始状态:slow=0,fast=0,nums=[0,0,1,1,1,2,2,3,3,4]
-
fast=0:nums[0] === nums[0] → 重复,fast++ 变成 1
-
fast=1:nums[1] === nums[0] → 重复,fast++ 变成 2
-
fast=2:nums[2]=1 !== nums[0]=0 → 新唯一元素:
-
slow++ 变成 1
-
nums[1] = nums[2] → nums 变成 [0,1,1,1,1,2,2,3,3,4]
-
fast++ 变成 3
-
-
fast=3:nums[3]=1 === nums[1]=1 → 重复,fast++ 变成 4
-
fast=4:nums[4]=1 === nums[1]=1 → 重复,fast++ 变成 5
-
fast=5:nums[5]=2 !== nums[1]=1 → 新唯一元素:
-
slow++ 变成 2
-
nums[2] = nums[5] → nums 变成 [0,1,2,1,1,2,2,3,3,4]
-
fast++ 变成 6
-
-
后续 fast 继续遍历,依次找到 3、4 两个新唯一元素,最终 slow 变成 4(指向最后一个唯一元素 4)
-
返回 slow + 1 = 5,正好是唯一元素的个数,nums 前 5 位为 [0,1,2,3,4](符合预期)
四、关键注意事项(避坑指南)
这道题的代码虽然简洁,但有几个细节很容易踩坑,尤其是和"移除元素"的慢指针定义对比时:
1. 慢指针的初始位置和定义
❌ 错误做法:把 slow 初始化为 1,或者先赋值再移指针。
css
✅ 正确做法:slow 初始化为 0,指向最后一个唯一元素;找到新元素时,**先移 slow 再赋值**。
原因:如果先赋值再移指针,会覆盖当前的唯一元素(比如初始时 slow=0,直接 nums[slow] = nums[fast] 会重复赋值 0)。
2. 返回值是 slow + 1,不是 slow
这是最容易出错的点!因为 slow 指向的是最后一个唯一元素的索引,而题目要求返回的是"元素个数"。比如 slow=4 时,个数是 5(索引 0-4 共 5 个元素),所以必须加 1。
3. 数组为空的边界处理
如果输入 nums = [],上面的代码会怎么样?
ini
此时 for 循环的 fast 从 0 开始,而 nums.length=0,循环不会执行,直接返回 slow + 1 = 0 + 1 = 1?这就错了!
✅ 修复方案:在代码开头加一句边界判断:
typescript
function removeDuplicates(nums: number[]): number {
if (nums.length === 0) return 0; // 处理空数组边界
let slow: number = 0;
for (let fast: number = 0; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
};
空数组是高频边界测试用例,一定要记得处理!
4. 为什么不用左右指针?
有同学可能会问:上道题的左右指针能不能用在这里?
答案是"可以,但没必要"。因为数组是有序的,重复元素必相邻,快慢指针只需要一次遍历就能解决,逻辑更简洁;而左右指针需要从两端向中间遍历,还要处理元素顺序问题(题目要求保持相对顺序),反而会把简单问题复杂化。
核心原则:有序数组的去重/移除问题,优先考虑快慢指针。
五、与"移除元素"的快慢指针对比
为了帮大家更好地举一反三,这里整理了两道题的快慢指针思路差异,方便对比记忆:
| 对比维度 | 移除元素 | 删除有序数组中的重复项 |
|---|---|---|
| 有效元素定义 | 不等于目标值 val 的元素 | 不等于慢指针指向元素的元素(新唯一元素) |
| 慢指针定义 | 指向有效区域的下一个空位 | 指向有效区域的最后一个元素 |
| 核心操作 | nums[slow++] = nums[fast](先赋值再移指针) | nums[++slow] = nums[fast](先移指针再赋值) |
| 返回值 | slow(直接是有效元素个数) | slow + 1(slow 是最后一个元素索引) |
六、总结
这道"删除有序数组中的重复项",是快慢指针思路的经典应用,核心要点可以总结为 3 句话:
-
利用"有序数组重复元素相邻"的特性,快慢指针一次遍历即可解决;
-
慢指针维护唯一元素区域,快指针找新唯一元素,判断条件是"快慢指针元素不同";
-
注意处理空数组边界,返回值是 slow + 1(不是 slow)。
通过这两道题的练习,相信大家对快慢指针的理解已经更深入了。快慢指针是解决"原地修改数组"问题的核心工具,尤其是在有序数组场景下,几乎是最优解。后续遇到类似的题目(比如"移除有序数组中的重复项 II"),都可以尝试复用这个思路,只需要微调判断条件即可。
最后,祝大家刷题顺利,慢慢体会算法思路"举一反三"的乐趣!