攻克算法面试必考题型!今日系统解析双指针算法的核心思想与高频应用场景,覆盖数组、链表、字符串等经典问题,助你掌握O(n)高效解法。
一、双指针算法核心思想
双指针算法通过维护两个指针协同遍历数据结构,将暴力解法的时间复杂度从O(n²)优化至O(n),核心策略:
缩减搜索空间:通过指针移动排除无效区间
利用单调性:基于数据有序性减少重复计算
同步/异步移动:根据问题需求设计指针移动逻辑
适用场景特征:
-
线性数据结构(数组、链表、字符串)
-
存在明显的位置关联性(如对称性、区间特性)
-
可分解为两两关系的问题(如两数之和、三数之和)
二、六大应用场景详解
场景1:相向双指针(对撞指针)
应用方向:
-
有序数组两数之和
-
盛水最多的容器
-
回文字符串验证
代码模板:
int left = 0, right = n-1;
while (left < right) {
if (满足条件) return 结果;
else if (条件分支1) left++;
else right--;
}
真题示例(LeetCode 15. 三数之和):
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
int n = nums.size();
for (int i=0; i<n-2; ++i) {
if (i>0 && nums[i]==nums[i-1]) continue;
int left = i+1, right = n-1;
while (left < right) {
int sum = nums[i]+nums[left]+nums[right];
if (sum == 0) {
res.push_back({nums[i], nums[left], nums[right]});
while (left<right && nums[left]==nums[left+1]) left++;
while (left<right && nums[right]==nums[right-1]) right--;
left++; right--;
}
else if (sum < 0) left++;
else right--;
}
}
return res;
}
场景2:快慢双指针(龟兔赛跑)
应用方向:
-
链表环检测(Floyd判圈算法)
-
链表中点定位
-
数组去重/元素迁移
代码模板:
int slow = 0, fast = 0;
while (fast < n) {
if (满足条件) {
swap(arr[slow], arr[fast]);
slow++;
}
fast++;
}
真题示例(LeetCode 26. 有序数组去重):
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int slow = 0;
for (int fast=1; fast<nums.size(); ++fast) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
场景3:滑动窗口(同向双指针)
应用方向:
-
最长无重复子串
-
最小覆盖子串
-
区间最大值/平均值
代码模板:
int left = 0, right = 0;
while (right < n) {
窗口扩展:将nums[right]加入窗口
while (窗口不满足条件) {
窗口收缩:移除nums[left]
left++;
}
更新最优解
right++;
}
真题示例(LeetCode 3. 最长无重复子串):
int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left=0, max_len=0;
for(int right=0; right<s.size(); ++right){
while(window.count(s[right])){
window.erase(s[left]);
left++;
}
window.insert(s[right]);
max_len = max(max_len, right-left+1);
}
return max_len;
}
场景4:分离双指针(多序列处理)
应用方向:
-
合并有序数组
-
判断子序列
-
区间列表交集
真题示例(LeetCode 88. 合并有序数组):
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = m-1, p2 = n-1, p = m+n-1;
while (p1 >=0 && p2 >=0) {
nums1[p--] = (nums1[p1] > nums2[p2]) ? nums1[p1--] : nums2[p2--];
}
while (p2 >=0) nums1[p--] = nums2[p2--];
}
场景5:链表的双指针操作
应用方向:
-
链表倒数第k个节点
-
链表翻转
-
链表相交判断
真题示例(剑指 Offer 22. 链表中倒数第k个节点):
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *fast = head, *slow = head;
for(int i=0; i<k; ++i) fast = fast->next;
while(fast) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
场景6:数学特性双指针
应用方向:
-
平方数判断
-
丑数生成
-
数值逼近问题
真题示例(LeetCode 633. 平方数之和):
bool judgeSquareSum(int c) {
long left=0, right=sqrt(c);
while(left <= right){
long sum = left*left + right*right;
if(sum == c) return true;
else if(sum < c) left++;
else right--;
}
return false;
}
三、复杂度与优化技巧
场景类型 | 时间复杂度 | 空间复杂度 | 优化要点 |
---|---|---|---|
相向双指针 | O(n) | O(1) | 利用数据有序性 |
快慢指针 | O(n) | O(1) | 避免多余空间使用 |
滑动窗口 | O(n) | O(k) | 哈希表替代暴力统计 |
分离双指针 | O(m+n) | O(1) | 逆向遍历避免覆盖 |
四、大厂真题实战
真题1:接雨水问题
题目描述:
给定表示高度的数组,计算下雨后能接多少雨水
双指针解法:
int trap(vector<int>& height) {
int left=0, right=height.size()-1;
int left_max=0, right_max=0, res=0;
while(left < right) {
left_max = max(left_max, height[left]);
right_max = max(right_max, height[right]);
if(height[left] < height[right]) {
res += left_max - height[left];
left++;
} else {
res += right_max - height[right];
right--;
}
}
return res;
}
真题2:字符串排列检查
题目描述:
判断字符串s2是否包含s1的某个排列的子串
滑动窗口解法:
bool checkInclusion(string s1, string s2) {
vector<int> cnt(26,0), window(26,0);
for(char c : s1) cnt[c-'a']++;
int left=0, n=s2.size(), k=s1.size();
for(int right=0; right<n; ++right){
window[s2[right]-'a']++;
if(right-left+1 == k) {
if(window == cnt) return true;
window[s2[left++]-'a']--;
}
}
return false;
}
、常见误区与避坑指南
-
指针越界:未检查指针移动后的有效性(如访问
nums[right+1]
导致越界) -
条件遗漏:滑动窗口收缩条件不完整(如
while
误写为if
) -
顺序依赖:未考虑数据有序性前提(如两数之和未排序直接使用对撞指针)
-
更新滞后:先移动指针再计算结果导致漏判
-
死循环陷阱:指针未在条件分支中正确移动
六、总结与拓展
核心能力提升:
-
问题转化能力(将复杂问题抽象为双指针模型)
-
边界处理能力(精确控制指针移动条件)
-
模板活用能力(根据场景选择最优指针策略)
进阶思考:
-
如何用双指针解决四数之和问题?
-
滑动窗口如何应用于数据流场景?
-
快慢指针在检测链表环时的数学原理是什么?
LeetCode真题训练: