双指针、滑动窗口、前缀和、二分查找 算法

双指针、滑动窗口、前缀和、二分查找 ------ 应用场景详解

一、双指针

核心特征

两个指针朝着某个方向移动,通常能将 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)、暴力会超时、单调性 二分答案
旋转数组 二分(判断有序侧)

相关推荐
顾北顾2 小时前
多头注意力机制
人工智能·深度学习·算法
H178535090962 小时前
SolidWorks_基于草图的实体特征20_特征错误排查
算法·3d建模·solidworks
hujinyuan201602 小时前
2025年12月中国电子学会青少年机器人技术等级考试试卷(二级) 真题+答案
人工智能·算法·机器人
bIo7lyA8v3 小时前
算法复杂度评估的实验统计方法与可视化的技术8
算法
李老师讲编程3 小时前
中国电子学会图形化2020.12月Scratch三级考级题
算法·scratch·信息学奥赛·图形化编程·scratch素材
退休倒计时3 小时前
【每日一题】LeetCode 53. 最大子数组和 TypeScript
数据结构·算法·leetcode·typescript
旖-旎3 小时前
FloodFill(图像渲染)(1)
c++·算法·深度优先·力扣
戴西软件4 小时前
戴西 DLM 许可授权管理系统:破解无网络环境下工业软件授权难题,助力制造企业降本增效
网络·人工智能·python·深度学习·程序人生·算法·制造