☰
双指针算法解决有序数组去重问题
2026/11/2 17:10:55 网站建设 项目流程

1. 题目背景与核心需求

力扣第80题"删除有序数组中的重复项Ⅱ"是数组操作类经典题目,属于"双指针"算法的典型应用场景。题目要求在一个非严格递增排列的数组中,原地删除重复出现的元素,使得每个元素最多出现两次,返回删除后数组的新长度。

与简单版(力扣第26题)的区别在于,本题允许元素最多重复两次而非完全去重。这种设定更贴近实际开发场景——比如日志系统中需要保留最近两次相同错误记录,或用户行为分析中保留关键操作的最近两次触发时间。

2. 解题思路分析与算法选择

2.1 暴力解法的时间复杂度问题

最直观的解法是使用额外数组存储结果,遍历原数组时检查当前元素是否已在结果数组中出现两次。但这种方法:

  • 空间复杂度O(n),不符合"原地修改"要求
  • 每次插入都需要遍历结果数组检查重复次数,时间复杂度恶化至O(n²)

2.2 双指针算法的优势

采用快慢指针可以在O(n)时间、O(1)空间内解决问题:

  • 慢指针(slow):指向下一个待写入位置
  • 快指针(fast):遍历数组的当前位置

关键判断条件:

  1. 当nums[fast] != nums[slow-2]时,说明当前元素未重复两次以上
  2. 将nums[fast]写入nums[slow]位置,然后slow前移

这种解法只需一次遍历,且无需额外存储空间,完美符合题目要求。

3. 详细实现步骤与代码解析

3.1 Python标准实现

def removeDuplicates(nums): if len(nums) <= 2: return len(nums) slow = 2 for fast in range(2, len(nums)): if nums[fast] != nums[slow-2]: nums[slow] = nums[fast] slow += 1 return slow

代码要点说明:

  1. 边界处理:数组长度≤2时直接返回
  2. 初始化slow=2,因为前两个元素无论是否重复都符合要求
  3. 从fast=2开始遍历,比较nums[fast]与nums[slow-2]

3.2 关键步骤可视化

以nums = [1,1,1,2,2,3]为例:

fastslownums状态判断条件操作
22[1,1,1,2,2,3]nums[2]==nums[0]不写入
32[1,1,1,2,2,3]nums[3]!=nums[0]nums[2]=2, slow=3
43[1,1,2,2,2,3]nums[4]!=nums[1]nums[3]=2, slow=4
54[1,1,2,2,2,3]nums[5]!=nums[2]nums[4]=3, slow=5

最终返回slow=5,数组前5位为[1,1,2,2,3]

4. 边界条件与特殊测试用例

4.1 必须考虑的边界情况

  1. 空数组:[]→ 返回0
  2. 单元素数组:[1]→ 返回1
  3. 双元素相同:[1,1]→ 返回2
  4. 全相同元素:[1,1,1,1]→ 返回2
  5. 无需删除:[1,2,3,4]→ 返回4

4.2 测试用例设计技巧

test_cases = [ ([], 0), ([1], 1), ([1,1], 2), ([1,1,1], 2), ([1,2,2,2,3,3], 5), ([0,0,1,1,1,1,2,3,3], 7) ]

建议在IDE中逐步调试观察指针变化,这对理解双指针移动逻辑至关重要。

5. 算法优化与变种思考

5.1 通用化解决方案

将最大重复次数抽象为参数k:

def removeDuplicates(nums, k=2): if len(nums) <= k: return len(nums) slow = k for fast in range(k, len(nums)): if nums[fast] != nums[slow-k]: nums[slow] = nums[fast] slow += 1 return slow

5.2 相关题目拓展

  1. 力扣26题(完全去重):将k设为1即可
  2. 力扣27题(移除元素):双指针变种,判断条件改为元素值不等于给定值
  3. 力扣283题(移动零):将特定值(零)移动到末尾的特殊情况

6. 常见错误与调试技巧

6.1 典型错误模式

  1. 指针初始化错误:

    • 错误:slow = 0→ 会导致前k个元素被错误覆盖
    • 正确:slow = k(保留前k个元素的合法性)
  2. 比较对象错误:

    • 错误:比较nums[fast]与nums[slow-1]
    • 正确:比较nums[fast]与nums[slow-k]
  3. 边界条件遗漏:

    • 未处理数组长度小于k的情况
    • 未考虑全相同元素的特殊情况

6.2 调试建议

  1. 使用小规模测试用例(长度3-5)逐步跟踪指针变化
  2. 打印每次循环后的数组状态:
    print(f"fast={fast}, slow={slow}, nums={nums}")
  3. 对特殊用例单独测试,特别是全相同和全不同两种情况

7. 实际工程应用场景

这种去重算法在以下场景有实际应用价值:

  1. 时间序列数据处理:

    • 传感器数据采集时,去除连续重复的异常值
    • 股票交易记录中,合并连续相同的价格变动
  2. 日志系统优化:

    • 合并连续的相同错误日志
    • 保留重要操作的最近两次记录
  3. 数据库操作:

    • 批量插入前的数据去重处理
    • 结果集的后处理优化

8. 性能对比与复杂度分析

方法时间复杂度空间复杂度是否原地修改
暴力解法O(n²)O(n)否
哈希表计数O(n)O(n)否
双指针(本文)O(n)O(1)是

实测对比(100万元素数组):

  • 双指针:约120ms
  • 暴力解法:超时(>10s)
  • 哈希表:约450ms(因额外空间分配)

9. 不同语言实现要点

9.1 Java实现注意点

public int removeDuplicates(int[] nums) { if (nums.length <= 2) return nums.length; int slow = 2; for (int fast = 2; fast < nums.length; fast++) { if (nums[fast] != nums[slow-2]) { nums[slow++] = nums[fast]; } } return slow; }

特别注意:

  • 数组长度通过nums.length获取
  • slow++实现了先赋值后自增

9.2 C++实现优化

int removeDuplicates(vector<int>& nums) { const int n = nums.size(); if (n <= 2) return n; int slow = 2; for (int fast = 2; fast < n; ++fast) { if (nums[fast] != nums[slow-2]) { nums[slow++] = nums[fast]; } } return slow; }

性能优化点:

  • 提前存储n = nums.size()避免多次调用
  • 前置++运算符效率略高于后置++

10. 刷题方法论建议

  1. 同类题目集中练习:

    • 连续刷双指针相关题目(26,27,80,283题)
    • 比较各题解法的异同点
  2. 解题步骤标准化:

    • 先厘清题目条件和要求
    • 画图模拟简单测试用例
    • 再着手编写代码
  3. 反复练习价值:

    • 第一遍:理解思路
    • 第二遍:独立实现
    • 第三遍:优化代码
    • 第四遍:尝试变种

这种分阶段练习法能有效提升算法思维和编码能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询