LeetCode 26.删除有序数组中的重复项:快慢指针的顺势应用

在上一篇解决"移除元素"问题时,我们从左右指针过渡到了更简洁的快慢指针解法。而这道"删除有序数组中的重复项",因为数组是非严格递增排列的特性,快慢指针几乎可以直接复用思路,只需要微调判断条件即可。如果你已经掌握了"移除元素"的快慢指针解法,这道题会让你感受到"举一反三"的乐趣。今天就带大家拆解这道题,看看快慢指针是如何顺势解决的。

一、题目回顾:删除有序数组中的重复项

先明确题目核心要求,避免理解偏差:

给你一个非严格递增排列的数组 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. 核心逻辑拆解

因为数组是有序的,重复元素必相邻,所以我们只需要比较快指针和慢指针指向的元素:

  1. 初始时,slow 和 fast 都指向数组第一个元素(索引 0)------因为第一个元素一定是唯一的,直接纳入有效区域。

  2. 快指针 fast 开始遍历数组:

    • 如果 nums[fast] !== nums[slow]:说明找到了新的唯一元素,先将 slow 右移一位(因为 slow 原本指向最后一个唯一元素,新元素要放到它后面),再把 nums[fast] 放到 slow 位置。

    • 如果 nums[fast] === nums[slow]:说明是重复元素,直接跳过,fast 继续右移。

  3. 遍历结束后,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]」为例,一步步看代码如何工作:

  1. 初始状态:slow=0,fast=0,nums=[0,0,1,1,1,2,2,3,3,4]

  2. fast=0:nums[0] === nums[0] → 重复,fast++ 变成 1

  3. fast=1:nums[1] === nums[0] → 重复,fast++ 变成 2

  4. 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

  5. fast=3:nums[3]=1 === nums[1]=1 → 重复,fast++ 变成 4

  6. fast=4:nums[4]=1 === nums[1]=1 → 重复,fast++ 变成 5

  7. 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

  8. 后续 fast 继续遍历,依次找到 3、4 两个新唯一元素,最终 slow 变成 4(指向最后一个唯一元素 4)

  9. 返回 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 句话:

  1. 利用"有序数组重复元素相邻"的特性,快慢指针一次遍历即可解决;

  2. 慢指针维护唯一元素区域,快指针找新唯一元素,判断条件是"快慢指针元素不同";

  3. 注意处理空数组边界,返回值是 slow + 1(不是 slow)。

通过这两道题的练习,相信大家对快慢指针的理解已经更深入了。快慢指针是解决"原地修改数组"问题的核心工具,尤其是在有序数组场景下,几乎是最优解。后续遇到类似的题目(比如"移除有序数组中的重复项 II"),都可以尝试复用这个思路,只需要微调判断条件即可。

最后,祝大家刷题顺利,慢慢体会算法思路"举一反三"的乐趣!

相关推荐
同学807962 小时前
H5实现网络信号检测全解析(附源码)
前端·javascript
不想秃头的程序员2 小时前
Vue 与 React 数据体系深度对比
前端·vue.js
前端流一2 小时前
[疑难杂症] 浏览器集成 browser-use 踩坑记录
前端·node.js
谷哥的小弟2 小时前
HTML5新手练习项目—ToDo清单(附源码)
前端·源码·html5·项目
pusheng20252 小时前
地下车库一氧化碳监测的技术挑战与解决方案
前端·安全
ResponseState2003 小时前
安卓原生写uniapp插件手把手教学调试、打包、发布。
前端·uni-app
颜酱3 小时前
SourceMap 深度解析:从映射原理到线上监控落地
前端·javascript
LYOBOYI1233 小时前
qt的事件传播机制
java·前端·qt
IT_陈寒3 小时前
Python 3.12 性能优化:5 个鲜为人知但提升显著的技巧让你的代码快如闪电
前端·人工智能·后端