1. 题目解析与核心思路
这道LeetCode经典题目要求我们在原地修改有序数组,删除重复出现的元素,并返回新数组的长度。题目看似简单,但考察了以下几个关键点:
- 数组操作的基本功
- 双指针技巧的灵活运用
- 边界条件的处理能力
1.1 题目具体要求
给定一个升序排列的数组nums,我们需要:
- 原地删除重复出现的元素
- 使每个元素只出现一次
- 返回删除后数组的新长度
- 必须使用O(1)的额外空间
注意:题目要求原地修改数组,这意味着不能使用额外的数组来存储结果,这也是这道题的难点所在。
1.2 双指针解法原理
双指针法是解决这类数组操作问题的利器。具体思路是:
- 使用一个慢指针(slow)指向当前不重复序列的末尾
- 使用一个快指针(fast)遍历整个数组
- 当发现nums[fast] ≠ nums[slow]时,将nums[fast]复制到nums[slow+1]
- 最后返回slow+1即为新数组长度
这种方法的精妙之处在于:
- 时间复杂度O(n),只需遍历一次数组
- 空间复杂度O(1),没有使用额外空间
- 保持了数组元素的原始顺序
2. 详细实现与代码解析
2.1 C++实现版本
int removeDuplicates(vector<int>& nums) { if(nums.empty()) return 0; int slow = 0; for(int fast = 1; fast < nums.size(); ++fast) { if(nums[fast] != nums[slow]) { nums[++slow] = nums[fast]; } } return slow + 1; }关键点解析:
- 首先处理空数组的特殊情况
- slow初始化为0,fast从1开始遍历
- 当发现不相等元素时,slow先自增再赋值
- 最终返回slow+1是因为数组索引从0开始
2.2 Python实现版本
def removeDuplicates(nums): if not nums: return 0 slow = 0 for fast in range(1, len(nums)): if nums[fast] != nums[slow]: slow += 1 nums[slow] = nums[fast] return slow + 1Python版本与C++逻辑完全一致,只是语法上的差异。注意Python中列表是可变的,可以直接修改。
2.3 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 空数组输入:直接返回0
- 单元素数组:无需处理,直接返回1
- 所有元素相同:只需要保留第一个元素
- 大数组测试:确保算法效率
3. 复杂度分析与优化
3.1 时间复杂度分析
- 最佳情况:O(1)(空数组或单元素数组)
- 最坏情况:O(n)(需要完整遍历数组)
- 平均情况:O(n)
由于我们只遍历数组一次,没有嵌套循环,时间复杂度是线性的。
3.2 空间复杂度分析
- 额外空间使用:O(1)
- 只使用了常数个额外变量(slow和fast)
这完全符合题目要求的原地修改条件。
3.3 可能的优化方向
虽然标准解法已经很高效,但仍有微优化空间:
- 提前终止:当fast到达数组末尾时可以提前结束循环
- 减少赋值操作:当fast - slow > 1时才进行赋值
- 使用while循环:某些情况下可能比for循环更高效
不过这些优化带来的性能提升通常很小,在LeetCode评测系统中可能看不出明显差异。
4. 常见错误与调试技巧
4.1 新手常见错误
- 忘记处理空数组情况
- slow指针初始化为1(应该是0)
- 返回slow而不是slow+1
- 使用额外数组存储结果(违反题目要求)
- 比较nums[fast]和nums[fast-1](逻辑错误)
4.2 调试技巧
当你的代码不能通过测试用例时:
- 打印指针位置和数组状态:
print(f"fast={fast}, slow={slow}, nums={nums}")- 使用小型测试用例手动模拟:
- 输入:[1,1,2]
- 预期输出:2(数组变为[1,2,_])
- 检查边界条件:
- 空数组[]
- 单元素数组[1]
- 全相同数组[1,1,1]
4.3 单元测试用例推荐
完善的测试用例应该包含:
test_cases = [ ([], 0), ([1], 1), ([1,1,2], 2), ([0,0,1,1,1,2,2,3,3,4], 5), ([1,1,1,1,1], 1), ([1,2,3,4,5], 5) ]5. 双指针技巧的扩展应用
这道题展示的双指针技巧可以应用于许多类似场景:
5.1 类似题目推荐
- LeetCode 27. 移除元素
- LeetCode 80. 删除有序数组中的重复项 II
- LeetCode 283. 移动零
- LeetCode 844. 比较含退格的字符串
5.2 双指针模式总结
双指针主要有以下几种使用模式:
- 前后指针:一个从头部开始,一个从尾部开始
- 快慢指针:以不同速度遍历
- 滑动窗口:维护一个满足条件的窗口
本题属于快慢指针的典型应用,掌握这种模式可以解决一大类数组操作问题。
5.3 实际工程应用
虽然这是一道算法题,但类似的思路在实际工程中也有应用:
- 日志去重处理
- 数据库记录清理
- 大数据集的流式处理
- 内存优化场景下的数据整理
6. 不同语言实现的注意事项
6.1 Java实现
public int removeDuplicates(int[] nums) { if(nums.length == 0) return 0; int slow = 0; for(int fast = 1; fast < nums.length; fast++) { if(nums[fast] != nums[slow]) { nums[++slow] = nums[fast]; } } return slow + 1; }Java注意事项:
- 数组长度使用nums.length
- 注意数组越界问题
- 方法签名要正确
6.2 JavaScript实现
function removeDuplicates(nums) { if(nums.length === 0) return 0; let slow = 0; for(let fast = 1; fast < nums.length; fast++) { if(nums[fast] !== nums[slow]) { nums[++slow] = nums[fast]; } } return slow + 1; }JS注意事项:
- 使用严格相等运算符!==
- 变量声明使用let/const
- 数组是对象,可以修改
6.3 Go实现
func removeDuplicates(nums []int) int { if len(nums) == 0 { return 0 } slow := 0 for fast := 1; fast < len(nums); fast++ { if nums[fast] != nums[slow] { slow++ nums[slow] = nums[fast] } } return slow + 1 }Go注意事项:
- 切片是引用类型
- 使用len()获取长度
- 语法简洁,没有++slow这种写法
7. 算法可视化与理解
为了更好理解双指针的工作方式,我们可以用以下例子演示:
初始数组:[0,0,1,1,1,2,2,3,3,4]
执行过程:
- slow=0, fast=1: 0=0 → 不操作
- slow=0, fast=2: 0≠1 → nums[1]=1 → [0,1,1,1,1,2,2,3,3,4]
- slow=1, fast=3: 1=1 → 不操作
- slow=1, fast=4: 1=1 → 不操作
- slow=1, fast=5: 1≠2 → nums[2]=2 → [0,1,2,1,1,2,2,3,3,4]
- slow=2, fast=6: 2=2 → 不操作
- slow=2, fast=7: 2≠3 → nums[3]=3 → [0,1,2,3,1,2,2,3,3,4]
- slow=3, fast=8: 3=3 → 不操作
- slow=3, fast=9: 3≠4 → nums[4]=4 → [0,1,2,3,4,2,2,3,3,4]
最终返回slow+1=5,前5个元素[0,1,2,3,4]就是去重后的结果。
8. 进阶思考与扩展
8.1 如果数组未排序怎么办?
对于无序数组,去重需要不同的方法:
- 先排序再使用双指针(O(nlogn)时间)
- 使用哈希表记录已出现元素(O(n)时间但需要额外空间)
8.2 允许最多保留k个重复项
这是LeetCode 80题的变种,解法思路类似:
- 比较nums[fast]和nums[slow-k]
- 当不相等时才进行赋值操作
8.3 并行化处理的可能性
对于超大数组,可以考虑:
- 分段处理
- 多线程/多进程并行
- MapReduce模式
不过这些方法通常需要额外空间,不符合本题要求。
9. 面试中的考察点
这道题在面试中经常出现,面试官可能关注:
- 能否正确理解题目要求(特别是原地修改)
- 双指针思路的清晰表达
- 边界条件的处理
- 代码的简洁性和可读性
- 时间/空间复杂度的分析能力
建议在面试中:
- 先确认理解题意
- 举例说明思路
- 写出代码后主动检查边界条件
- 讨论可能的优化
10. 个人实战经验分享
在实际刷题过程中,我发现以下几点特别重要:
- 初始条件设置:slow从0开始还是1开始容易混淆
- 赋值时机:是先++slow还是先赋值要清楚
- 返回值:记住数组长度是索引+1
- 测试用例:一定要测试全相同和全不同的极端情况
一个容易忽略的细节是,当数组已经无重复时,我们的算法仍然会进行不必要的赋值操作。虽然不影响正确性,但在性能敏感的场景可能需要优化。