1、给定一个由 n 个正整数组成的数组 nums 和一个正整数 s。请你找出 nums 中满足其元素之和大于或等于 s 的、长度最小的连续子数组,并返回这个最小长度。如果不存在这样的子数组,则返回 0。例如,对于数组 nums = [2,3,1,2,4,3],输入 s = 7, 则输出 2,即数组 nums 中的连续子数组 [4,3] 的和为 7,是满足条件且长度最小的子数组。要求:
- 给出算法的基本设计思想。
- 根据设计思想,采用 C 或 C++ 语言描述算法,关键之处给出注释。
- 说明所设计算法的时间复杂度和空间复杂度。
【解】这是一个典型的数组子区间问题(长度最小的子数组),可以使用多种方法来解决。最优解法是利用数组元素均为正数的特性,采用滑动窗口技术。同时,也可以使用"前缀和 + 二分查找"的思路来求解。
💡 方法一:前缀和 + 二分查找
【设计思想】
对于数组中每一个可能的结束位置 j,需要找到一个最靠右的开始位置 i(i <= j),使得子数组 nums[i...j] 的和大于等于 s。
为了快速计算任意子数组的和,先预处理一个前缀和数组 prefix_sum。定义 prefix_sum[k] 为数组 nums 前 k 个元素的和,即 nums[0] + ... + nums[k-1]。那么,子数组 nums[i...j] 的和就可以通过 prefix_sum[j+1] - prefix_sum[i] 在 O(1)O(1)O(1) 时间内计算出来。
如此,问题就转化为:对于每个 i(从 0 到 n-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。
【算法步骤】
- 创建一个长度为 n+1的前缀和数组prefix_sum,并计算其值。prefix_sum[0] = 0。
- 初始化最小长度 min_len为一个极大值。
- 遍历 i从0到n:
 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)。
- 遍历结束后,如果 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(nlogn)O(n \log n)O(nlogn)。计算前缀和数组需要 O(n)O(n)O(n)。之后,进行 n次循环,每次循环内部执行一次二分查找,耗时 O(\\log n) 。所以总时间复杂度是 O(nlogn)O(n \log n)O(nlogn)。
- 空间复杂度:O(n)O(n)O(n)。需要一个额外的数组来存储前缀和。
💡 方法二:滑动窗口 (最优解)
【设计思想】
滑动窗口(Sliding Window)是一种非常高效的解决连续子数组问题的技巧。可以使用两个指针 start 和 end 来定义一个"窗口",即子数组 nums[start...end]。
- 扩大窗口:不断移动 end指针向右,将新元素nums[end]加入窗口,并累加窗口内的元素和current_sum。
- 检查条件:每当 current_sum >= s时,就找到了一个满足条件的子数组。此时,记录下它的长度end - start + 1,并与已知的最小长度进行比较,取较小者。
- 收缩窗口:因为想找到长度最小的子数组,所以在找到一个满足条件的窗口后,应该尝试收缩窗口的左边界。从 current_sum中减去nums[start],并将start指针向右移动。然后再次检查current_sum是否仍然满足>= s的条件。如果满足,说明找到了一个更短的符合条件的子数组,继续更新最小长度并收缩窗口。如果不满足,则停止收缩,回到第1步,继续扩大窗口。
由于数组中的所有元素都是正数,所以当一个窗口的和满足条件时,扩大窗口只会让和更大,不可能让长度更小。因此,在满足条件时收缩窗口是寻找最小长度的正确策略。
【算法步骤】
- 初始化左指针 start = 0,当前和current_sum = 0,最小长度min_len为一个极大值(例如n + 1或者无穷大)。
- 使用右指针 end遍历数组从0到n-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++。
- 遍历结束后,如果 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)。虽然代码里有嵌套循环,但 start和end指针都各自从头到尾只遍历了数组一次,每个元素最多被访问两次(一次被end指针加入,一次被start指针移除),所以总时间复杂度是线性的。
- 空间复杂度:O(1)O(1)O(1)。只使用了几个额外的变量来存储指针和当前和。