1. 力扣删除有序数组中的重复项问题解析
这道题目是力扣(LeetCode)题库中的经典题目,编号26。题目要求我们处理一个非严格递增排列的数组,原地删除重复出现的元素,使每个元素只出现一次,并返回新数组的长度。这里的"原地"意味着不能使用额外的数组空间,必须在原数组上直接修改。
这个问题看似简单,但考察了程序员对数组操作、指针(或迭代器)使用以及算法效率的理解。在实际开发中,类似的需求经常出现,比如处理日志数据、用户行为记录等场景,都需要对有序数据进行去重操作。
2. 问题分析与解题思路
2.1 题目要求详解
题目给出的函数签名通常是:
cpp复制int removeDuplicates(vector<int>& nums);
具体要求:
- 输入是一个整数数组nums,按非严格递增顺序排列(即允许相邻元素相等)
- 需要原地修改数组,删除重复元素
- 返回新数组的长度
- 不需要考虑数组中超出新长度后面的元素
2.2 双指针解法思路
最优解法是使用双指针(快慢指针)技术:
- 慢指针(slow)指向当前不重复序列的最后一个位置
- 快指针(fast)用于遍历整个数组
- 当nums[fast] != nums[slow]时,将nums[fast]复制到nums[slow+1],然后slow++
- 最后返回slow+1作为新数组长度
这种解法的时间复杂度是O(n),空间复杂度是O(1),完全符合题目要求。
3. C++实现详解
3.1 基础实现代码
cpp复制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;
}
3.2 代码逐行解析
- 首先检查数组是否为空,如果是,直接返回0
- 初始化慢指针slow为0,指向第一个元素
- 快指针fast从1开始遍历数组
- 当发现nums[fast]不等于nums[slow]时,说明找到了新的不重复元素
- 将slow向前移动一位,并将nums[fast]的值赋给nums[slow]
- 遍历结束后,slow+1就是不重复元素的个数
3.3 边界条件处理
在实际编码中,我们需要考虑以下边界情况:
- 空数组:直接返回0
- 单元素数组:直接返回1
- 所有元素相同:只需要保留第一个元素
- 大数组:确保算法效率
4. 算法优化与变种
4.1 使用STL的unique函数
C++标准库提供了unique函数,可以简化实现:
cpp复制int removeDuplicates(vector<int>& nums) {
return distance(nums.begin(), unique(nums.begin(), nums.end()));
}
不过面试时通常要求自己实现算法,而不是直接调用库函数。
4.2 处理最多保留k个重复元素
这是该问题的变种,比如力扣第80题。解法思路类似,只是判断条件稍有变化:
cpp复制int removeDuplicates(vector<int>& nums, int k) {
if (nums.size() <= k) return nums.size();
int slow = k;
for (int fast = k; fast < nums.size(); ++fast) {
if (nums[fast] != nums[slow - k]) {
nums[slow++] = nums[fast];
}
}
return slow;
}
5. 常见错误与调试技巧
5.1 新手常见错误
- 忘记处理空数组的情况
- 慢指针初始位置错误(应该从0开始)
- 返回长度计算错误(应该是slow+1)
- 使用额外空间(违反了原地修改的要求)
5.2 调试技巧
- 打印指针位置和数组状态:
cpp复制cout << "slow: " << slow << ", fast: " << fast << endl;
for (int num : nums) cout << num << " ";
cout << endl;
- 使用小规模测试用例:
- 空数组:[]
- 单元素:[1]
- 全相同:[1,1,1]
- 正常情况:[1,1,2,2,3,4,4,4,5]
- 使用力扣的自定义测试功能验证边界情况
6. 性能分析与优化
6.1 时间复杂度分析
双指针解法只需要一次遍历,时间复杂度是O(n),这是最优解。
6.2 空间复杂度分析
原地修改数组,只使用了常数级别的额外空间(slow和fast指针),空间复杂度是O(1)。
6.3 实际运行优化
虽然时间复杂度已经是O(n),但可以通过以下方式进一步优化:
- 添加提前终止条件:当fast到达数组末尾时可以直接终止循环
- 减少不必要的赋值:当fast == slow+1时,可以跳过赋值操作
优化后的代码:
cpp复制int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if (n <= 1) return n;
int slow = 0;
for (int fast = 1; fast < n; ++fast) {
if (nums[fast] != nums[slow]) {
if (fast > slow + 1) {
nums[slow + 1] = nums[fast];
}
slow++;
}
}
return slow + 1;
}
7. 实际应用场景
这种去重算法在实际开发中有广泛应用:
- 数据库查询结果去重
- 日志分析中去除重复条目
- 用户行为分析中的事件去重
- 图像处理中去除连续相同的像素点
- 时间序列数据分析中的噪声过滤
8. 扩展练习与学习建议
为了彻底掌握这类问题,建议尝试以下扩展练习:
- 力扣27题:移除元素
- 力扣80题:删除有序数组中的重复项II
- 力扣283题:移动零
- 力扣844题:比较含退格的字符串
学习建议:
- 理解双指针法的核心思想
- 多画图分析指针移动过程
- 自己设计测试用例验证代码
- 尝试不同的实现方式并比较效率
9. C++语言特性应用
在解决这个问题时,我们可以利用一些C++特性使代码更简洁高效:
9.1 使用迭代器替代下标
cpp复制int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
auto slow = nums.begin();
for (auto fast = nums.begin() + 1; fast != nums.end(); ++fast) {
if (*fast != *slow) {
*(++slow) = *fast;
}
}
return distance(nums.begin(), slow) + 1;
}
9.2 使用STL算法
虽然前面提到面试时不建议直接使用unique,但在实际项目中可以合理利用STL:
cpp复制int removeDuplicates(vector<int>& nums) {
nums.erase(unique(nums.begin(), nums.end()), nums.end());
return nums.size();
}
10. 面试技巧与注意事项
在面试中遇到这类问题时,应注意:
- 先明确问题要求,与面试官确认边界条件
- 解释清楚算法思路后再开始编码
- 边写代码边解释关键步骤
- 主动考虑并处理边界情况
- 写完代码后用小测试用例验证
- 分析算法的时间和空间复杂度
- 讨论可能的优化方向
记住,面试官不仅考察你的编码能力,还考察你的问题分析、沟通表达和代码质量意识。
