一、双指针概述
双指针是算法题中非常经典的解题思想,核心是通过两个指针协同遍历,在一次遍历内完成目标,通常可以把暴力解法的时间复杂度从 O (n²) 优化到 O (n)。常见的双指针分为两大类:对撞指针、快慢指针。
1. 对撞指针(左右指针)
对撞指针多用于数组这类顺序结构,也常被称为左右指针。
- 两个指针分别从数组的两端出发,左指针从最左侧开始,右指针从最右侧开始,逐步向中间逼近。
- 循环的终止条件一般是两个指针相遇(
left == right),或者两个指针错开(left > right);也可能在循环内部找到目标结果后直接跳出循环。
2. 快慢指针(龟兔赛跑算法)
快慢指针的核心思想,是使用两个移动速度不同的指针,在数组、链表等顺序结构上移动。
- 这种方法最经典的应用场景是判断环形链表、寻找环入口。
- 不止是环形结构,只要问题中存在循环往复的特征,都可以考虑用快慢指针的思路来解决。
- 最常用的实现方式:在一次循环中,慢指针每次向后移动一步,快指针每次向后移动两步,通过一快一慢的速度差来识别循环。
二、例题:移动零(LeetCode 283,难度:Easy)
这是一道非常经典的「数组分两块」题型,核心是按照指定规则,把数组元素划分成左右两部分。这类区间划分的题目,通常都可以用双指针的思路高效解决。
题目描述
解法思路:快排划分思想(数组分两块)
这道题可以借鉴快速排序中的区间划分思想:用两个指针维护两个区间,一个是非零元素区间,一个是零元素区间,一次遍历完成划分。
核心思想
定义两个指针:
cur:遍历指针,用来扫描整个数组的每一个元素dest:标记非零元素序列的最后一个位置
在 cur 遍历的全程,始终维持一个不变量:
[0, dest]区间内的元素全部是非零元素;[dest + 1, cur - 1]区间内的元素全部是 0。
根据 cur 遇到的元素不同,分情况处理,最终就能把所有非零元素留在左边,所有零移到右边。
算法流程
初始化
cur = 0,从数组第一个元素开始遍历dest = -1,初始时非零区间还不存在,所以非零末尾位置初始化为 -1
遍历处理 cur 依次向后遍历每一个元素,分两种情况处理:
- 遇到元素为 0 :
cur直接向后移动一位即可。 因为我们的目标是让[dest+1, cur-1]全是 0,cur 遇到 0 时自增,这个 0 自然就落在了零区间内。 - 遇到元素非 0 :先让
dest自增 1,然后交换cur位置和dest位置的元素,之后cur再向后移动一位。
dest原本指向非零区间的末尾,新发现一个非零元素,它的位置应该在dest+1,所以 dest 先加 1;dest++之后指向的元素原本是 0,和 cur 位置的非零元素交换后,新的非零元素就被纳入了非零区间,0 被换到了零区间,两个区间的性质始终保持不变。
C++ 代码实现
class Solution {
public:
void moveZeroes(vector<int>& nums) {
// cur 遍历整个数组,dest 标记非零区间的末尾位置
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
{
// 遇到非零元素,将其交换到非零区间末尾
if(nums[cur])
{
swap(nums[++dest], nums[cur]);
}
}
}
};
复杂度分析
- 时间复杂度:O (n),仅遍历数组一次
- 空间复杂度:O (1),只使用常数个额外变量,全程原地修改数组
补充:这道题的双指针属于同向快慢指针的变体,两个指针都向右移动、速度不同,是双指针体系里非常基础的模板题。
同一道算法题,解法往往不唯一。这里分享的只是我个人的思路与实现,供大家参考。大家也可以顺着自己的想法去尝试不同的写法,如果有更优的解法或是不一样的理解,欢迎在评论区留言交流,我们一起探讨、共同进步。