双指针模板:从两端逼近到快慢指针的高频套路#
双指针的核心思想很简单:
用两个指针代替暴力枚举,让区间、位置或状态变化变得可控。
双指针并不只有一种写法,最常见的有三类:
- 左右指针:从两端向中间逼近
- 快慢指针:不同速度或不同职责前进
- 同向双指针:都从左往右,但维护不同边界
⚡ 速记版模板#
// 模板用途:用两个指针协同移动,替代暴力双重枚举
int left = 0; // 左指针,通常从区间左端出发
int right = nums.length - 1; // 右指针,通常从区间右端出发
while (left < right) { // 当两指针未相遇时持续收缩/推进
if (满足条件) { // 根据题目单调性决定移动方向
left++; // 条件指向左指针右移
} else {
right--; // 否则右指针左移
}
}java🎯 什么时候想到双指针?#
典型信号:
- 数组有序,想在线性时间里找答案
- 需要原地修改数组
- 链表里要找中点、环、倒数第 K 个节点
- 需要从两端收缩区间
Hot 100 里的典型题目:
💡 模板一:左右夹逼型#
适合:
- 有序数组两数和
- 盛水容器
- 回文判断
- 接雨水(双指针解法)
class Solution {
public int solve(int[] nums) {
int left = 0; // 左边界指针
int right = nums.length - 1; // 右边界指针
int answer = 0; // 题目答案(此模板中作为占位)
while (left < right) { // 两端向中间夹逼
if (shouldMoveLeft(nums, left, right)) { // 依据题意判断应移动哪一边
left++; // 移动左指针
} else {
right--; // 移动右指针
}
}
return answer; // 返回累计/更新后的结果
}
private boolean shouldMoveLeft(int[] nums, int left, int right) {
return nums[left] <= nums[right]; // 示例规则:左值不大于右值时优先移动左侧
}
}java关键是:
- 每次移动后,必须保证不会错过最优解
- 所以题目的单调性或比较逻辑非常重要
💡 模板二:快慢指针型#
适合:
- 移动零
- 删除重复元素
- 原地压缩数组
class Solution {
public void moveZeroes(int[] nums) {
int slow = 0; // 慢指针:指向下一个应放“非零元素”的位置
for (int fast = 0; fast < nums.length; fast++) { // 快指针:负责扫描整个数组
if (nums[fast] != 0) { // 发现非零元素就交换到 slow 位置
swap(nums, slow, fast); // 原地把非零元素前移
slow++; // slow 后移,准备放下一个非零元素
}
}
}
private void swap(int[] nums, int first, int second) {
int temp = nums[first]; // 临时保存 first 位置值
nums[first] = nums[second]; // second 覆盖到 first
nums[second] = temp; // 完成交换
}
}java思路:
fast负责扫描slow负责维护“下一个该放正确元素的位置”
💡 模板三:链表快慢指针型#
适合:
- 找中点
- 判断是否有环
- 找倒数第 K 个节点
class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) { // 边界:空链表或单节点链表不可能成环
return false;
}
ListNode slow = head; // 慢指针每次走一步
ListNode fast = head; // 快指针每次走两步
while (fast != null && fast.next != null) { // 保证 fast 走两步时不空指针
slow = slow.next; // 慢指针前进一步
fast = fast.next.next; // 快指针前进两步
if (slow == fast) { // 快慢指针相遇,说明存在环
return true;
}
}
return false; // 快指针走到 null,说明无环
}
}java💡 模板四:排序 + 双指针#
适合:
- 三数之和
- 四数之和
- 去重组合问题
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums); // 先排序,便于双指针夹逼和去重
List<List<Integer>> result = new ArrayList<>(); // 保存所有不重复三元组
for (int index = 0; index < nums.length - 2; index++) { // 固定第一个数 nums[index]
if (index > 0 && nums[index] == nums[index - 1]) { // 外层去重:同值首元素只处理一次
continue;
}
int left = index + 1; // 第二个数从 index 右侧起
int right = nums.length - 1; // 第三个数从数组末尾起
while (left < right) { // 双指针查找另外两个数
int sum = nums[index] + nums[left] + nums[right]; // 当前三数和
if (sum == 0) { // 命中答案
result.add(Arrays.asList(nums[index], nums[left], nums[right])); // 收集三元组
left++; // 继续寻找下一组
right--;
while (left < right && nums[left] == nums[left - 1]) { // 内层去重:跳过重复 left 值
left++;
}
while (left < right && nums[right] == nums[right + 1]) { // 内层去重:跳过重复 right 值
right--;
}
} else if (sum < 0) { // 和偏小,需要更大值
left++; // 左指针右移
} else {
right--; // 和偏大,右指针左移
}
}
}
return result; // 返回所有不重复且和为 0 的三元组
}
}java⚠️ 易错点#
-
去重时机写错
- 三数之和里,外层和内层的去重都不能漏
-
快慢指针职责混乱
- 扫描归扫描,写入归写入
-
链表空指针判断遗漏
fast != null && fast.next != null要先判断全
-
左右指针移动依据不充分
- 不能凭感觉移动,要有单调性支撑
🎨 面试时怎么说#
我用双指针把原本的二重枚举压到线性扫描,关键是让一个指针负责搜索,另一个指针负责维护合法区间或有效位置。
📌 一句话总结#
双指针不是固定模板,而是一个思想:
- 让两个指针承担不同职责
- 用指针移动替代重复遍历