当前位置: 首页 > news >正文

LeetCode 80:删除排序数组中的重复项 II | 双指针进阶应用

LeetCode 80:删除排序数组中的重复项 II | 双指针进阶应用

引言

删除排序数组中的重复项 II(Remove Duplicates from Sorted Array II)是 LeetCode 第 80 题,属于 Medium 难度,是删除排序数组中重复项的进阶版本。题目要求在原地修改数组,使得每个元素最多出现两次。与简单版本(每个元素最多出现一次)不同,这个进阶版本需要我们仔细处理重复次数的限制,考察对双指针技巧的深入理解。

这道题的核心挑战在于如何在不使用额外空间的情况下,将重复超过两次的元素从数组中移除。由于输入数组是排序的,相同的元素会聚集在一起,这为双指针法的应用提供了便利条件。通过合理设计指针移动策略和边界条件处理,我们可以在单次遍历中完成去重任务。

问题分析

题目描述

给定一个排序数组 nums,原地删除重复出现的元素,使得每个元素最多出现两次。返回删除后数组的新长度。不要使用额外的数组空间,必须在原地修改输入数组并在 O(1) 额外空间下运行。例如输入 [1,1,1,2,2,3],删除后数组变为 [1,1,2,2,3],新长度为 5。

问题特点

这个问题有几个显著特点需要特别注意。首先是"原地修改"的要求,意味着我们不能创建新的数组来存储结果,必须在原数组上进行操作。其次是"最多出现两次"的限制,这比简单版本(最多出现一次)更为复杂,因为我们需要区分一个元素是第一次出现还是第二次出现。最后是排序数组的前提条件,这使得相同元素必然相邻,为双指针法提供了应用基础。

理解"最多出现两次"的含义至关重要。每个元素可以保留两次,但如果有超过两个的相同元素,多余的部分必须被删除。在保留的两个中,哪个位置保留、哪个位置删除是需要仔细考虑的问题。通常的策略是保留第一次和第二次出现的位置,将后续的重复都删除。

与简单版本的对比

LeetCode 第 26 题是简单版本,要求每个元素最多出现一次。简单版本的解法相对直接:遍历数组,当发现当前元素与前一元素不同时,将其移动到结果位置。由于只需要保留一个,我们可以简单地用快指针遍历数组,遇到新元素就将其复制到慢指针位置。

对于最多出现两次的版本,情况会更加复杂。我们需要记录当前元素已经出现了多少次,当第三次出现时开始删除操作。这增加了状态追踪的复杂性,也是本题的难点所在。

双指针法设计

指针角色定义

在解决这个问题的双指针法中,我们定义两个指针:slow 指针指向当前已处理部分(无重复或重复符合要求)的最后一个位置,fast 指针用于遍历整个数组。初始时,slow 指向数组开头(对于非空数组),fast 也指向开头。

算法维护一个关键信息:当前处理的元素已经出现了多少次。由于数组是排序的,当遇到与 nums[slow] 相同的元素时,说明这是重复元素。在简单版本中,一旦遇到不同的元素就直接处理;在进阶版本中,我们需要额外判断这已经是第几次出现。

处理逻辑

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

这段代码展示了双指针法的核心逻辑。当 fast 指向的元素与 nums[slow] 相同时,说明遇到了重复元素。此时如果 count 小于 2(即该元素还没有保留够两次),我们可以将 fast 位置的元素复制到 slow+1 位置,并增加计数。当遇到不同元素时,说明开始处理新的元素,需要将计数重置为 1,并将新元素复制到 slow+1 位置。

优化版本

上述实现虽然正确,但还有一个更优雅的版本,直接基于元素出现次数的判断:

def removeDuplicates_optimized(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

这个优化版本更加简洁。其核心思想是:对于最多出现两次的要求,在位置 slow 处的元素只需要保证它与 slow-2 位置的元素不同即可。因为 slow-2 是 slow 位置前两个位置的元素,如果当前元素与 slow-2 相同,说明这是第三个或更后的重复元素,应该被跳过;如果不同,说明要么是新的元素,要么是第一个或第二个重复,可以保留。

代码实现详解

边界情况处理

首先需要处理数组长度小于等于 2 的情况。对于这样的短数组,无论是否存在重复,每个元素都可以保留(因为最多只需要两个),因此直接返回原数组长度即可。这是代码中第一个 if 语句的作用。

if len(nums) <= 2: return len(nums)

主循环逻辑

主循环从索引 2 开始,这是因为前两个元素无论是否相同都可以保留(最多出现两次的限制对前两个元素没有影响)。对于索引小于 2 的位置,我们不需要任何判断,直接保留原值即可。

循环中,每次检查 nums[fast] 是否与 nums[slow-2] 相同。这里的 slow-2 是关键:它代表在已处理部分中,可能与当前 fast 位置元素重复的最近位置。由于我们只允许每个元素最多出现两次,如果当前元素与 slow-2 位置的元素相同,就说明这是第三个(或更后的)重复,应该被跳过。

复制与更新

当条件满足时(即当前元素与 slow-2 位置的元素不同),我们将 nums[fast] 复制到 nums[slow] 位置,然后将 slow 加 1。这个复制操作实际上是将"保留"的元素移动到正确的位置。由于 fast 总是向前移动,这个过程保证了每个元素最多被检查一次,时间复杂度为 O(n)。

算法复杂度分析

时间复杂度

双指针法的时间复杂度为 O(n),其中 n 是数组长度。fast 指针遍历整个数组一次,每次循环只进行常数次的操作(比较和可能的赋值)。虽然内层没有循环,但由于 fast 和 slow 最多都只移动 n 次,整体时间复杂度仍然是线性的。

空间复杂度

算法的空间复杂度为 O(1),只使用了两个指针变量和少量辅助变量。这满足了题目"必须在 O(1) 额外空间下运行"的要求。无论输入数组有多长,额外的内存使用始终是常数级别的。

更一般化的问题

K次重复限制

将问题扩展到每个元素最多出现 K 次是很自然的。首先考虑如何修改代码以支持任意的 K 值:

def removeDuplicatesK(nums, k): 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

这个通用版本中,我们将判断条件从 nums[slow-2] 改为 nums[slow-k],将初始的 slow 值从 2 改为 k。这样,对于 K=1 的情况,就变成了简单版本(每个元素最多出现一次);对于 K=2 的情况,就是本题的版本。

时间空间权衡

上述方法在时间上是最优的 O(n),空间上是 O(1)。如果允许额外的空间,我们可以使用哈希表来计数每个元素出现的次数,然后重新构建数组。但哈希表方法需要 O(n) 的额外空间,在空间受限的场景下不如双指针法优雅。

测试用例

def test_remove_duplicates(): nums1 = [1, 1, 1, 2, 2, 3] assert removeDuplicates(nums1) == 5 assert nums1[:5] == [1, 1, 2, 2, 3] nums2 = [0, 0, 1, 1, 1, 1, 2, 3, 3] assert removeDuplicates(nums2) == 7 assert nums2[:7] == [0, 0, 1, 1, 2, 3, 3] nums3 = [1, 1, 1] assert removeDuplicates(nums3) == 2 nums4 = [1, 2, 3] assert removeDuplicates(nums4) == 3 nums5 = [] assert removeDuplicates(nums5) == 0 nums6 = [1, 1] assert removeDuplicates(nums6) == 2 print("所有测试用例通过!")

实际应用场景

删除排序数组重复元素的问题在实际开发中有多种应用场景。在数据清洗过程中,经常需要去除重复记录,当数据已排序且只需要保留部分重复时,这类算法非常有用。在数据库查询结果处理中,如果底层已经对结果进行了排序,应用层可能需要进一步处理重复记录。

此外,在实时数据流处理场景中,如果需要维护一个无重复或重复受限的滑动窗口,这类算法可以提供高效的解决方案。在图像处理或信号处理中,去除重复采样点也是常见的预处理步骤。

总结

删除排序数组中的重复项 II 是双指针法的一座重要里程碑,它展示了如何将一个看似简单的去重问题扩展为支持任意重复次数限制的通用算法。通过本文的详细分析,我们深入理解了双指针法在处理这类问题时的灵活性:只需调整比较的位置(从 slow-1 到 slow-k),就可以支持不同的重复次数限制。

这个问题也启示我们,算法设计中的微小变化可能导致问题难度的大幅变化。从每个元素最多出现一次到最多出现两次,虽然只增加了一次的容忍度,但解决方案的设计思路却需要相应调整。深入理解这些变化背后的原理,是提升算法能力的关键。

http://www.jsqmd.com/news/861550/

相关文章:

  • FPGA/ASIC时序约束:从建立保持时间到SDC文件实战指南
  • 军队文职线上培训品牌排行:北京早起点教育文职/北京早起点文职/早起点教育文职/军队文职早起点教育/北京早起点军队文职/选择指南 - 优质品牌商家
  • 基于ZYNQ与IgH的EtherCAT主站方案:软硬协同实现工业实时控制
  • 自动化文件管理:基于Python的网盘批量处理方案
  • WT32-S3-DK开发板全解析:从硬件设计到物联网项目实战
  • FPGA/ASIC时序约束实战:从建立保持时间到SDC语法详解
  • 从USB设备枚举到描述符交互:深入Linux Gadget框架通信机制
  • 树莓派警示灯服务开发:从GPIO控制到RESTful API的完整实现
  • LeetCode 142:环形链表 II | 双指针检测与定位详解
  • AI Agent Harness Engineering 技术选型指南:根据场景选择合适的大模型与框架
  • ops-transformer里的FlashAttention:把注意力矩阵留在片上的秘密
  • AI Agent Harness Engineering 在餐饮行业的应用:智能点餐与库存管理
  • 2026 软考中级《多媒体应用设计师》备考全攻略(附全套资料)
  • 2026年当前宁波环氧地坪企业盘点:深度解析宁波奇元环氧地坪工程有限公司 - 2026年企业推荐榜
  • Simulink电池模块建模:从等效电路到BMS联合仿真实践
  • Windows C/C++文件路径处理:宽字符API、安全实践与常见陷阱
  • 后敏捷时代:从“交付效率”转向“价值探索”的项目管理新范式
  • 找刊网产品体系与功能定位解析
  • 从 0 到 1:10 分钟跑通第一个 Ascend ACL 推理程序
  • STM32F1低功耗模式实战:从睡眠到停止模式的深度优化与避坑指南
  • 基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)
  • Linux内核调试利器:/proc/sysrq-trigger原理与实战指南
  • 提示词失效?Midjourney印象派出图不稳的8大陷阱,资深AIGC架构师逐帧解析SD/MJ风格迁移差异
  • Windows C/C++文件处理实战:编码、路径与API避坑指南
  • 等保测评工程师资料包|从政策到制度,一次性配齐
  • QNX 与 Linux 常用命令和区别(重点:QNX)
  • 振弦采集模块精度检测实战:从原理到环境测试全解析
  • 系统设计 012:从用户系统出发,吃透缓存、数据库与高并发设计
  • 丙午年三月廿九冷暖知
  • 在智能客服系统中集成Taotoken实现多模型路由与成本控制