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

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)。只使用了几个额外的变量来存储指针和当前和。
相关推荐
我有一些感想……7 小时前
浅谈 BSGS(Baby-Step Giant-Step 大步小步)算法
c++·算法·数论·离散对数·bsgs
j_xxx404_7 小时前
C++ STL:string类(3)|operations|string类模拟实现|附源码
开发语言·c++
麦麦大数据7 小时前
F042 A星算法课程推荐(A*算法) | 课程知识图谱|课程推荐vue+flask+neo4j B/S架构前后端分离|课程知识图谱构造
vue.js·算法·知识图谱·neo4j·a星算法·路径推荐·课程推荐
贝塔实验室8 小时前
LDPC 码的度分布
线性代数·算法·数学建模·fpga开发·硬件工程·信息与通信·信号处理
快手技术8 小时前
端到端短视频多目标排序机制框架 EMER 详解
算法
Wenhao.8 小时前
LeetCode LRU缓存
算法·leetcode·缓存·golang
京东零售技术8 小时前
告别 “盲买”!京东 AI 试穿 Oxygen Tryon:让服饰购物从“想象”到“所见即所得”
算法
小白菜又菜8 小时前
Leetcode 2273. Find Resultant Array After Removing Anagrams
算法·leetcode·职场和发展
milanyangbo8 小时前
谁生?谁死?从引用计数到可达性分析,洞悉GC的决策逻辑
java·服务器·开发语言·jvm·后端·算法·架构