LeetCode每日一练(209, 167)

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int left = 0;
        int right = numbers.size() - 1;
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum == target) {
                return {left + 1, right + 1};
            } else if (sum < target) {
                ++left;
            } else {
                --right;
            }
        }
        return {};
    }
};

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        vector<long long> prefix(n + 1, 0);
        for (int i = 0; i < n; ++i)
            prefix[i + 1] = prefix[i] + nums[i];

        deque<int> dq;
        int res = INT_MAX;

        for (int i = 0; i <= n; ++i) {
            while (!dq.empty() && prefix[i] - prefix[dq.front()] >= target) {
                res = min(res, i - dq.front());
                dq.pop_front();
            }
            while (!dq.empty() && prefix[dq.back()] >= prefix[i]) {
                dq.pop_back();
            }
            dq.push_back(i);
        }
        return res == INT_MAX ? 0 : res;
    }
};

双指针与单调队列两道典型题整理

一、Two Sum II(有序数组的两数之和)------双指针

题意简述

给定一个递增有序数组 numbers,在其中找出两个数,使得它们的和等于 target。返回这两个数的下标(1-based),且保证恰好存在一对解。

解法:双指针

有序数组 + 两数之和,是双指针的典型场景。

  • 左指针 left 指向开头

  • 右指针 right 指向末尾

  • 计算 sum = numbers[left] + numbers[right]

    • sum == target:直接返回下标(注意是 1-based)

    • sum < target:说明和偏小,需要增大和 → 左指针右移

    • sum > target:说明和偏大,需要减小和 → 右指针左移

数组有序这一条件保证:

针对当前 left,所有右侧元素中,随着下标减小,和单调减小;针对当前 right,随着左指针增大,和单调增大。

因此每一步都可以排除一段不可能区间,整体时间复杂度 O(n)。

代码要点

  • 使用 while (left < right),保证每对只枚举一次

  • 返回 {left + 1, right + 1},符合题目 1-based 要求

  • 若题目没有保证答案存在,可以循环结束后返回空数组

时间复杂度:O(n)

空间复杂度:O(1)


二、最短子数组长度(前缀和 + 单调队列)

对应函数:

cpp 复制代码
int minSubArrayLen(int target, vector<int>& nums);

这里的实现不是经典"全正数 + 滑动窗口"的版本,而是前缀和 + 单调队列版本,对负数也适用,如果就是这道题的话,就定义一个sum存left到right的全部和就行,大于就移动left,小的话就移动right。

1. 前缀和构造

定义前缀和数组:

cpp 复制代码
prefix[0] = 0;
prefix[i+1] = prefix[i] + nums[i];   // i = 0..n-1

prefix[i] 表示前 i 个元素之和。

任意区间 [l, r) 的和为:

cpp 复制代码
sum(l, r) = prefix[r] - prefix[l];

问题转化为:

找一对下标 (j, i),满足

cpp 复制代码
prefix[i] - prefix[j] >= target

i - j 尽量小。

i 从左到右扫描,对每个 i,需要找最小的 i - j,同时满足上式。

2. 单调队列设计

使用一个双端队列 dq,存储一组下标 j,并维护以下性质:

  1. 队列中的下标按递增顺序排列:dq.front() < ... < dq.back()

  2. 对应的前缀和值严格单调递增:
    prefix[dq[0]] < prefix[dq[1]] < ... < prefix[dq.back()]

理由:

  • 对当前 i,若想让区间 [j, i) 的和尽量大,优先使用更小的前缀和 prefix[j]

  • 若存在 j1 < j2,且 prefix[j1] >= prefix[j2]

    那么对之后所有 i

    • j1 不会比用 j2 更好(区间更长,和更小或相等)

    • 因此 j1 可以从队列中丢弃

3. 扫描过程

i0n 遍历(注意前缀和长度为 n+1):

  1. 尝试更新答案(用当前前缀和 prefix[i] 作为右端):

    cpp 复制代码
    while (!dq.empty() && prefix[i] - prefix[dq.front()] >= target) {
        res = min(res, i - dq.front());
        dq.pop_front();
    }

    这一步含义:

    • 队首始终是当前可用中最早、且前缀和最小的下标

    • 若已经满足 prefix[i] - prefix[dq.front()] >= target

      说明 [dq.front(), i) 是一个合法区间

    • 为了找到更短区间,持续弹出队首

  2. 维护单调性(保证队列里的 prefix 单调递增):

    cpp 复制代码
    while (!dq.empty() && prefix[dq.back()] >= prefix[i]) {
        dq.pop_back();
    }
    dq.push_back(i);

    这一步是在保证:

    • 对未来的任何 i2 > i,若同时存在:

      • 较旧下标 j_old = dq.back()

      • 较新下标 j_new = i

    • prefix[j_old] >= prefix[j_new]

      j_old 永远不会优于 j_new,可以直接删掉。

整体时间复杂度 O(n),因为每个下标最多入队、出队一次。

4. 完整逻辑小结

  • 利用前缀和将区间和问题转化为差值问题

  • 双端队列按下标递增维护一个"前缀和单调递增"的序列

  • 扫描到每个 i 时:

    • 尝试从队首弹出所有能构成合法区间的下标并更新答案

    • 再维护队列尾部的单调性,加入当前 i

时间复杂度:O(n)

空间复杂度:O(n)

相关推荐
乌萨奇也要立志学C++4 分钟前
【洛谷】递归初阶 三道经典递归算法题(汉诺塔 / 占卜 DIY/FBI 树)详解
数据结构·c++·算法
vyuvyucd22 分钟前
C++引用:高效编程的别名利器
算法
鱼跃鹰飞1 小时前
Leetcode1891:割绳子
数据结构·算法
️停云️1 小时前
【滑动窗口与双指针】不定长滑动窗口
c++·算法·leetcode·剪枝·哈希
charlie1145141911 小时前
嵌入式现代C++教程: 构造函数优化:初始化列表 vs 成员赋值
开发语言·c++·笔记·学习·嵌入式·现代c++
IT=>小脑虎1 小时前
C++零基础衔接进阶知识点【详解版】
开发语言·c++·学习
码农小韩2 小时前
基于Linux的C++学习——指针
linux·开发语言·c++·学习·算法
小L~~~2 小时前
绿盟校招C++研发工程师一面复盘
c++·面试
微露清风2 小时前
系统性学习C++-第十九讲-unordered_map 和 unordered_set 的使用
开发语言·c++·学习
_Lzk666888_2 小时前
洛谷用户2002780求关注
c++·其他