【双指针专题】之复写零
一、题目描述
给定一个长度固定的整数数组
arr
,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。要求:
请不要在超过该数组长度的位置写入元素。
请对输入的数组 就地 进行上述修改,不要返回任何东西。
示例 :
输入:arr = [1,0,2,3,0,4,5,0]
输出:[1,0,0,2,3,0,0,4]
解释:调用函数后,输入数组被修改为 [1,0,0,2,3,0,0,4]
二、思路引导:双指针 / 反向复写
这一题的难点在于:如果我们从前往后复写,会导致后面的元素被覆盖掉。
举个例子:当我们遇到
0
时,需要把它写两次,但后面本该还没来得及复写的数会被提前覆盖。所以我们需要换一种思路:
从后往前复写!
那为什么可行呢?
- 因为往后写的时候,后面的位置还没被“覆盖”,这样就能保证数据不会丢失。
- 关键点是要先找到“最后一个能被复写的数”,然后再从后往前依次复写。
三、算法流程
这里我们用两个指针:
cur
:扫描数组的当前下标;dest
:复写到结果数组的下标。
步骤 1:找到最后一个需要复写的数
初始化:
cur = 0, dest = -1
- 如果
arr[cur] != 0
,则dest++
- 如果
arr[cur] == 0
,则dest += 2
- 一旦
dest >= n - 1
(即超过数组尾部),循环结束。
步骤 2:边界处理
如果
dest == n
,说明最后一个位置正好是复写 0 导致的越界:
- 把
arr[n - 1]
赋值为0
cur--, dest -= 2
进行修正
步骤 3:从后往前复写
- 如果
arr[cur] != 0
,那么就复写一个元素:arr[dest--] = arr[cur--]
- 如果
arr[cur] == 0
,那么就复写两次:arr[dest--] = 0; arr[dest--] = 0; cur--;
四、代码实现(C++)
#include <vector>
#include <iostream>
using namespace std;class Solution {
public:void duplicateZeros(vector<int>& arr) {int dest = -1, cur = 0;// 步骤 1: 找到最后一个需要复写的数// 思路:如果是非零元素,结果数组只占 1 个位置;// 如果是 0,结果数组需要占 2 个位置。while (cur < arr.size()) {if (arr[cur]) ++dest; // 非零元素,目标指针前进 1else dest += 2; // 0 元素,目标指针前进 2if (dest >= arr.size() - 1) // 超过数组末尾,停止break;cur++;}// 思路 2: 边界处理// 如果刚好越界到数组的末尾 n(即多出了一位),// 说明最后一个元素是因为 0 的复写产生的。if (dest == arr.size()) {arr[arr.size() - 1] = 0; // 把最后一位补上 0dest -= 2; // 回退两步cur--; // 当前元素回退一步}// 思路 3: 从右往左复写// 反向扫描,避免元素被覆盖while (cur >= 0) {if (arr[cur]) {// 非零元素:正常复写一个arr[dest--] = arr[cur];} else {// 零元素:复写两次arr[dest--] = 0;arr[dest--] = 0;}cur--;}}
};
五、代码剖析与细节问题
1. 为什么要从后往前复写?
如果从前往后,一旦遇到
0
,需要写两次,但此时后面的元素还没有被处理,就会被覆盖掉。
所以必须从后往前复写,这样可以保证后面未处理的元素不会丢失。
2. 边界处理的坑
在 步骤 1 里我们用
dest
来记录结果数组的下标,有可能出现dest == n
的情况。
这种情况代表 最后一个位置正好是因为复写0
产生的越界。
处理办法是:
- 把
arr[n - 1]
强制置为0
;- 然后
cur--, dest -= 2
回退到正确位置。如果忘记处理这个边界,程序会 数组越界 or 少复写一个元素。这是这题的 最大坑。
3. 为什么 dest 初始是 -1?
因为在第一次遇到非零元素时,
dest
应该指向 0 号下标。
如果初始化成 0,那么第一步就会偏移一位,导致整体错位。
六、复杂度分析
- 时间复杂度:O(n)
- 第一次扫描数组,找到最后一个需要复写的数,耗时 O(n);
- 第二次从后往前复写,也最多 O(n);
- 因此整体时间复杂度为 O(n)。
- 空间复杂度:O(1)
- 我们只使用了常数级别的额外变量
cur
和dest
;- 属于 原地算法。
七、总结
这道题与上一篇“移动零”非常相似,都是 双指针 的思路:
- “移动零”是从前往后,把非零数挪到前面;
- “复写零”则是从后往前,把零多写一次。
两道题结合起来,能更好地理解:双指针既可以“前向扫描”,也可以“反向复写”。
笔者的话:这道题如果不换角度,会卡在“覆盖问题”上。掌握了“从后往前复写”的技巧,你会发现很多数组操作题都能迎刃而解。