1089. 复写零 - 解题思路整理
题目分析
问题描述:
给定一个固定长度的整数数组 arr,对数组中的每个 0 进行复写(每个 0 在数组中占据两个位置),并将其余元素向右平移。如果数组长度不足,则舍弃超出长度的元素。
核心要求:
-
对数组中的 0 进行复写(每个 0 变成两个 0)
-
非 0 元素保持原值
-
修改必须在原数组中进行
-
从数组末尾开始处理,避免覆盖未处理的元素
示例说明:
示例1:输入: [1,0,2,3,0,4,5,0]
输出: [1,0,0,2,3,0,0,4]
示例2:输入: [0,1,2,3,0,4]
输出: [0,0,1,2,3,0]
特殊情况:最后一个0被舍弃,因为数组长度不足
算法分析
1. 问题难点
-
从前往后复写会覆盖还未处理的元素
-
需要先知道哪些元素会被保留,哪些会被舍弃
-
必须从后往前进行复写操作
2. 双指针法(从后向前)
算法思想:
-
第一阶段:模拟从前往后复写的过程,找到最后一个会被保留的元素
-
第二阶段:从后往前进行实际的复写操作
指针定义:
-
cur:从前往后扫描指针,用于模拟复写过程 -
dest:从前往后复写时的目标位置指针 -
在第二阶段,
cur和dest的含义会变化
第一阶段:寻找最后一个复写的数
目标:确定在复写后,数组中最后一个元素是什么,以及它是从哪里复写过来的。
初始化:
-
cur = 0:当前处理的元素位置 -
dest = -1:如果在这里复写,应该写入的位置
处理逻辑:
while (cur < arr.length) {
if (arr[cur] != 0) {
// 非0元素,dest只移动一步
dest++;
} else {
// 0元素,dest需要移动两步(复写)
dest += 2;
}
// 判断dest是否已到达或超过数组末尾
if (dest >= arr.length - 1) {
break; // 找到最后一个复写的元素
}
cur++;
}
特殊情况处理:
// 如果dest正好等于arr.length(越界1位)
if (dest == arr.length) {
// 说明最后一个位置需要放置一个0的复写
arr[arr.length - 1] = 0;
dest -= 2; // dest回退两位
cur--; // cur回退一位
}
执行过程示例:
输入: [1,0,2,3,0,4,5,0] (长度8)
步骤 cur dest arr[cur] 操作
0) 0 -1 1 dest=0
1) 1 0 0 dest=2
2) 2 2 2 dest=3
3) 3 3 3 dest=4
4) 4 4 0 dest=6
5) 5 6 4 dest=7
6) 6 7 5 dest=8 (停止)
此时:
- dest = 8 (已越界,需要特殊处理)
- cur = 6 (指向元素5)
- 最后一个被处理的元素是5
第二阶段:从后往前复写
目标:从后向前遍历数组,将元素复写到正确位置。
处理逻辑:
// 此时cur指向最后一个被处理的元素
// dest指向最后一个应该写入的位置
while (cur >= 0 && dest >= 0) {
if (arr[cur] != 0) {
// 非0元素,直接复制
arr[dest] = arr[cur];
dest--;
} else {
// 0元素,需要复写两次
if (dest >= 1) {
arr[dest] = 0;
arr[dest - 1] = 0;
} else if (dest == 0) {
// dest=0时,只能写入一个0
arr[dest] = 0;
}
dest -= 2;
}
cur--;
}
示例执行(继续上面的例子):
第一阶段结束后:
cur = 6, dest = 8 (越界,需调整)
arr = [1,0,2,3,0,4,5,0]
特殊情况处理:
dest == 8 > 7,需要特殊处理
arr[7] = 0 (最后一个位置放0)
dest = 8-2 = 6
cur = 6-1 = 5
从后往前复写:
cur=5, dest=6: arr[5]=4 → arr[6]=4
cur=4, dest=5: arr[4]=0 → arr[5]=0, arr[4]=0
cur=3, dest=3: arr[3]=3 → arr[3]=3
cur=2, dest=2: arr[2]=2 → arr[2]=2
cur=1, dest=1: arr[1]=0 → arr[1]=0, arr[0]=0
cur=0, dest=-1: 结束
最终结果: [0,0,1,2,3,0,0,4]
完整算法步骤总结
步骤1:寻找最后一个复写的数
-
初始化
cur = 0,dest = -1 -
遍历数组:
-
如果
arr[cur] == 0,dest += 2 -
否则,
dest += 1 -
当
dest >= arr.length-1时停止
-
-
处理边界情况:
- 如果
dest == arr.length,在末尾放一个0,dest-=2,cur-=1
- 如果
步骤2:从后往前复写
-
从
cur开始向前遍历:-
如果
arr[cur] != 0:arr[dest--] = arr[cur] -
如果
arr[cur] == 0:-
arr[dest--] = 0 -
如果
dest >= 0,arr[dest--] = 0
-
-
-
继续直到
cur < 0
时间复杂度与空间复杂度
时间复杂度:O(n)
-
第一阶段:遍历一次数组,找到最后一个元素
-
第二阶段:反向遍历一次数组
-
总共两次遍历,O(2n) = O(n)
空间复杂度:O(1)
-
只使用了常数个额外变量
-
满足原地操作的要求
关键技巧总结
-
双向指针:一个指针模拟复写过程,一个指针记录目标位置
-
从后往前处理:避免覆盖未处理的元素
-
边界处理:特别注意数组末尾的0复写可能导致越界
-
分阶段处理:先"模拟"找到边界,再实际执行
这种方法的核心思想是先虚拟地走一遍复写过程,确定哪些元素会被保留,然后从后往前进行实际复写,确保不会覆盖还未处理的元
class Solution {
public void duplicateZeros(int[] arr) {
//先找到最后一个复写的数字
int cur=0;
int dest=-1;
for(cur=0;cur<arr.length;cur++){
if(arr[cur]!=0){
dest++;
}else{
dest+=2;
}
if(dest>=arr.length-1)
break;
}
//特殊情况
if(dest==arr.length){
arr[arr.length-1]=0;
cur--;
dest-=2;
}
//从后往前完成复写操作
while(cur>=0){
if(arr[cur]!=0){
arr[dest--]=arr[cur--];
}else{
arr[dest--]=0;
arr[dest--]=0;
cur--;
}
}
}
}
素。