1. 问题背景与核心需求
这道算法题来自经典的数组操作场景,要求我们在原地修改有序数组,使得每个元素最多出现两次。举个例子,给定数组[1,1,1,2,2,3],处理后应该返回长度5,且前五个元素变为[1,1,2,2,3]。这类问题在实际开发中经常遇到,比如日志去重、数据清洗等场景。
与基础版(只保留单一元素)相比,这个变体增加了"允许保留两个重复项"的条件,考验我们对指针移动的精确控制能力。题目要求空间复杂度必须为O(1),这意味着不能使用额外数组,必须原地修改。
2. 双指针解法原理剖析
2.1 快慢指针的协同机制
我们采用快指针(fast)和慢指针(slow)两个标记:
- 快指针:遍历数组的侦察兵,负责探测新元素
- 慢指针:重建数组的工程师,决定哪些元素该保留
初始时两个指针都从索引2开始(因为前两个元素无论是否重复都无需处理)。快指针每次循环前进1步,而慢指针只在满足条件时才前进。
2.2 关键比较逻辑
核心判断条件是:nums[fast] != nums[slow-2]。这个精妙的比较实现了:
- 当当前元素与慢指针前两位不同时,说明当前元素出现次数未超限
- 反之则表示已经有两个相同元素,应该跳过当前项
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 slow3. 边界情况与特殊处理
3.1 短数组处理
当数组长度≤2时直接返回原长度,因为:
- 空数组返回0
- 单元素数组必然满足条件
- 两元素数组无论是否相同都符合要求
3.2 全相同元素场景
比如[1,1,1,1,1],算法会正确返回长度2。慢指针会在第三次比较时停止前进,最终保留前两个元素。
4. 复杂度分析与优化空间
4.1 时间复杂度
单次遍历完成操作,时间复杂度为O(n)。这是最优解,因为每个元素至少需要被访问一次。
4.2 空间复杂度
仅使用常数级别的额外空间(几个指针变量),满足O(1)要求。比使用哈希表等方案更节省内存。
5. 同类问题扩展思路
掌握这个解法后,可以轻松应对变种问题:
- 保留k个重复项:只需将判断条件改为
nums[slow-k] - 无序数组去重:先排序再使用相同逻辑
- 链表去重:将数组操作改为链表节点操作
6. 实战中的常见错误
6.1 指针初始位置错误
新手常犯的错误是将双指针都初始化为0。实际上应该从索引2开始,因为前两个元素无论是否重复都无需处理。
6.2 比较对象选择不当
错误的写法如比较nums[fast]和nums[slow],这会漏判某些重复情况。正确的做法是比较nums[fast]和nums[slow-2]。
6.3 返回值混淆
注意题目要求返回的是新长度,而不是修改后的数组。虽然输入的数组会被修改,但函数应该返回的是整数。
7. 测试用例设计建议
完整的测试应该包含:
test_cases = [ ([1,1,1,2,2,3], 5), # 标准情况 ([0,0,1,1,1,1,2,3,3], 7), # 多段重复 ([1,1], 2), # 刚好两个 ([1], 1), # 单个元素 ([], 0), # 空数组 ([1,1,1,1], 2), # 全相同 ([1,2,3,4,5], 5) # 无重复 ]8. 实际工程应用场景
这种算法在以下场景特别有用:
- 时间序列数据压缩:保留足够精度同时减少存储
- 日志去重处理:避免完全去重导致信息丢失
- 数据库查询优化:预处理结果集减少传输量
- 图像处理:去除噪点同时保留细节
在实现这类需求时,我通常会先考虑数据规模。对于小型数组,这个算法已经足够高效。但对于超大型数据(如数GB的日志文件),可能需要结合分块处理技术。