双指针、滑动窗口、前缀和、二分查找 ------ 应用场景详解
一、双指针
核心特征
两个指针朝着某个方向移动,通常能将 O(N²) 的暴力枚举优化到 O(N)
场景1:相向双指针
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 有序数组找两数之和 | 给定升序数组,找两个数使和等于target | 数组有序,和固定 |
| 回文串判断 | 判断字符串是否是回文 | 从两端向中间比较 |
| 盛最多水的容器 | 找两条线使装水最多 | 面积由短板决定 |
| 三数之和 | 找三个数使和为0 | 固定一个,另外两个用双指针 |
判断标志 :数组有序 + 要找两个(或多个)数的组合 + 和/差有固定关系
cpp
// 典型场景:有序数组两数之和
// 暴力 O(N²) → 双指针 O(N)
vector<int> twoSum(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l < r) {
int sum = nums[l] + nums[r];
if (sum == target) return {l, r};
else if (sum < target) l++; // 和太小,左指针右移增大
else r--; // 和太大,右指针左移减小
}
return {};
}
场景2:快慢双指针
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 原地去重 | 删除有序数组中的重复项 | 需要原地修改,不增加额外空间 |
| 移动零 | 把数组中的0移到末尾 | 保持非0元素相对顺序 |
| 链表找环 | 判断链表是否有环 | 快指针走2步,慢指针走1步 |
| 找链表中点 | 找出链表的中间节点 | 快慢指针,快指针到末尾时慢指针在中点 |
判断标志 :需要原地修改 + 两个指针移动速度不同 + 或者一个先走一个后走
cpp
// 典型场景:原地去重
// 暴力 O(N²)(每次删除后移动元素)→ 双指针 O(N)
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;
}
二、滑动窗口
核心特征
维护一个动态窗口,窗口边界只向前移动,每个元素最多进出窗口一次,O(N)复杂度
场景1:固定大小窗口
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 固定长度子数组最值 | 大小为k的子数组最大和 | 窗口长度固定为k |
| 滑动窗口平均值 | 计算每k个数的平均值 | 固定窗口,滑动计算 |
| 大小为k的子数组最大值 | 滑动窗口最大值(单调队列) | 需要O(1)获取窗口内最大值 |
判断标志 :子数组/子串长度固定 + 求最大值/最小值/平均值
cpp
// 典型场景:大小为k的子数组最大和
// 暴力 O(N*k) → 滑动窗口 O(N)
int maxSum(vector<int>& nums, int k) {
int sum = 0, ans = INT_MIN;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
if (i >= k - 1) {
ans = max(ans, sum);
sum -= nums[i - k + 1]; // 去掉窗口最左边的元素
}
}
return ans;
}
场景2:可变大小窗口
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 和≥target的最短子数组 | 找最短连续子数组使其和≥s | 求满足条件的最短长度 |
| 最长无重复子串 | 找最长不含重复字符的子串 | 求满足条件的最长长度 |
| 包含所有字符的最短子串 | 最小覆盖子串 | 窗口内必须包含某组字符 |
| 最多包含K个不同字符的最长子串 | 最长无重复字符子串的扩展 | 窗口内种类数有限制 |
判断标志:
连续子数组/子串+满足某个条件+求最短/最长- 条件通常是:和 ≥ target / 不包含重复 / 包含指定字符集
cpp
// 典型场景:和≥target的最短子数组
// 暴力 O(N²) → 滑动窗口 O(N)
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, sum = 0, ans = INT_MAX;
for (int right = 0; right < nums.size(); right++) {
sum += nums[right]; // 扩大窗口
while (sum >= target) { // 条件满足,尝试收缩
ans = min(ans, right - left + 1);
sum -= nums[left];
left++;
}
}
return ans == INT_MAX ? 0 : ans;
}
场景3:字符串滑动窗口
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 字符串排列 | 判断s2是否包含s1的排列 | 窗口内字符频率相等 |
| 找所有字母异位词 | 找出字符串中所有异位词子串 | 窗口内字符频率相等 |
| 最小覆盖子串 | 包含T所有字符的最短子串 | 需要跟踪窗口内匹配情况 |
判断标志 :字符串 + 字符频率比较 + 子串包含关系
cpp
// 典型场景:找所有字母异位词
vector<int> findAnagrams(string s, string p) {
vector<int> need(26, 0), window(26, 0);
for (char c : p) need[c - 'a']++;
vector<int> ans;
int left = 0;
for (int right = 0; right < s.size(); right++) {
window[s[right] - 'a']++;
if (right - left + 1 > p.size()) {
window[s[left] - 'a']--;
left++;
}
if (right - left + 1 == p.size() && window == need) {
ans.push_back(left);
}
}
return ans;
}
三、前缀和
核心特征
预处理出从起点到每个位置的累加和,将区间和问题转化为O(1)查询
场景1:频繁区间和查询
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 区域和检索 | 多次查询任意区间l,r的和 | 查询次数多,数组不变 |
| 二维矩阵区域和 | 多次查询子矩阵的和 | 二维前缀和 |
| 除自身外数组的乘积 | 每个位置是其他所有元素的乘积 | 前缀积 + 后缀积 |
判断标志 :多次查询区间和 + 原始数组不频繁修改
cpp
// 典型场景:多次查询区间和
class NumArray {
vector<int> pre;
public:
NumArray(vector<int>& nums) {
pre.resize(nums.size() + 1, 0);
for (int i = 1; i <= nums.size(); i++) {
pre[i] = pre[i - 1] + nums[i - 1]; // 预处理
}
}
int sumRange(int left, int right) {
return pre[right + 1] - pre[left]; // O(1)查询
}
};
场景2:统计子数组个数(前缀和 + 哈希表)
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 和为K的子数组个数 | 统计连续子数组和为k的个数 | 子数组和 = 前缀和之差 |
| 连续子数组和可被K整除 | 统计和能被K整除的子数组 | 同余问题 |
| 连续子数组和为奇数的个数 | 奇偶性统计 | 前缀和奇偶性 |
判断标志 :统计连续子数组个数 + 和满足某个条件 + 不能使用滑动窗口(负数)
cpp
// 典型场景:和为K的子数组个数
// 暴力 O(N²) → 前缀和+哈希表 O(N)
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1;
int sum = 0, ans = 0;
for (int num : nums) {
sum += num;
ans += mp[sum - k]; // 找有多少个前缀和等于 sum - k
mp[sum]++;
}
return ans;
}
场景3:二维前缀和
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 矩阵区域和 | 多次查询子矩阵的和 | 二维矩阵,多次查询 |
| 子矩阵和等于目标值 | 统计和为target的子矩阵个数 | 枚举上下边界 + 哈希表 |
判断标志 :二维矩阵 + 子矩阵和查询 + 查询次数多
四、二分查找(Binary Search)
核心特征
每次将搜索范围缩小一半,O(log N)时间复杂度,前提是序列具有单调性
场景1:标准二分(精确查找)
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 有序数组查找 | 在有序数组中找target | 数组严格有序 |
| 搜索插入位置 | 找到target应插入的位置 | 找第一个≥target的位置 |
| 猜数字大小 | 猜数字游戏 | 1~n范围猜数 |
判断标志 :有序数组 + 找确切值或插入位置
cpp
// 典型场景:有序数组找target
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) l = mid + 1;
else r = mid - 1;
}
return -1;
}
场景2:边界二分(找左右边界)
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 找第一个/最后一个等于target | 有重复元素的有序数组 | 找边界位置 |
| 找第一个大于target | 二分查找边界 | 需要区分左/右边界 |
| 山脉数组峰顶 | 找山脉数组的峰值 | 比较相邻元素决定方向 |
判断标志 :有重复元素 + 需要找第一个或最后一个
cpp
// 典型场景:找第一个等于target的位置
int firstPosition(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] >= target) {
if (nums[mid] == target) ans = mid;
r = mid - 1; // 继续向左找
} else {
l = mid + 1;
}
}
return ans;
}
场景3:二分答案(值域二分)
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 最大化最小值 | 分割数组的最大值 | 在值域上二分,check函数判断 |
| 最小化最大值 | 爱吃香蕉的珂珂 | 速度范围1, max,二分找最小可行速度 |
| 在D天内送达包裹 | 给定天数,求最小运载能力 | 能力范围1, sum,二分找最小 |
| 寻找重复数 | 数组1,n,有一个重复 | 值域二分 O(NlogN) 代替 O(N²) |
判断标志:
题目要求找到满足条件的最大值/最小值答案在某个连续范围内可以写一个check函数判断某个值是否可行
cpp
// 典型场景:爱吃香蕉的珂珂(最小化最大值)
int minEatingSpeed(vector<int>& piles, int h) {
auto can = [&](int k) { // 速度为k能否吃完
long long hours = 0;
for (int p : piles) hours += (p + k - 1) / k;
return hours <= h;
};
int l = 1, r = *max_element(piles.begin(), piles.end());
while (l < r) {
int mid = l + (r - l) / 2;
if (can(mid)) r = mid; // 可以吃完,尝试更慢速度
else l = mid + 1; // 吃不完,需要更快
}
return l;
}
场景4:旋转排序数组
| 场景 | 典型问题 | 关键特征 |
|---|---|---|
| 旋转数组查找 | 在旋转排序数组中找target | 先判断哪边有序 |
| 旋转数组最小值 | 找旋转数组的最小值 | 比较mid与right |
| 寻找峰值 | 找任意一个峰值 | 比较mid与mid+1 |
判断标志 :数组原本有序,但被旋转了 + 找值或找最小/最大
cpp
// 典型场景:旋转排序数组找target
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
if (nums[l] <= nums[mid]) { // 左边有序
if (nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
} else { // 右边有序
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return -1;
}
五、快速判断指南
看关键词 → 选算法
| 关键词 | → | 推荐算法 |
|---|---|---|
| 有序数组、找两数之和 | → | 相向双指针 |
| 原地修改、去重、移动零 | → | 快慢双指针 |
| 连续子数组/子串、求最值 | → | 滑动窗口 |
| 子串包含/覆盖/排列 | → | 滑动窗口+哈希表 |
| 多次查询区间和 | → | 前缀和 |
| 统计子数组个数(含负数) | → | 前缀和+哈希表 |
| 有序数组、找值/插入位置 | → | 标准二分 |
| 有重复、找第一个/最后一个 | → | 边界二分 |
| 最大化最小值、最小化最大值 | → | 二分答案 |
| 值很大(1e9)、暴力会超时、单调性 | → | 二分答案 |
| 旋转数组 | → | 二分(判断有序侧) |