典型算法题解:长度最小的子数组

1、给定一个由 n 个正整数组成的数组 nums 和一个正整数 s。请你找出 nums 中满足其元素之和大于或等于 s 的、长度最小的连续子数组,并返回这个最小长度。如果不存在这样的子数组,则返回 0。例如,对于数组 nums = [2,3,1,2,4,3],输入 s = 7, 则输出 2,即数组 nums 中的连续子数组 [4,3] 的和为 7,是满足条件且长度最小的子数组。要求:

  1. 给出算法的基本设计思想。
  2. 根据设计思想,采用 C 或 C++ 语言描述算法,关键之处给出注释。
  3. 说明所设计算法的时间复杂度和空间复杂度。

【解】这是一个典型的数组子区间问题(长度最小的子数组),可以使用多种方法来解决。最优解法是利用数组元素均为正数的特性,采用滑动窗口技术。同时,也可以使用"前缀和 + 二分查找"的思路来求解。

💡 方法一:前缀和 + 二分查找

【设计思想】

对于数组中每一个可能的结束位置 j,需要找到一个最靠右的开始位置 ii <= j),使得子数组 nums[i...j] 的和大于等于 s

为了快速计算任意子数组的和,先预处理一个前缀和数组 prefix_sum。定义 prefix_sum[k] 为数组 numsk 个元素的和,即 nums[0] + ... + nums[k-1]。那么,子数组 nums[i...j] 的和就可以通过 prefix_sum[j+1] - prefix_sum[i] 在 O(1)O(1)O(1) 时间内计算出来。

如此,问题就转化为:对于每个 i(从 0n-1),找到最小的 j >= i,使得 prefix_sum[j+1] - prefix_sum[i] >= s。这个不等式可以变形为:
prefix_sum[j+1]≥s+prefix_sum[i] prefix\_sum[j+1] \ge s + prefix\_sum[i] prefix_sum[j+1]≥s+prefix_sum[i]

由于 nums 中的元素都是正数,所以 prefix_sum 数组是严格单调递增的。这意味着对于固定的 i,可以在 prefix_sum 数组中(i 之后的部分)通过二分查找来高效地找到满足条件的、最小的 j+1

【算法步骤】

  1. 创建一个长度为 n+1 的前缀和数组 prefix_sum,并计算其值。prefix_sum[0] = 0
  2. 初始化最小长度 min_len 为一个极大值。
  3. 遍历 i0n
    a. 计算目标值 target = s + prefix_sum[i]
    b. 在 prefix_sum 数组中(从 i+1 开始的范围)二分查找第一个大于或等于 target 的位置,记为 j_idx
    c. 如果找到了这样的 j_idx,说明存在一个以 i 为左端点的子数组满足条件。该子数组的右端点索引为 j_idx - 1,长度为 (j_idx - 1) - i + 1 = j_idx - i。更新 min_len = min(min_len, j_idx - i)
  4. 遍历结束后,如果 min_len 未被更新,返回 0,否则返回 min_len

【算法描述】

c++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // for std::min, std::lower_bound
#include <climits>   // for INT_MAX

/**
 * @brief 使用前缀和 + 二分查找求解
 * @param s 目标和
 * @param nums 输入数组
 * @return 最小子数组长度
 * @note 时间复杂度: O(n log n), 空间复杂度: O(n)
 */
int minSubArrayLen_prefix_sum(int s, const std::vector<int>& nums) {
    int n = nums.size();
    if (n == 0) {
        return 0;
    }

    // 1. 计算前缀和数组
    std::vector<long long> prefix_sum(n + 1, 0); // 使用 long long 防止溢出
    for (int i = 0; i < n; ++i) {
        prefix_sum[i + 1] = prefix_sum[i] + nums[i];
    }

    int min_len = INT_MAX;

    // 2. 遍历所有可能的左端点
    for (int i = 0; i <= n; ++i) {
        long long target = s + prefix_sum[i];

        // 3. 在 i 之后的部分二分查找第一个 >= target 的位置
        // std::lower_bound 返回一个指向第一个不小于 target 的元素的迭代器
        auto it = std::lower_bound(prefix_sum.begin() + i + 1, prefix_sum.end(), target);

        if (it != prefix_sum.end()) { // 如果找到了
            // 计算该元素在数组中的索引
            int j_idx = std::distance(prefix_sum.begin(), it);
            min_len = std::min(min_len, j_idx - i);
        }
    }

    return (min_len == INT_MAX) ? 0 : min_len;
}

【算法分析】

  • 时间复杂度:O(nlog⁡n)O(n \log n)O(nlogn)。计算前缀和数组需要 O(n)O(n)O(n)。之后,进行 n 次循环,每次循环内部执行一次二分查找,耗时 O(\\log n) 。所以总时间复杂度是 O(nlog⁡n)O(n \log n)O(nlogn)。
  • 空间复杂度:O(n)O(n)O(n)。需要一个额外的数组来存储前缀和。

💡 方法二:滑动窗口 (最优解)

【设计思想】

滑动窗口(Sliding Window)是一种非常高效的解决连续子数组问题的技巧。可以使用两个指针 startend 来定义一个"窗口",即子数组 nums[start...end]

  1. 扩大窗口:不断移动 end 指针向右,将新元素 nums[end] 加入窗口,并累加窗口内的元素和 current_sum
  2. 检查条件:每当 current_sum >= s 时,就找到了一个满足条件的子数组。此时,记录下它的长度 end - start + 1,并与已知的最小长度进行比较,取较小者。
  3. 收缩窗口:因为想找到长度最小的子数组,所以在找到一个满足条件的窗口后,应该尝试收缩窗口的左边界。从 current_sum 中减去 nums[start],并将 start 指针向右移动。然后再次检查 current_sum 是否仍然满足 >= s 的条件。如果满足,说明找到了一个更短的符合条件的子数组,继续更新最小长度并收缩窗口。如果不满足,则停止收缩,回到第1步,继续扩大窗口。

由于数组中的所有元素都是正数,所以当一个窗口的和满足条件时,扩大窗口只会让和更大,不可能让长度更小。因此,在满足条件时收缩窗口是寻找最小长度的正确策略。

【算法步骤】

  1. 初始化左指针 start = 0,当前和 current_sum = 0,最小长度 min_len 为一个极大值(例如 n + 1 或者无穷大)。
  2. 使用右指针 end 遍历数组从 0n-1
    a. 将 nums[end] 加入 current_sum
    b. 当 current_sum >= s 时,进入一个循环:
    i. 更新最小长度:min_len = min(min_len, end - start + 1)
    ii. 从 current_sum 中减去 nums[start]
    iii. 将 start 指针右移一位 start++
  3. 遍历结束后,如果 min_len 仍然是初始的极大值,说明没有找到任何满足条件的子数组,返回 0。否则,返回 min_len

【算法描述】

c 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // for std::min
#include <climits>   // for INT_MAX

/**
 * @brief 使用滑动窗口求解
 * @param s 目标和
 * @param nums 输入数组
 * @return 最小子数组长度
 * @note 时间复杂度: O(n), 空间复杂度: O(1)
 */
int minSubArrayLen_sliding_window(int s, const std::vector<int>& nums) {
    int n = nums.size();
    if (n == 0) {
        return 0;
    }

    int min_len = INT_MAX;
    int start = 0;
    long long current_sum = 0; // 使用 long long 防止整数溢出

    for (int end = 0; end < n; ++end) {
        current_sum += nums[end];

        // 当窗口和满足条件时,尝试收缩窗口
        while (current_sum >= s) {
            min_len = std::min(min_len, end - start + 1);
            current_sum -= nums[start];
            start++;
        }
    }

    // 如果 min_len 没有被更新过,说明没有找到符合条件的子数组
    return (min_len == INT_MAX) ? 0 : min_len;
}

【算法分析】

  • 时间复杂度:O(n)O(n)O(n)。虽然代码里有嵌套循环,但 startend 指针都各自从头到尾只遍历了数组一次,每个元素最多被访问两次(一次被 end 指针加入,一次被 start 指针移除),所以总时间复杂度是线性的。
  • 空间复杂度:O(1)O(1)O(1)。只使用了几个额外的变量来存储指针和当前和。
相关推荐
_OP_CHEN几秒前
【算法基础篇】(三十一)动态规划之基础背包问题:从 01背包到完全背包,带你吃透背包问题的核心逻辑
算法·蓝桥杯·动态规划·背包问题·01背包·完全背包·acm/icpc
誰能久伴不乏12 分钟前
深入理解 `poll` 函数:详细解析与实际应用
linux·服务器·c语言·c++·unix
长安er18 分钟前
LeetCode876/141/142/143 快慢指针应用:链表中间 / 环形 / 重排问题
数据结构·算法·leetcode·链表·双指针·环形链表
Aaron158823 分钟前
电子战侦察干扰技术在反无人机领域的技术浅析
算法·fpga开发·硬件架构·硬件工程·无人机·基带工程
zhglhy43 分钟前
Jaccard相似度算法原理及Java实现
java·开发语言·算法
workflower1 小时前
PostgreSQL 数据库的典型操作
数据结构·数据库·oracle·数据库开发·时序数据库
仰泳的熊猫1 小时前
1140 Look-and-say Sequence
数据结构·c++·算法·pat考试
Hard but lovely1 小时前
C/C++ ---条件编译#ifdef
c语言·开发语言·c++
闻缺陷则喜何志丹1 小时前
【计算几何】P12144 [蓝桥杯 2025 省 A] 地雷阵|普及+
c++·数学·蓝桥杯·计算几何
handuoduo12341 小时前
SITAN中avp必要性分析
人工智能·算法·机器学习