1. 问题背景与需求分析
- 复写零是力扣(LeetCode)上一道经典的数组操作题目,考察对双指针技巧的掌握程度。题目要求:给定一个固定长度的整数数组,遇到零就复制一个零插入其后,其余元素依次右移,超出数组长度的部分舍弃。
这个问题的难点在于需要在原地修改数组(即不使用额外空间),并且要正确处理连续零的情况。例如输入[1,0,2,3,0,4]应该输出[1,0,0,2,3,0,0],而数组长度保持不变。
2. 双指针解法核心思路
2.1 正向思维与逆向思维对比
大多数初学者会首先想到正向遍历数组,遇到零就插入一个新零。但这种方法存在两个致命缺陷:
- 插入操作会导致后续元素位置变化,需要频繁移动大量元素
- 无法预先知道最终哪些元素会被挤出数组
逆向双指针法(从后向前处理)完美解决了这些问题:
- 先扫描确定哪些元素会被保留
- 再从后向前放置元素,避免频繁移动
2.2 具体算法步骤
-
第一次遍历:使用快慢指针确定最终保留的元素
- 慢指针指向最终数组位置
- 快指针遍历原数组
- 遇到零时快指针多走一步(因为零需要复制)
-
边界条件处理:
- 处理快指针刚好超出数组边界的情况
- 处理最后一个元素是零且无法完整复制的情况
-
第二次遍历:从后向前填充元素
- 快指针从数组末尾开始
- 慢指针指向最后保留的元素
- 遇到零时写入两个零
3. 代码实现与详细注释
cpp复制void duplicateZeros(vector<int>& arr) {
int n = arr.size();
int slow = 0, fast = 0;
// 第一次遍历:确定保留的元素位置
while (fast < n) {
if (arr[slow] == 0) fast++;
slow++;
fast++;
}
// 处理边界情况
slow--; fast--;
if (fast > n - 1) {
arr[n - 1] = 0;
fast -= 2;
slow--;
}
// 第二次遍历:从后向前填充
while (slow >= 0) {
if (arr[slow] == 0) {
arr[fast--] = 0;
arr[fast--] = 0;
} else {
arr[fast--] = arr[slow];
}
slow--;
}
}
4. 复杂度分析与优化空间
4.1 时间复杂度
算法进行了两次线性遍历,时间复杂度为O(n),这是最优解。
4.2 空间复杂度
完全原地操作,仅使用常数额外空间,空间复杂度O(1)。
4.3 可能的优化方向
- 合并部分条件判断减少分支
- 使用位运算等技巧加速零的判断
- 针对特定语言特性优化(如C++的指针操作)
5. 常见错误与调试技巧
5.1 典型错误案例
- 正向遍历导致数组越界
- 处理连续零时复制次数错误
- 边界条件处理不完整
5.2 调试建议
- 使用小规模测试用例(如[0]、[1,0,1])
- 打印指针位置和数组中间状态
- 特别注意数组最后一个元素是否为零
6. 同类问题拓展
双指针技巧在数组问题中应用广泛,类似题目包括:
- 移除元素(27题)
- 移动零(283题)
- 合并有序数组(88题)
掌握这类问题的关键在于:
- 明确每个指针的物理意义
- 处理好边界条件
- 选择适当的遍历方向(正向/逆向)
