双指针算法面试通关指南:从入门到精通
双指针是面试中最常考的算法技巧之一,本文系统梳理快慢指针、对撞指针、滑动窗口三大类型,助你轻松应对算法面试。
前言
在算法面试中,双指针技巧出现的频率极高。它不仅能将 O(n²) 的暴力解法优化到 O(n),而且代码简洁优雅,是面试官非常青睐的考察点。
本文将从原理到实战,带你彻底掌握双指针算法。
一、什么是双指针?
双指针是指在遍历对象的过程中,使用两个指针进行访问,而不是普通的单个指针。
根据指针移动方式的不同,双指针可以分为三类:
| 类型 | 特点 | 典型场景 |
|---|---|---|
| 快慢指针 | 一快一慢,相对移动 | 链表判环、找中点、去重 |
| 对撞指针 | 左右指针,相向移动 | 两数之和、反转数组、盛水容器 |
| 滑动窗口 | 左右指针,同向移动 | 子串问题、最小覆盖子串 |
二、快慢指针
2.1 核心思想
快指针每次走多步,慢指针每次走一步,通过速度差解决问题。
2.2 经典题目:判断链表是否有环
javascript
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow === fast) {
return true; // 相遇,有环
}
}
return false; // 快指针到达末尾,无环
}
原理:如果链表有环,快指针一定会追上慢指针;无环则快指针先到达末尾。
2.3 经典题目:寻找链表的中点
javascript
function middleNode(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // 慢指针指向中点
}
2.4 经典题目:删除排序数组中的重复项
javascript
function removeDuplicates(nums) {
if (nums.length === 0) return 0;
let slow = 0;
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // 新数组长度
}
三、对撞指针
3.1 核心思想
一个指针从头部开始,一个从尾部开始,向中间移动,直到相遇。
3.2 经典题目:两数之和 II(有序数组)
javascript
function twoSum(numbers, target) {
let left = 0;
let right = numbers.length - 1;
while (left < right) {
const sum = numbers[left] + numbers[right];
if (sum === target) {
return [left + 1, right + 1];
} else if (sum < target) {
left++; // 和太小,左指针右移
} else {
right--; // 和太大,右指针左移
}
}
return [];
}
为什么有序数组可以用对撞指针?
- 数组有序,可以通过比较和与 target 的大小,决定移动哪个指针
- 时间复杂度 O(n),空间复杂度 O(1)
3.3 经典题目:盛最多水的容器
javascript
function maxArea(height) {
let left = 0;
let right = height.length - 1;
let maxArea = 0;
while (left < right) {
// 面积 = 宽度 * 高度(取两边较小值)
const area = (right - left) * Math.min(height[left], height[right]);
maxArea = Math.max(maxArea, area);
// 移动高度较小的一边,才可能找到更大的面积
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
关键思路:移动高度较小的一边,因为宽度在减小,只有高度增加才可能使面积变大。
3.4 经典题目:验证回文串
javascript
function isPalindrome(s) {
let left = 0;
let right = s.length - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !isAlphaNum(s[left])) left++;
while (left < right && !isAlphaNum(s[right])) right--;
if (s[left].toLowerCase() !== s[right].toLowerCase()) {
return false;
}
left++;
right--;
}
return true;
}
四、滑动窗口
4.1 核心思想
维护一个窗口,左右指针同向移动,右指针扩展窗口,左指针收缩窗口。
4.2 经典题目:无重复字符的最长子串
javascript
function lengthOfLongestSubstring(s) {
const set = new Set();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
// 右指针扩展,遇到重复字符则收缩左边界
while (set.has(s[right])) {
set.delete(s[left]);
left++;
}
set.add(s[right]);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
4.3 经典题目:最小覆盖子串
javascript
function minWindow(s, t) {
const need = new Map(); // 需要的字符及数量
const window = new Map(); // 窗口中的字符及数量
for (const c of t) {
need.set(c, (need.get(c) || 0) + 1);
}
let left = 0, right = 0;
let valid = 0; // 窗口中满足 need 条件的字符数
let start = 0, len = Infinity;
while (right < s.length) {
const c = s[right];
right++;
if (need.has(c)) {
window.set(c, (window.get(c) || 0) + 1);
if (window.get(c) === need.get(c)) {
valid++;
}
}
// 窗口满足条件,尝试收缩
while (valid === need.size) {
if (right - left < len) {
start = left;
len = right - left;
}
const d = s[left];
left++;
if (need.has(d)) {
if (window.get(d) === need.get(d)) {
valid--;
}
window.set(d, window.get(d) - 1);
}
}
}
return len === Infinity ? '' : s.substring(start, start + len);
}
滑动窗口解题框架:
- 初始化左右指针、窗口数据、结果变量
- 右指针扩展窗口,更新窗口数据
- 当窗口满足条件时,收缩左指针,更新结果
- 重复直到右指针到达末尾
五、面试高频题目汇总
| 题目 | 类型 | 难度 | 考察点 |
|---|---|---|---|
| 141. 环形链表 | 快慢指针 | 简单 | 链表判环 |
| 876. 链表的中间结点 | 快慢指针 | 简单 | 找中点 |
| 26. 删除有序数组中的重复项 | 快慢指针 | 简单 | 数组去重 |
| 167. 两数之和 II | 对撞指针 | 中等 | 有序数组 |
| 11. 盛最多水的容器 | 对撞指针 | 中等 | 贪心思想 |
| 125. 验证回文串 | 对撞指针 | 简单 | 字符串处理 |
| 3. 无重复字符的最长子串 | 滑动窗口 | 中等 | 窗口维护 |
| 76. 最小覆盖子串 | 滑动窗口 | 困难 | 窗口收缩 |
| 424. 替换后的最长重复字符 | 滑动窗口 | 中等 | 窗口条件 |
六、解题技巧总结
6.1 如何选择指针类型?
题目涉及链表?
├─ 判环、找中点、倒数第K个 → 快慢指针
└─ 反转、重排 → 对撞指针
题目涉及数组/字符串?
├─ 有序数组、两数之和 → 对撞指针
├─ 子串、子数组、连续序列 → 滑动窗口
└─ 原地修改、去重 → 快慢指针
6.2 代码模板速记
快慢指针模板:
javascript
let slow = 0, fast = 0;
while (fast < n) {
// 处理逻辑
if (满足条件) {
slow++;
}
fast++;
}
对撞指针模板:
javascript
let left = 0, right = n - 1;
while (left < right) {
if (满足条件) return result;
else if (条件A) left++;
else right--;
}
滑动窗口模板:
javascript
let left = 0;
for (let right = 0; right < n; right++) {
// 右指针扩展,加入窗口
window.add(s[right]);
// 收缩左边界
while (需要收缩) {
window.remove(s[left]);
left++;
}
// 更新结果
result = Math.max(result, right - left + 1);
}
七、总结
双指针算法是面试中的高频考点,掌握以下要点:
- 快慢指针:用于链表问题,通过速度差解决问题
- 对撞指针:用于有序数组,从两端向中间逼近
- 滑动窗口:用于子串问题,维护动态窗口
- 识别场景:根据题目特征快速判断使用哪种指针
- 代码模板:熟记三种指针的基本框架,面试时快速写出
双指针的精髓在于减少不必要的遍历,将暴力解法的 O(n²) 优化到 O(n)。多刷题、多总结,面试遇到双指针问题就能游刃有余。
如果本文对你有帮助,欢迎点赞、收藏、评论!有任何问题可以在评论区留言讨论。
标签 :算法 面试 双指针 LeetCode JavaScript 前端