目录
- 双指针算法:化繁为简的优雅解法
-
- 引言:一个经典的优化故事
- 什么是双指针算法?
- 三大经典类型详解
-
- [1. 相向双指针:从两端向中间](#1. 相向双指针:从两端向中间)
-
- [经典问题:盛最多水的容器(LeetCode 11)](#经典问题:盛最多水的容器(LeetCode 11))
- [三数之和问题(LeetCode 15)](#三数之和问题(LeetCode 15))
- [2. 同向双指针:快慢指针的妙用](#2. 同向双指针:快慢指针的妙用)
-
- [删除有序数组中的重复项(LeetCode 26)](#删除有序数组中的重复项(LeetCode 26))
- 链表中的快慢指针:检测环
- 实际应用场景
-
- 场景1:合并两个有序数组
- [场景2:接雨水问题(LeetCode 42)](#场景2:接雨水问题(LeetCode 42))
- 算法模板总结
- 常见问题与技巧
-
- [1. 如何选择双指针类型?](#1. 如何选择双指针类型?)
- [2. 指针移动的条件是什么?](#2. 指针移动的条件是什么?)
- [3. 边界条件处理](#3. 边界条件处理)
- 实战练习题目
- 结语
双指针算法:化繁为简的优雅解法
从两数之和到滑动窗口,双指针算法如何用O(n)的时间复杂度解决看似复杂的问题?
引言:一个经典的优化故事
想象一下这个场景:你需要在一个有序数组中找到两个数,使它们的和等于目标值。最直观的方法是什么?很多人会想到双重循环:
cpp
// 暴力解法 O(n²)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
但这样做的时间复杂度是O(n²),当数组很大时效率极低。有没有更好的方法?
双指针算法 给出了答案,可以将时间复杂度优化到O(n):
cpp
// 双指针解法 O(n)
int left = 0, right = nums.size() - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) return {left, right};
else if (sum < target) left++;
else right--;
}
这就是双指针的魅力所在!
什么是双指针算法?
双指针算法是一种通过两个指针(索引或引用)在数据结构上协同工作来解决问题的技巧。这两个指针按照一定规则移动,从而减少不必要的遍历,将许多O(n²)的问题优化到O(n)或O(n log n)。
核心优势
- 时间复杂度优化:通常从O(n²)降至O(n)
- 空间复杂度低:通常只需要O(1)的额外空间
- 代码简洁优雅:逻辑清晰,易于理解和维护
三大经典类型详解
1. 相向双指针:从两端向中间
这是最常见的一种类型,特别适合有序数组。
经典问题:盛最多水的容器(LeetCode 11)
cpp
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int maxWater = 0;
while (left < right) {
int h = min(height[left], height[right]);
int w = right - left;
maxWater = max(maxWater, h * w);
// 关键:移动较小的一侧
if (height[left] < height[right])
left++;
else
right--;
}
return maxWater;
}
为什么移动较小的一侧?
因为容器的容量由较短的边决定。移动较短的边,虽然宽度减小了,但高度可能增加,从而可能获得更大的面积。而移动较长的边,高度不会增加(仍由短边决定),宽度却减小了,容量必然减小。
三数之和问题(LeetCode 15)
cpp
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1, right = nums.size() - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.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 result;
}
2. 同向双指针:快慢指针的妙用
这种模式中,两个指针从同一端开始,但移动速度不同。
删除有序数组中的重复项(LeetCode 26)
cpp
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]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // 返回新长度
}
理解这个算法:
slow指针维护"处理好的部分"fast指针遍历整个数组- 当
fast找到新元素时,将其放到slow+1的位置
这个过程就像我们整理书架:slow指向已经整理好的部分,fast寻找新书,找到后放到整理区的末尾。
链表中的快慢指针:检测环
cpp
bool hasCycle(ListNode *head) {
if (!head || !head->next) return false;
ListNode *slow = head;
ListNode *fast = head->next;
while (slow != fast) {
// 快指针到达末尾,说明无环
if (!fast || !fast->next)
return false;
slow = slow->next; // 慢指针走一步
fast = fast->next->next; // 快指针走两步
}
return true; // 相遇说明有环
}
有趣的事实: 如果有环,快慢指针一定会相遇,就像两个人在环形跑道上跑步,速度快的人总会追上速度慢的人。
实际应用场景
场景1:合并两个有序数组
cpp
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) {
if (nums1[p1] > nums2[p2]) {
nums1[p--] = nums1[p1--];
} else {
nums1[p--] = nums2[p2--];
}
}
// 如果nums2还有剩余元素
while (p2 >= 0) {
nums1[p--] = nums2[p2--];
}
}
场景2:接雨水问题(LeetCode 42)
cpp
int trap(vector<int>& height) {
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
// 左边较矮,计算左边
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
water += leftMax - height[left];
}
left++;
}
// 右边较矮,计算右边
else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
water += rightMax - height[right];
}
right--;
}
}
return water;
}
算法模板总结
通用模板
cpp
// 相向双指针
void oppositePointers(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
// 根据具体条件决定移动哪个指针
if (condition1) left++;
else if (condition2) right--;
else {
// 找到结果,同时移动两个指针
left++;
right--;
}
}
}
// 快慢指针
void fastSlowPointers(vector<int>& nums) {
int slow = 0; // 慢指针:维护结果位置
for (int fast = 0; fast < nums.size(); fast++) {
// 当快指针满足条件时,更新慢指针
if (condition) {
nums[slow] = nums[fast];
slow++;
}
}
}
常见问题与技巧
1. 如何选择双指针类型?
- 有序数组查找 → 相向双指针
- 去重/元素操作 → 快慢指针
- 链表问题 → 快慢指针
2. 指针移动的条件是什么?
这是双指针算法的关键!通常:
- 比较两指针指向的值
- 根据目标值与当前值的关系
- 根据是否满足某种条件
3. 边界条件处理
- 空数组/空字符串
- 单元素情况
- 指针越界检查
- 重复元素处理
实战练习题目
| 难度 | 题目 | 类型 | 关键点 |
|---|---|---|---|
| 简单 | 移动零 | 快慢指针 | 将非零元素前移 |
| 简单 | 反转字符串 | 相向双指针 | 交换首尾字符 |
| 中等 | 三数之和 | 相向双指针 | 排序+去重 |
| 中等 | 盛最多水的容器 | 相向双指针 | 移动较小边 |
| 中等 | 无重复字符的最长子串 | 滑动窗口 | 字符计数 |
| 困难 | 最小覆盖子串 | 滑动窗口 | 字符需求统计 |
结语
双指针算法之所以强大,是因为它利用数据的特性 (如有序性)和问题的约束,避免了不必要的计算。