1. 题目背景与核心要求解析
这道LeetCode 26题"删除有序数组中的重复项"是算法入门阶段的经典练习题,也是技术面试中的高频考题。我第一次接触这道题时,看似简单的要求下其实暗藏玄机,很多新手(包括当年的我)都会在"原地操作"这个关键点上栽跟头。
题目给出一个非严格递增排列的整数数组,要求我们:
- 原地删除重复出现的元素
- 保证每个元素只出现一次
- 保持元素的相对顺序不变
- 返回新数组的长度k
- 数组前k个元素必须是处理后的唯一元素
关键提示:这里的"删除"并不是真的从内存中移除元素,而是通过覆盖的方式让前k位成为不重复的有序序列。判题系统只会检查前k个元素,后面的内容不影响结果判定。
2. 解题思路深度剖析
2.1 暴力解法的局限性
很多同学的第一反应可能是创建一个新数组,遍历原数组时将不重复的元素放入新数组。这种方法虽然直观,但违反了题目"原地修改"的核心要求,空间复杂度会达到O(n),无法通过LeetCode的测试用例。
python复制# 错误示范(使用了额外空间)
def removeDuplicates(nums):
unique = []
for num in nums:
if num not in unique:
unique.append(num)
return len(unique)
2.2 双指针法的精妙之处
正确的解法是使用双指针技巧,这也是面试官最希望看到的解决方案。我在多次面试中验证过,能清晰解释双指针工作原理的候选人,通常对算法有更深入的理解。
双指针法的核心思想是:
- 慢指针(slow):标记当前唯一序列的末尾位置
- 快指针(fast):遍历整个数组寻找新元素
具体操作流程:
- 初始化slow=0(数组第一个元素必定是唯一的)
- fast从1开始遍历数组
- 当nums[fast] ≠ nums[slow]时:
- slow前进一位
- 将nums[fast]的值赋给nums[slow]
- 最终返回slow+1作为新长度
2.3 时间复杂度分析
这个算法只需要一次遍历数组,时间复杂度是O(n)。由于只使用了常数级别的额外空间(两个指针变量),空间复杂度是O(1),完全符合题目要求。
3. 代码实现与逐行解读
3.1 C++实现版本
cpp复制class Solution {
public:
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]) {
++slow;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
};
3.2 Python实现版本
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 + 1
3.3 Java实现版本
java复制class Solution {
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]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
}
4. 常见错误与调试技巧
4.1 新手常犯的5个错误
- 忽略空数组处理:没有检查输入数组是否为空,直接开始操作导致越界错误
- 指针初始化错误:将slow和fast都初始化为0,导致第一个元素被错误处理
- 边界条件错误:返回slow而不是slow+1,导致长度计算错误
- 不必要的元素交换:在元素相同时也进行赋值操作,增加不必要的计算
- 误解题目要求:试图真正删除数组元素(如使用erase操作),改变数组长度
4.2 调试技巧分享
当你的代码出现问题时,可以尝试以下调试方法:
- 打印指针位置:在循环中打印slow和fast的值,观察指针移动规律
python复制print(f"slow={slow}, fast={fast}, nums={nums}")
- 小规模测试用例:先用[1,1,2]这样的小数组测试,更容易发现问题
- 边界测试:测试空数组、单元素数组、全相同数组等特殊情况
- 可视化跟踪:在纸上画出数组和指针位置的变化过程
5. 算法变种与扩展思考
5.1 类似题目推荐
掌握了这道题的核心思想后,可以尝试解决以下变种题目:
- LeetCode 80. 删除有序数组中的重复项 II(允许最多出现两次)
- LeetCode 27. 移除元素(删除特定值的所有出现)
- LeetCode 283. 移动零(将零移动到数组末尾)
5.2 允许k次重复的通用解法
如果题目改为允许每个元素最多出现k次,我们可以扩展双指针解法:
python复制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
5.3 实际应用场景
这种去重算法在以下场景有实际应用:
- 数据库查询结果去重
- 日志数据清洗
- 时间序列数据处理
- 大数据分析中的预处理阶段
6. 性能优化与进阶技巧
6.1 编译器优化考虑
在C++实现中,我们可以做一些微优化:
- 使用++i而非i++:前缀递增在多数情况下效率更高
- 将nums.size()缓存:避免在循环中重复计算
- 使用引用而非拷贝:确保不会意外修改输入数组
6.2 多语言实现的注意事项
不同语言实现时需要注意:
- Java/C++:数组长度是固定属性,不能改变
- Python:列表是可变对象,但不要使用del等改变长度的操作
- JavaScript:数组长度可变,但题目要求原地修改
6.3 面试中的加分回答
当面试官问及这道题时,可以补充以下知识点:
- STL中unique函数的实现原理
- 双指针法的其他应用场景(如快排、链表问题)
- 空间复杂度与原地算法的关系
- 稳定性在排序算法中的意义
经过多次实践,我发现理解这道题的核心不在于记住代码,而是真正掌握双指针法的思维方式。这种技巧在解决数组、链表等问题时非常有用,是每个程序员都应该熟练掌握的基础算法技能。