1. 问题背景与需求分析
在算法面试和日常编程中,处理有序数组的去重操作是一个经典问题。LeetCode第26题"删除有序数组中的重复项"要求我们原地修改输入数组,使得每个元素只出现一次,并返回新的长度。
这个问题看似简单,但考察了几个关键编程能力:
- 对数组数据结构的理解
- 双指针算法的应用
- 原地修改数组的技巧
- 边界条件的处理能力
实际业务场景中,类似操作也很常见。比如:
- 处理日志数据时去除连续重复条目
- 数据库查询结果去重
- 时间序列数据的清洗
2. 问题描述与示例
给定一个非严格递增排列的数组nums,原地删除重复出现的元素,使每个元素只出现一次。返回删除后数组的新长度。
要求:
- 必须原地修改输入数组
- 使用O(1)额外空间
- 元素的相对顺序必须保持一致
示例1: 输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应返回长度2,原数组前两个元素修改为1和2
示例2: 输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4,,,,,_]
3. 双指针解法详解
3.1 基本思路
最直观的解法是使用双指针技术:
- 慢指针(slow)指向当前唯一元素的最后一个位置
- 快指针(fast)遍历数组寻找下一个不重复元素
- 当找到不重复元素时,将其复制到slow+1位置,然后slow前进
这种方法的优势在于:
- 时间复杂度O(n),只需遍历一次数组
- 空间复杂度O(1),只使用了常数个额外变量
- 保持了元素的原始顺序
3.2 代码实现
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 + 13.3 执行过程分析
以nums = [0,0,1,1,1,2,2,3,3,4]为例:
初始状态: slow=0, fast=1 nums[1]=0 == nums[0]=0 → 不操作
fast=2: nums[2]=1 != nums[0]=0 → slow=1, nums[1]=1 数组变为[0,1,1,1,1,2,2,3,3,4]
fast=3: nums[3]=1 == nums[1]=1 → 不操作
fast=4: nums[4]=1 == nums[1]=1 → 不操作
fast=5: nums[5]=2 != nums[1]=1 → slow=2, nums[2]=2 数组变为[0,1,2,1,1,2,2,3,3,4]
fast=6: nums[6]=2 == nums[2]=2 → 不操作
fast=7: nums[7]=3 != nums[2]=2 → slow=3, nums[3]=3 数组变为[0,1,2,3,1,2,2,3,3,4]
fast=8: nums[8]=3 == nums[3]=3 → 不操作
fast=9: nums[9]=4 != nums[3]=3 → slow=4, nums[4]=4 最终数组:[0,1,2,3,4,2,2,3,3,4] 返回长度:5
4. 边界条件与特殊情况处理
4.1 空数组处理
当输入数组为空时,应直接返回0。这是最容易忽略的边界情况。
if not nums: return 04.2 单元素数组
数组只有一个元素时,无需任何操作,直接返回1。
4.3 全相同元素数组
如nums = [1,1,1,1],应返回1,数组变为[1,,,_]
4.4 已去重数组
如nums = [1,2,3,4],应返回4,数组保持不变
5. 算法复杂度分析
时间复杂度:O(n)
- 只需一次遍历数组,每个元素最多被访问一次
空间复杂度:O(1)
- 只使用了slow和fast两个额外变量
6. 变种问题与扩展
6.1 允许最多保留k个重复项
LeetCode第80题是此问题的扩展,要求每个元素最多出现两次。通用解法:
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 slow6.2 无序数组去重
如果数组无序,可以使用哈希表记录已出现元素:
def removeDuplicates(nums): seen = set() slow = 0 for fast in range(len(nums)): if nums[fast] not in seen: seen.add(nums[fast]) nums[slow] = nums[fast] slow += 1 return slow但这种方法空间复杂度为O(n),不符合本题要求。
6.3 返回新数组而非修改原数组
如果允许返回新数组,可以直接使用集合:
def removeDuplicates(nums): return list(set(nums))但会丢失元素顺序,且空间复杂度为O(n)
7. 实际应用场景
7.1 数据库查询结果去重
SELECT DISTINCT column FROM table在内存中处理时,可能需要类似的去重算法
7.2 日志处理
去除连续的重复日志条目,减少存储空间
7.3 时间序列数据清洗
去除传感器采集的重复数据点
8. 常见错误与调试技巧
8.1 数组越界错误
忘记检查数组是否为空,直接访问nums[0]会导致错误
8.2 逻辑错误
错误地将slow指针初始化为1,导致第一个元素被跳过
8.3 测试用例建议
应测试以下情况:
- 空数组
- 单元素数组
- 全相同元素数组
- 已去重数组
- 常规有重复数组
8.4 调试技巧
可以在循环中加入打印语句,观察指针移动和数组变化:
print(f"slow={slow}, fast={fast}, nums={nums}")9. 性能优化与语言特性
9.1 Python中的优化
在Python中,列表操作可能较慢。对于大型数组,可以考虑使用numpy数组:
import numpy as np def removeDuplicates(nums): nums = np.array(nums) if len(nums) == 0: return 0 slow = 0 for fast in range(1, len(nums)): if nums[fast] != nums[slow]: slow += 1 nums[slow] = nums[fast] return slow + 19.2 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; }9.3 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; }10. 面试技巧与注意事项
10.1 面试官可能问的问题
- 为什么选择双指针解法?
- 如何处理边界条件?
- 时间复杂度和空间复杂度是多少?
- 如何测试你的代码?
- 如果数组很大,如何优化?
10.2 回答技巧
- 先明确问题要求和约束条件
- 解释算法思路,画图说明指针移动
- 主动讨论边界条件和测试用例
- 分析复杂度,讨论优化可能性
10.3 代码风格建议
- 添加有意义的变量名(slow/fast比i/j更好)
- 添加必要的注释
- 先处理边界条件
- 保持代码简洁但可读
11. 相关题目推荐
- LeetCode 27 - 移除元素
- LeetCode 80 - 删除有序数组中的重复项 II
- LeetCode 283 - 移动零
- LeetCode 844 - 比较含退格的字符串
- LeetCode 977 - 有序数组的平方
12. 学习资源与进阶
- 《算法导论》 - 基础算法部分
- 《编程珠玑》 - 数组处理技巧
- LeetCode探索卡片 - 数组与字符串
- 双指针算法专题
- 原地算法(in-place algorithm)研究
在实际刷题过程中,我发现理解双指针的核心思想比记忆特定解法更重要。这个问题的关键在于认识到我们可以利用数组的有序性,通过比较相邻元素来识别重复项。这种模式可以推广到许多其他数组处理问题中。