一、问题描述
1. 核心要求
给定一个升序排列 的数组 nums,需要原地删除重复出现的元素,使得每个元素最多出现两次,返回删除后数组的新长度。
-
空间复杂度要求:O(1)(不能使用额外数组);
-
输入数组以"引用"传递,修改需对调用者可见;
-
无需处理新长度之外的数组元素。
2. 示例分析
| 输入数组 | 输出长度 | 输出数组(前N位) | 说明 |
|---|---|---|---|
| [1,1,1,2,2,3] | 5 | [1,1,2,2,3] | 数字1重复3次,保留2次 |
| [0,0,1,1,1,1,2,3,3] | 7 | [0,0,1,1,2,3,3] | 数字1重复4次,保留2次 |
3. 关键约束
-
数组长度范围:1 ≤ nums.length ≤ 3×10⁴;
-
数组元素范围:-10⁴ ≤ nums[i] ≤ 10⁴;
-
数组已按升序排列(核心前提,可利用有序性简化逻辑)。
二、解题思路
1. 核心逻辑:双指针法
由于数组是有序的,重复元素必然连续出现,因此可以用快慢指针实现原地修改:
-
慢指针(slow):标记新数组的末尾位置(即最终保留的元素的最后一位);
-
快指针(fast):遍历原数组,检查当前元素是否符合"最多出现两次"的条件。
2. 条件判断规则
对于快指针指向的元素 nums[fast],只需判断:
nums[fast] 是否与慢指针前两位的元素 nums[slow-2] 相等。
-
若不相等:说明当前元素可保留,将其赋值给
nums[slow],慢指针右移; -
若相等:说明当前元素已出现两次以上,跳过该元素,快指针继续右移。
3. 边界处理
-
当数组长度 ≤ 2 时,无需删除任何元素,直接返回原长度;
-
慢指针初始值为 2(前两位元素必然保留),快指针从 2 开始遍历。
三、完整代码实现
Python
from typing import List
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# 边界条件:数组长度≤2时,直接返回原长度
if len(nums) <= 2:
return len(nums)
# 慢指针:初始为2(前两位必然保留)
slow = 2
# 快指针:从2开始遍历数组
for fast in range(2, len(nums)):
# 核心判断:当前元素与慢指针前两位元素不同,说明可保留
if nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
# 慢指针的最终位置即为新数组长度
return slow
四、代码核心解析
1. 边界条件处理
Python
if len(nums) <= 2:
return len(nums)
- 当数组长度≤2时,无论元素是否重复,都满足"最多出现两次"的要求,直接返回原长度,避免无效计算。
2. 双指针初始化
Python
slow = 2
for fast in range(2, len(nums)):
-
慢指针
slow初始化为 2:前两位元素(索引0、1)必然保留,无需检查; -
快指针
fast从 2 开始遍历:只需检查第三位及以后的元素。
3. 核心判断与赋值
Python
if nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
-
关键逻辑:利用数组的有序性,只需对比当前元素与新数组倒数第二位(
slow-2)是否相等,即可判断是否超过两次; -
赋值操作:将符合条件的元素"覆盖"到慢指针位置,实现原地修改;
-
慢指针右移:标记新数组的下一个空位。
4. 返回值
Python
return slow
- 慢指针的最终位置即为新数组的长度(因为慢指针始终指向新数组的下一个空位)。
五、测试用例验证
测试用例1:nums = [1,1,1,2,2,3]
| 遍历步骤 | fast | nums[fast] | nums[slow-2] | 条件判断 | slow | nums 数组变化 |
|---|---|---|---|---|---|---|
| 初始 | - | - | - | - | 2 | [1,1,1,2,2,3] |
| 1 | 2 | 1 | nums[0]=1 | 相等 | 2 | 无变化 |
| 2 | 3 | 2 | nums[0]=1 | 不相等 | 3 | [1,1,2,2,2,3] |
| 3 | 4 | 2 | nums[1]=1 | 不相等 | 4 | [1,1,2,2,2,3] |
| 4 | 5 | 3 | nums[2]=2 | 不相等 | 5 | [1,1,2,2,3,3] |
- 最终返回
slow=5,数组前5位为[1,1,2,2,3],符合示例要求。
测试用例2:nums = [0,0,1,1,1,1,2,3,3]
| 遍历步骤 | fast | nums[fast] | nums[slow-2] | 条件判断 | slow | nums 数组变化 |
|---|---|---|---|---|---|---|
| 初始 | - | - | - | - | 2 | [0,0,1,1,1,1,2,3,3] |
| 1 | 2 | 1 | nums[0]=0 | 不相等 | 3 | [0,0,1,1,1,1,2,3,3] |
| 2 | 3 | 1 | nums[1]=0 | 不相等 | 4 | [0,0,1,1,1,1,2,3,3] |
| 3 | 4 | 1 | nums[2]=1 | 相等 | 4 | 无变化 |
| 4 | 5 | 1 | nums[2]=1 | 相等 | 4 | 无变化 |
| 5 | 6 | 2 | nums[2]=1 | 不相等 | 5 | [0,0,1,1,2,1,2,3,3] |
| 6 | 7 | 3 | nums[3]=1 | 不相等 | 6 | [0,0,1,1,2,3,2,3,3] |
| 7 | 8 | 3 | nums[4]=2 | 不相等 | 7 | [0,0,1,1,2,3,3,3,3] |
- 最终返回
slow=7,数组前7位为[0,0,1,1,2,3,3],符合示例要求。
测试用例3:nums = [1,2,3]
-
数组长度=3,slow初始为2,fast遍历2:
- nums[2]=3 ≠ nums[0]=1 → slow=3;
-
返回3,数组无变化,符合预期。
六、复杂度分析
时间复杂度
-
仅遍历数组一次(fast指针从2到len(nums)-1),时间复杂度为 O(n)(n为数组长度);
-
所有操作均为常数级,无嵌套循环,效率最优。
空间复杂度
- 仅使用了slow、fast两个指针变量,无额外数组或容器,空间复杂度为 O(1),满足题目要求。
七、总结
关键点回顾
-
双指针核心:慢指针标记新数组边界,快指针遍历检查,利用有序性简化重复判断;
-
条件简化 :无需统计元素出现次数,只需对比当前元素与新数组倒数第二位(
slow-2),即可判断是否超过两次; -
原地修改:通过覆盖赋值实现O(1)空间复杂度,符合题目核心要求;
-
边界处理:提前处理数组长度≤2的情况,避免逻辑冗余。
该方法是解决"有序数组去重"类问题的经典思路,通过优化判断条件,既保证了逻辑简洁性,又实现了最优的时间和空间复杂度,可推广到"保留k次重复"的通用场景(只需将判断条件中的slow-2改为slow-k)。