目录
[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,那么如何直接找到这个这个位置呢?此时可以考虑前面从左向右复写的方式,但此时,"不复写,纯遛弯儿"。
步骤如下:
- 当cur < n (n表示数组的长度) 时,判断cur位置的元素是否为0;
- 若cur位置的元素为0,dest走2步,反之dest走1步;
- 判断dest是否越界,若没越界,则cur走一步,反之结束循环;

对应
当dest 触达数组最后下标 n-1 的时候停止,此时cur的位置的元素就是最后一个被复写的元素。此时,再开始从后往前复写。
但别着急,此时可能有边界情况,比如当数组为 1,0,2,3,0,4,当找到最后一个被复写的元素时,dest的位置刚好越界,如果这种情况下直接开始复写,就会导致发生下标越界的异常。所以在复写前就要做出边界处理。
2) 从右向前复写
步骤如下:
- 判断dest位置是否越界,如果越界,直接将n-1位置上的元素修改为0,dest-=2,cur-=1;
- 若不越界,直接开始复写;
- 判断cur位置元素的值,为0时 dest-=2;为非0时 dest-=1;
- 最后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]这个案例,吃透这个特殊场景。
下一篇我们继续双指针的其他经典题型。