【双指针|LeetCode1089 复写零】巧用逆向思路,破解原地覆盖陷阱

一、题目描述

👉 力扣在线题目:1089. 复写零,建议先自己尝试做题,再阅读本篇题解。

二、思路分析

很多人第一反应会选择从前向后遍历,遇到 0 就把后面元素后移。但这个方案有致命缺陷:前面的 0 复写时,会直接覆盖后面还没遍历到的元素,原始数据直接丢失,所以不能正向原地填充。

那怎么解决覆盖问题? 核心想法:从后往前逆向复写。 先通过一次遍历,cur 遍历原数组,dest 标记目标位置,找到最终需要处理的末尾位置。再利用双指针,从后向前把元素挪到目标位置,遇到 0 就连续写两个 0。从后向前操作,就不会覆盖还没有读取的原始数据。

整体分两大步骤:

第一次正向预处理遍历:用 cur 扫描原数组,dest 模拟复写后的目标数组下标,找到最后一个需要保留的元素。这里分为普通情况和边界例外情况;

逆向填充:双指针从后向前遍历,完成数组原地复写。

三、正向遍历为什么行不通(拓展对比)

我们先设想正向操作: 数组 [1,0,2,3,0,4,5,0],遇到下标 1 的 0,我们要在下标 2 再写一个 0。 写入之后数组变成:[1,0,0,3,0,4,5,0]。 👉 问题来了:原来下标 2 的数字 2 直接被覆盖,永久丢失!

只要正向一边读、一边往后面写,写入操作会破坏还没读取的原始数组。 如果想用正向实现,只能额外开辟一个新数组保存结果,空间复杂度变成 O (n),不满足原地修改的要求。

💡提示: 正向遍历:代码简单,但原地操作会覆盖原始数据;想不覆盖就要额外数组,耗费内存。 逆向双指针:多一次预处理遍历,但是原地修改,空间 O (1),符合题目要求。

四、C++ 代码 分段讲解

复制代码
class Solution { 
public:    
    void duplicateZeros(vector<int>& arr) {        
        int cur = 0;        
        int dest = -1;        
        int n = arr.size();        

初始化两个指针:cur 用来遍历原数组,dest 标记复写后的目标下标

重点:两个指针的含义 & 初始化解释

cur(读取指针):cur 指向原数组正在读取的元素下标,专门用来读取原始数组,不修改数组内容。 正向遍历时,cur 从左向右逐个读取元素;逆向填充阶段,cur 从后往前读取未被破坏的原始数据。 一句话:cur 负责遍历原始数组,所以初始化为 0,从数组第一个元素开始扫描。

dest(目标模拟指针):可以这样理解:cur 扫描原始数组,dest 用来模拟复写完成之后的目标数组,提前预判每个元素在结果数组中的下标。 dest 在这里定义为:上一次写入完成后,最后一个已经填好元素的下标。 初始化为 - 1,含义是:目前还没有写入任何元素。

读到非 0 元素:这个元素只占 1 个位置,dest++;

读到 0:需要复写两个 0,占用 2 个位置,dest += 2;

💡灯泡提示:dest 初始化为 - 1 是和我们逻辑配套的技巧。逻辑是先移动 dest,再存放元素。

复制代码
        // 第一步:预处理,找到最后一个需要保留的元素位置        
        while(cur < n)        
        {            
            if(arr[cur] != 0)                
                dest++;            
            else                
                dest += 2;            
            // dest达到数组最后一个合法下标,停止预扫描循环            
            if(dest >= n - 1)                
                break;            
            cur++;        
        }        

遍历数组,非 0 元素目标位置 + 1;遇到 0,目标位置向后移动两位。 如果 dest 已经触达数组尾部,提前终止循环,cur 此时指向最后一个待处理元素。

普通情况 和 边界例外情况

✅ 普通情况(最常见)

dest 预处理结束后,dest == n-1 代表当前 cur 指向的元素,完整放到数组内刚好填满,不会越界。 举例子:arr = [1,0,2,3,0,4,5,0],数组长度 n=8,最大下标 7。 预处理结束 dest=7,刚好等于 n-1。 此时直接进入逆向填充,读到 0 就连续写两个 0,读到数字直接搬运,所有内容都落在数组下标 0~7 范围内,没有超出数组长度。

⚠️ 例外边界情况:dest == n

这种场景的成因:当前 cur 位置是 0,如果完整复写两个 0,第二个 0 会写到下标 n,超出数组的合法下标范围(最大 n-1)。 题目要求不能写入超出数组长度,因此这个 0 我们只能保留 1 个 0,不能写两个。

举个例子:arr = [1,0,0,3],n=4,下标 0,1,2,3。 初始化 cur=0,dest=-1

  1. cur=0,arr0=1,dest++ → dest=0,cur=1
  2. cur=1,arr1=0,dest +=2 → dest=2,cur=2
  3. cur=2,arr2=0,dest +=2 → dest=4

dest=4,刚好等于 n=4,触发特殊情况if(dest == n)
这里解释下关键代码 cur--; dest -= 2;

  1. arr[n-1] = 0:先把能合法写入的那一份 0 写进数组末尾,放弃复写第二个 0,避免越界。
  2. cur--:当前 cur 对应的这个 0 已经处理完毕,读取指针前移,准备读取前面剩下的原始元素。
  3. dest -= 2:写入指针回退两格。原本这个 0 正常要占 2 个位置,虽然我们舍弃了越界的那一个,但写入位置需要往前跳 2 格,维持双指针的位置映射关系不变。如果这里只写 dest--,dest 只回退 1 格,后面反向拷贝的时候下标对应关系错乱,数组结果会出错。

我们手动走一遍这个例子的反向填充阶段:当前状态:cur=1,dest=2,此时已经提前执行边界处理:arr[n-1]=0,数组变为[1,0,0,0]

  • cur=1,arr cur=0:向 dest=2 和 dest=1 的位置依次写入 0,dest 连续减 2,dest 变成 0;cur 减 1,cur=0
  • cur=0,arr cur=1:将 1 写入 dest=0 位置,dest 减 1 变成 - 1;cur 减 1,cur=-1

循环结束,最终数组:[1,0,0,0] ✔验证:原始数组[1,0,0,3],完整复写后的理想结果应为[1,0,0,0,0,3],但数组长度固定为 4,只能保留前 4 位,最终得到[1,0,0,0],符合题目要求。

复制代码
        // 处理特殊边界:dest刚好越界的情况        
        if(dest == n)        
        {            
            arr[n - 1] = 0;            
            cur--;            
            dest -= 2;        
        }        

边界场景:当 0 复写后会超出数组长度,我们只能保留一个 0,防止数组越界。

复制代码
        // 第二步:从后向前逆向复写        
        while(cur >= 0)        
        {            
            if(arr[cur] != 0)            
            {                
                arr[dest--] = arr[cur--];            
            }            
            else            
            {                
                // 遇到0,连续写入两个0                
                arr[dest--] = 0;                
                arr[dest--] = 0;                
                cur--;            
            }        
        }    
    } 
}; 

逆向遍历:非 0 元素直接搬运;碰到 0,连续写两个 0。 从后往前写,不会覆盖还未读取的原始数据,完美避开覆盖陷阱。

五、算法总结

✅ 核心难点:正向遍历会覆盖后面未读取的数据

✅ 解题策略:逆向双指针,原地修改,不开额外数组,空间复杂度 O (1)

✅ 时间复杂度:O (n),数组最多遍历两遍

💡提示:这道题是原地数组操作的经典题。遇到原地修改、元素后移类题目,如果正向遍历容易覆盖原始数据,可以优先想一想逆向遍历的思路。 另外一定要留意边界:0 复写后超出数组长度的场景,此时 0 只能写 1 个,是高频易错点。

算法题从来都不是只有一种写法,这里给出的解法仅供参考。你也可以按照自己的思路去尝试不同的实现方式,如果有更优的解法或者不一样的理解,欢迎在评论区一起交流,共同进步。

相关推荐
专注仿真10 小时前
知识问答模型用到的算法集详解及代码实例
python·算法·代码·大模型训练·控制算法
罗西的思考11 小时前
【轻量化复刻大模型 / KIMI K3】AttnRes 模块交互式演示
人工智能·算法·机器学习
好悬给我拽开线14 小时前
ZILOT 论文解析
算法
饮水桶_T15 小时前
JAVA IO流笔记
开发语言·数据结构·算法
星夜离尘5616 小时前
MATLAB 离散数据分箱:半开区间、末端点与计数、加权和、平均
算法·matlab
Pniubi16 小时前
力扣76.最小覆盖子串
算法·leetcode·职场和发展
地平线开发者17 小时前
征程6|用 hrt\_model\_exec 看清 YOLOv5x 的输入、原始输出和后处理
人工智能·算法
csdn_aspnet18 小时前
C++ 3个有序点的方向(Orientation of 3 ordered points)
c++·算法
赤生云电竞18 小时前
GPU算力共享平台架构实践:任务分发与资源隔离的关键配置
算法·架构·gpu算力