双指针问题

双指针

1. 讲解

一、双指针的两种常见形式

双指针并不是一定要用 C/C++ 中的指针,很多情况下用数组下标、链表节点引用也可以充当指针。

1. 对撞指针

对撞指针也叫左右指针,通常用于顺序结构,比如有序数组、字符串等。

  • 一个指针从最左端开始,另一个指针从最右端开始。

  • 两个指针向中间移动,逐渐逼近。

  • 终止条件一般是:

    • left == right:两个指针指向同一个位置;

    • left > right:两个指针错开;

    • 或者在循环内部找到结果后直接跳出。

常见应用场景:

  • 有序数组的两数之和;

  • 判断回文串;

  • 三数之和、四数之和;

  • 盛最多水的容器等。

对撞指针的核心思想是:

利用左右两端的信息,不断缩小搜索区间,从而减少不必要的枚举。

2. 快慢指针

快慢指针又叫龟兔赛跑算法。

基本思想:

使用两个移动速度不同的指针,在数组、链表等序列结构上移动。

常见实现方式:

  • 慢指针每次向后移动一位;

  • 快指针每次向后移动两位;

  • 通过速度差来判断是否存在环、找到中间节点、处理循环往复问题等。

常见应用场景:

  • 判断链表是否有环;

  • 找链表中间节点;

  • 环形数组;

  • 一些循环替换问题。

快慢指针的核心思想是:

用两个不同速度的指针,在序列中形成相对位置差或速度差,从而发现循环、中点、重复模式等规律。

二、双指针的做题思想

步骤:

  1. 用两个指针划分区间;

  2. 明确指针及划分区间的含义;

  3. 根据题目条件决定两个指针如何移动;

  4. 在移动过程中更新答案或调整区间。

常见作用:

  • 将暴力枚举 O(n²) 优化为 O(n)

  • 原地完成数组划分、去重、移动元素;

  • 利用单调性减少重复计算;

  • 发现循环结构、找中点等。

三、例题讲解:移动零

我们以力扣上的移动零为例题讲解双指针的思想

1. 题目解析

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

要求:

  • 必须原地操作,不能复制数组。

示例:

复制代码
 输入:nums = [0,1,0,3,12]
 输出:[1,3,12,0,0]
复制代码
 输入:nums = [0]
 输出:[0]
2. 算法原理:数组划分思想

这道题本质上是数组划分问题。

我们可以把数组分成三个区间:

复制代码
 [0, dest]             非零元素区间
 [dest + 1, cur - 1]   零元素区间
 [cur, n - 1]          待处理区间

两个指针的含义:

  • cur:从左往右扫描数组,遍历数组;

  • dest:已处理区间内,最后一个非零元素的位置。

初始状态:

复制代码
 dest = -1
 cur = 0

dest = -1 表示一开始没有处理过任何非零元素。

结束状态:

复制代码
 dest = 最后一个非零元素位置
 cur = n

cur = n 表示数组全部被处理完

3. 移动规则

cur 从前往后遍历的过程中:

情况一:遇到 0

说明这个元素应该放在中间的零区间,不需要交换,直接移动 cur

复制代码
 cur++

情况二:遇到非零元素

需要把这个非零元素放到左边非零区间的末尾,也就是 dest + 1 的位置。

操作:

要让非零元素加入到最左边 0, dest 区间,由于该区间会多一个元素,因此要右移 dest ,但这个非零元素不能直接覆盖(真的不能直接覆盖吗?后续优化代码会给出答案),因此我们这里做一个交换操作,交换完成后,cur右移,继续处理下一个元素

复制代码
 swap(nums[dest + 1], nums[cur])
 dest++
 cur++

交换后:

  • dest + 1 位置变成了非零元素;

  • 原来的零被交换到后面;

  • 非零区间向右扩展一位。

4. 演示流程

nums = [0, 1, 0, 0, 3, 12] 为例:

5. 代码实现
java 复制代码
class Solution {
     public void moveZeroes(int[] nums) {
         for (int cur = 0, dest = -1; cur < nums.length; cur++) {
             if (nums[cur] != 0) {
                 dest++;
                 int tmp = nums[cur];
                 nums[cur] = nums[dest];
                 nums[dest] = tmp;
             }
         }
     }
 }
6. 复杂度分析
  • 时间复杂度:O(n)

    因为 cur 从头到尾遍历一次数组。

  • 空间复杂度:O(1)

    只使用了常数级别的额外空间,是原地操作。

7.优化

前面我们提到,遇到非零元素时不能直接覆盖,用到了交换,但观察发现,交换操作永远是将一个非零元素与零元素交换,因此,此处我们可以将 cur 位置的非零元素直接覆盖 dest 位置上的 0 ,然后将 cur 位置的元素置为 0 :

java 复制代码
public static void moveZeroes(int[] nums) {
     int dest = -1, cur = 0;
     int len = nums.length;
     while (cur < len) {
         if (nums[cur] != 0) {
             dest++;
             // 当dest和cur指向同一位置时,如果还进行下面的操作会报错
             // 比如:[1] dest和cur都指向1,如果还进行nums[cur]=0会将原来的1覆盖掉,就会报错
             if (dest != cur) {
                 nums[dest] = nums[cur];
                 nums[cur] = 0;
             }
         }
         cur++;
     }
 }

优点

  1. 该代码减少了不必要的交换,当 dest == cur 时,说明当前非零元素已经在正确位置,不需要做任何写操作,直接跳过。

  2. 遇到非零元素时,swap 需要进行三次赋值,该代码只需要两次赋值,减少了一次赋值操作。

  3. 时间复杂度依然是 O(n),空间复杂度 O(1)。但常数项更小(数组读取次数少,且没有临时变量),实际运行更快。

当然,我们原来的代码如果为了减少交换次数也可以加上 if(dest != cur) 的判断:

java 复制代码
public void moveZeroes(int[] nums) {
     for (int cur = 0, dest = -1; cur < nums.length; cur++) {
         if (nums[cur] != 0) {
             dest++;
             if (dest != cur) {
                 int tmp = nums[cur];
                 nums[cur] = nums[dest];
                 nums[dest] = tmp;
             }
         }
     }
 }
8. 扩展:数组划分与快速排序

这种「数组分块」的思想非常重要,它可以解决快速排序中最核心的一步:partition

快速排序的划分可以抽象为:

复制代码
 [0, dest]              小于等于基准值的区间
 [dest + 1, cur - 1]    大于基准值的区间
 [cur, n - 1]           待处理区间

通过类似的双指针遍历和交换,就能把数组按基准值划分成左右两部分。

四、总结

「移动零」这道题本质上是:

用两个下标 curdest 维护数组区间,cur 负责扫描,dest 负责记录非零区间的末尾。遇到非零元素就交换到前面,遇到零就跳过。

它体现的是双指针中常见的"划分区间"思想:

  • cur 相当于快指针,负责遍历;

  • dest 相当于慢指针,负责记录已经处理好的区域边界。

掌握这种区间划分方法,对后续学习快速排序、数组去重、移除元素等问题都很有帮助。

2. 习题链接

移动零

283.移动零

复写零

1089.复写零

快乐数

202.快乐数

盛最多水的容器

11.盛最多水的容器

有效三角形的个数

611.有效三角形的个数

和为s的两个数

LCR 179.查找总价格为目标值的两个商品

三数之和

15.三数之和

四数之和

18.四数之和

相关推荐
拾光Ծ1 小时前
【MySQL】复合查询、表的连接
java·数据库·sql·mysql
MetaLite1 小时前
Java通用枚举驱动下拉框-元数据接口与前端契约
java·前端·状态模式
心抵鹊1 小时前
力扣每日一题:计算右侧小于当前元素的个数(hard)
算法·leetcode
文子越来越强1 小时前
线程池使用总结
java
50万马克的面包1 小时前
数据结构:线性表 —— 顺序表与链表完整总结
数据结构·链表
鹿角片ljp1 小时前
LeetCode 142:环形链表 II |HashSet 保底解 + Floyd 快慢指针找环入口
算法·leetcode·链表
Fairy要carry1 小时前
初创实习-IMU模型
人工智能·算法·机器学习
a187927218311 小时前
【算法】回溯算法(一):从一道 IP 题到万能模板
算法·leetcode·go·回溯·leetcode93·ip复原·算法讲解
鹿角片ljp1 小时前
LeetCode 19:删除链表的倒数第 N 个节点复盘| dummy + 快慢指针
算法·leetcode·链表