力扣解题-209. 长度最小的子数组

4 阅读6分钟

力扣解题-209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]

输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]

输出:0

提示:

1 <= target <= 10⁹

1 <= nums.length <= 10⁵

1 <= nums[i] <= 10⁴

进阶: 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

Related Topics

数组、二分查找、前缀和、滑动窗口


示例解答

解题思路

核心方法:滑动窗口(双指针)法,通过右指针扩展窗口、左指针收缩窗口,在一次遍历中找到满足条件的最小长度子数组,时间复杂度O(n)、空间复杂度O(1),是本题的最优解法。

核心原理铺垫(滑动窗口的合理性)

由于数组中所有元素都是正整数,因此窗口内的和具有“单调性”:

  • 右指针右移 → 窗口扩大 → 窗口和递增;
  • 左指针右移 → 窗口缩小 → 窗口和递减。 基于此特性,可通过“扩大窗口找满足条件的解,缩小窗口优化解”的思路,避免暴力枚举所有子数组。
核心逻辑拆解
  1. 变量定义
    • sum:当前滑动窗口内的元素和(初始为0);
    • left:窗口左边界(初始为0);
    • min:记录满足条件的最小窗口长度(初始为Integer.MAX_VALUE,表示未找到);
  2. 右指针扩展窗口
    • 遍历右指针right,将nums[right]加入sum,扩大窗口;
  3. 左指针收缩窗口
    • sum ≥ target时,说明当前窗口满足条件,进入循环收缩左边界:
      • 计算当前窗口长度right-left+1,更新min为更小值;
      • sum中减去nums[left],左指针右移(缩小窗口),尝试找到更短的满足条件的窗口;
  4. 结果处理
    • min仍为Integer.MAX_VALUE(未找到满足条件的子数组),返回0;
    • 否则返回min
具体步骤(以示例1 target=7,nums=[2,3,1,2,4,3]为例)
rightnums[right]sumsum≥target窗口范围窗口长度min左指针收缩操作
022[0,0]-MAX
135[0,1]-MAX
216[0,2]-MAX
328[0,3]44sum-2=6,left=1(sum<7,退出收缩)
4410[1,4]44sum-3=7 → 窗口长度3(min=3),sum-1=6,left=3(sum<7,退出)
539[3,5]33sum-2=7 → 窗口长度2(min=2),sum-4=3,left=5(sum<7,退出)
最终min=2,返回2,与示例结果一致。
性能说明
  • 时间复杂度:O(n)(每个元素最多被右指针访问一次、左指针访问一次,总操作数为2n);
  • 空间复杂度:O(1)(仅使用3个变量,无额外数组/集合开销);
  • 核心优势:
    1. 利用正整数的单调性,避免暴力法O(n²)的时间复杂度;
    2. 一次遍历完成所有操作,无冗余计算;
    3. 空间开销极小,适合处理n=10⁵的大数据量。
public int minSubArrayLen(int target, int[] nums) {
        int sum=0;
        int left=0;
        int min=Integer.MAX_VALUE;
        for(int right=0;right<nums.length;right++){
            sum=sum+nums[right];
            while(sum>=target){
                min=Math.min(min,right-left+1);
                sum=sum-nums[left];
                left++;
            }
        }
        // 如果 min 未被更新,说明无解
        return min == Integer.MAX_VALUE ? 0 : min;
    }
拓展解法:前缀和+二分查找法(进阶要求)

核心方法:前缀和数组 + 二分查找,先构建前缀和数组,再对每个前缀和,通过二分查找找到满足“前缀和差值≥target”的最小下标,时间复杂度O(n logn),满足进阶要求。

核心原理
  1. 前缀和数组prefix[i]表示前i个元素的和(prefix[0]=0prefix[1]=nums[0]prefix[2]=nums[0]+nums[1]...),前缀和数组严格递增(因元素为正整数);
  2. 二分查找:对于每个prefix[right],查找最小的left使得prefix[right] - prefix[left] ≥ target,此时子数组长度为right-left
  3. 最小长度:遍历所有right,记录最小的right-left
代码实现
import java.util.Arrays;

public int minSubArrayLen(int target, int[] nums) {
    int n = nums.length;
    int[] prefix = new int[n + 1];
    // 构建前缀和数组
    for (int i = 0; i < n; i++) {
        prefix[i + 1] = prefix[i] + nums[i];
    }
    
    int min = Integer.MAX_VALUE;
    // 遍历每个前缀和,二分查找满足条件的最小left
    for (int right = 1; right <= n; right++) {
        int targetSum = prefix[right] - target;
        // 在前缀和数组的[0, right]范围内找≥targetSum的最小下标
        int left = Arrays.binarySearch(prefix, 0, right, targetSum);
        // 处理binarySearch的返回值(未找到时返回-(插入点)-1)
        if (left < 0) {
            left = -left - 1;
        }
        // 找到有效left,更新最小长度
        if (left < right) {
            min = Math.min(min, right - left);
        }
    }
    
    return min == Integer.MAX_VALUE ? 0 : min;
}
性能说明
  • 时间复杂度:O(n logn)(构建前缀和O(n),遍历+二分查找O(n logn));
  • 空间复杂度:O(n)(存储前缀和数组);
  • 适用场景:满足进阶要求,适合理解“前缀和+二分”的解题思路,虽性能略低于滑动窗口,但拓展性更强(如处理非正整数数组时,滑动窗口不再适用,二分法仍可尝试)。
对比:暴力法(基础思路,仅作参考)

核心方法:枚举所有子数组,遍历每个起点,累加元素和直到≥target,记录最小长度,逻辑直观但性能极差。

代码实现
public int minSubArrayLen(int target, int[] nums) {
    int n = nums.length;
    int min = Integer.MAX_VALUE;
    // 枚举所有起点
    for (int i = 0; i < n; i++) {
        int sum = 0;
        // 枚举终点
        for (int j = i; j < n; j++) {
            sum += nums[j];
            if (sum >= target) {
                min = Math.min(min, j - i + 1);
                break; // 元素为正,后续更长,无需继续
            }
        }
    }
    return min == Integer.MAX_VALUE ? 0 : min;
}
性能说明
  • 时间复杂度:O(n²)(最坏情况每个起点都要遍历到数组末尾),n=10⁵时会超时;
  • 空间复杂度:O(1);
  • 适用场景:仅适合理解问题本质,实际工程中绝对不推荐使用。

总结

  1. 滑动窗口法(最优解):O(n)时间+O(1)空间,利用正整数的单调性,一次遍历完成,工程首选;
  2. 前缀和+二分查找法:O(n logn)时间+O(n)空间,满足进阶要求,拓展性更强;
  3. 暴力法:O(n²)时间+O(1)空间,仅作思路参考,性能极差;
  4. 关键技巧:
    • 滑动窗口的核心:利用“正整数和的单调性”,收缩左边界优化解;
    • 二分查找的核心:前缀和数组的严格递增性,保证二分的有效性;
    • 边界处理:初始min设为极大值,最终需判断是否找到有效解(返回0或min)。