【优选算法:双指针】——复写零题解

目录

一、算法概述

二、例题讲解

[2.1 题目1:复写零](#2.1 题目1:复写零)

[2.1.1 思路讲解:](#2.1.1 思路讲解:)

[2.1.2 做题步骤:](#2.1.2 做题步骤:)

1) 找到最后一个被复写的元素的位置 找到最后一个被复写的元素的位置)

2) 从右向前复写 从右向前复写)

[2.1.3 代码实现(Java/c++)](#2.1.3 代码实现(Java/c++))


一、算法概述

指针算法实质上就是用两个变量标识列表中的具体位置,来达到目标。常见的双指针一般分为两类:左右指针 和 快慢指针。

  • 左右指针 :两个指针分别从数组 / 序列的左右两端向中间移动,也叫对撞指针。多用于有序数组,实现二分查找、两数之和、反转数组等场景,一左一右根据条件不断收缩区间。
  • 快慢指针:两个指针从同一侧起点出发,一个走得快、一个走得慢(步长不一样)。典型用于链表,判断链表是否有环、找链表中点;也可以用于数组,原地移除元素。

对于算法的讲解,文字叙述我不想写太多,主要还是通过例题讲解让大家真正领悟双指针算法。单纯记概念很容易遗忘,看懂例题、看懂指针每一步怎么移动,才能吃透双指针的思想:不产生额外数组,仅通过下标移动,降低时间、空间复杂度,很多时候可以把暴力 O (n²) 优化到 O (n)。

二、例题讲解

2.1 题目1:复写零

力扣题目地址:1089. 复写零 - 力扣(LeetCode):

给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。

注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地进行上述修改,不要从函数返回任何东西。

2.1.1 思路讲解 :

首先题目中明确是 就地修改 ,就明白不能够新开辟空间进行复写。此时想到使用两个指针模拟复写一下示例1,一般都会想从左向右复写,定义两个指针,cur表示遍历当前要复写的元素位置,dest表示待复写位置,初始cur=0,dest=-1。cur位置非零时dest走1步,但cur为零dest则走2步,如图:

结果分析:

由上图所示,由于dest指针的速度是不稳定的,所以dest指针的位置可能会跑到cur指针的前面,那么从左向右复写就可能导致每次cur走到的位置都为0,导致后续的元素全被复写成0了。

那么换一个角度思考,从左到右复写不行,那从右到左呢?由于复写后的数组长度要和复写前的数组长度保持一致 ,那么只要数组中存在0元素,那么一定会有末尾的元素被丢掉 。所以想要从右向左复写元素,有一个前提,就是找到最后一个被复写的元素的位置!

2.1.2 做题步骤:

1) 找到最后一个被复写的元素的位置

从示例1的答案不难看出,对于数组 1,0,2,3,0,4,5,0,最后一个被复写的元素是4,那么如何直接找到这个这个位置呢?此时可以考虑前面从左向右复写的方式,但此时,"不复写,纯遛弯儿"。

步骤如下:

  1. 当cur < n (n表示数组的长度) 时,判断cur位置的元素是否为0;
  2. 若cur位置的元素为0,dest走2步,反之dest走1步;
  3. 判断dest是否越界,若没越界,则cur走一步,反之结束循环;

对应

当dest 触达数组最后下标 n-1 的时候停止,此时cur的位置的元素就是最后一个被复写的元素。此时,再开始从后往前复写。

但别着急,此时可能有边界情况,比如当数组为 1,0,2,3,0,4,当找到最后一个被复写的元素时,dest的位置刚好越界,如果这种情况下直接开始复写,就会导致发生下标越界的异常。所以在复写前就要做出边界处理。

2) 从右向前复写

步骤如下:

  1. 判断dest位置是否越界,如果越界,直接将n-1位置上的元素修改为0,dest-=2,cur-=1;
  2. 若不越界,直接开始复写;
  3. 判断cur位置元素的值,为0时 dest-=2;为非0时 dest-=1;
  4. 最后cur--,复写下一个位置的元素,注意cur>=0;

以例题1为例:

2.1.3 代码实现(Java/c++)

  • java代码实现:
java 复制代码
class Solution {
    public void duplicateZeros(int[] arr) {
        int cur=0;
        int dest=-1;
        int n=arr.length;
        //1.找到最后一个被复写的元素位置
        while(cur<n){
            if(arr[cur]==0) dest+=2;
            else dest++;
            if(dest>=n-1) break;
            cur++;
        }

         //2.处理边界情况
            if(dest==n){
                arr[n-1]=0;
                dest-=2;
                cur--;
            }

        //3.开始复写
        while(cur>=0){
            if(arr[cur]==0){
                arr[dest--]=0;
                arr[dest--]=0;
                cur--;
            }else{
                arr[dest--]=arr[cur--];
            }
        }
    }
}
  • c++代码实现:
cpp 复制代码
#include <vector>
using namespace std;

class Solution
{
public:
    void duplicateZeros(vector<int>& arr)
    {
        // 1. 先找到最后一个数
        int cur = 0, dest = -1, n = arr.size();
        while(cur < n)
        {
            if(arr[cur]) dest++;
            else dest += 2;
            if(dest >= n - 1) break;
            cur++;
        }
        // 2. 处理一下边界情况
        if(dest == n)
        {
            arr[n - 1] = 0;
            cur--; dest -=2;
        }
        // 3. 从后向前完成复写操作
        while(cur >= 0)
        {
            if(arr[cur]) 
                arr[dest--] = arr[cur--];
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

这道题是原地双指针非常经典的入门例题。核心思想一句话总结:正向遍历只模拟不写入,定位最后一个有效元素;再反向拷贝,避免未复制的数据被覆盖。

很多同学一开始会直接从左向右原地写,踩掉数据覆盖的坑。记住:原地修改数组,当写指针跑的比读指针更快时,优先考虑反向填充。

本题的边界判断 dest==n 是面试高频考点,建议手动推演[0,1,2]这个案例,吃透这个特殊场景。

下一篇我们继续双指针的其他经典题型。

相关推荐
kloc2 分钟前
聊天室项目入口网关子服务的实现
linux·c++
SL_staff1 小时前
私有化低代码:面向强监管行业的可控数字化实践
java·低代码
饮水桶_T1 小时前
JAVA IO流笔记
开发语言·数据结构·算法
三翼鸟数字化技术团队1 小时前
高性能存储引擎RocksDB:原理与Java生态实践
java
SL_staff2 小时前
JVS-APS有限产能建模的七个刚性支点:面向产线真实约束的技术实现解析
java·开源·全栈
特立独行的猫A2 小时前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python
星夜离尘562 小时前
MATLAB 离散数据分箱:半开区间、末端点与计数、加权和、平均
算法·matlab
Pniubi2 小时前
力扣76.最小覆盖子串
算法·leetcode·职场和发展
学长毕设2 小时前
基于SpringBoot的高校后勤报修管理系统-附源码
java·spring boot·后端
特立独行的猫A2 小时前
cj-tauri vs Rust/Tauri 同款播放器实测:编译 21 秒 vs 11 分钟,磁盘 12 MB vs 1.2 GB
java·后端·python