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)

相关推荐
靠沿3 小时前
Java数据结构初阶——LinkedList
java·开发语言·数据结构
Want5953 小时前
C/C++跳动的爱心②
c语言·开发语言·c++
初晴や3 小时前
指针函数:从入门到精通
开发语言·c++
铁手飞鹰4 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表
悦悦子a啊4 小时前
项目案例作业(选做):使用文件改造已有信息系统
java·开发语言·算法
小殊小殊4 小时前
【论文笔记】知识蒸馏的全面综述
人工智能·算法·机器学习
无限进步_4 小时前
C语言动态内存管理:掌握malloc、calloc、realloc和free的实战应用
c语言·开发语言·c++·git·算法·github·visual studio
hweiyu004 小时前
数据结构:循环链表
数据结构·链表
im_AMBER4 小时前
AI井字棋项目开发笔记
前端·笔记·学习·算法