LeetCode 209 - 长度最小的子数组算法详解

文章目录

LeetCode 209 - 长度最小的子数组算法详解

题目描述

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

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

最优解法:滑动窗口(双指针)

算法思路

这是一个典型的滑动窗口问题。我们使用两个指针维护一个可变长度的窗口:

  • 右指针(right):扩展窗口,增加元素
  • 左指针(left):收缩窗口,移除元素
  • 核心思想:当窗口内元素和 >= target时,尝试收缩左边界来寻找更短的满足条件的子数组

Java解决方案

java 复制代码
public class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int left = 0;              // 左指针,窗口左边界
        int sum = 0;               // 当前窗口内元素和
        int minLen = Integer.MAX_VALUE;  // 记录最小长度
        
        // 右指针遍历数组
        for (int right = 0; right < n; right++) {
            sum += nums[right];    // 扩展窗口,加入右边元素
            
            // 当窗口和 >= target时,尝试收缩左边界
            while (sum >= target) {
                minLen = Math.min(minLen, right - left + 1);
                sum -= nums[left]; // 收缩窗口,移除左边元素
                left++;            // 左指针右移
            }
        }
        
        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }
}

关键变量含义

变量名 含义 作用
left 滑动窗口的左边界索引 控制窗口的起始位置,收缩窗口时右移
right 滑动窗口的右边界索引 在for循环中遍历,扩展窗口
sum 当前窗口内所有元素的和 判断是否满足 >= target 的条件
minLen 满足条件的最小子数组长度 记录并更新找到的最小长度

算法可视化演示

示例:target = 7, nums = [2,3,1,2,4,3]

初始状态:

复制代码
数组: [2, 3, 1, 2, 4, 3]
索引:  0  1  2  3  4  5
left = 0, right = 0, sum = 0, minLen = ∞

第1步:right = 0

复制代码
数组: [2, 3, 1, 2, 4, 3]
       ↑
     left,right
sum = 0 + 2 = 2
sum < target(7),继续扩展

第2步:right = 1

复制代码
数组: [2, 3, 1, 2, 4, 3]
       ↑  ↑
     left right
sum = 2 + 3 = 5
sum < target(7),继续扩展

第3步:right = 2

复制代码
数组: [2, 3, 1, 2, 4, 3]
       ↑     ↑
     left   right
sum = 5 + 1 = 6
sum < target(7),继续扩展

第4步:right = 3

复制代码
数组: [2, 3, 1, 2, 4, 3]
       ↑        ↑
     left      right
sum = 6 + 2 = 8
sum >= target(7)!进入while循环收缩

收缩阶段1:

复制代码
窗口: [2, 3, 1, 2],长度 = 4
minLen = min(∞, 4) = 4
sum = 8 - 2 = 6, left = 1
sum < target,退出while

第5步:right = 4

复制代码
数组: [2, 3, 1, 2, 4, 3]
          ↑        ↑
        left      right
sum = 6 + 4 = 10
sum >= target(7)!进入while循环收缩

收缩阶段2:

复制代码
窗口: [3, 1, 2, 4],长度 = 4
minLen = min(4, 4) = 4
sum = 10 - 3 = 7, left = 2
sum >= target,继续收缩

收缩阶段3:

复制代码
窗口: [1, 2, 4],长度 = 3
minLen = min(4, 3) = 3
sum = 7 - 1 = 6, left = 3
sum < target,退出while

第6步:right = 5

复制代码
数组: [2, 3, 1, 2, 4, 3]
             ↑        ↑
           left      right
sum = 6 + 3 = 9
sum >= target(7)!进入while循环收缩

收缩阶段4:

复制代码
窗口: [2, 4, 3],长度 = 3
minLen = min(3, 3) = 3
sum = 9 - 2 = 7, left = 4
sum >= target,继续收缩

收缩阶段5:

复制代码
窗口: [4, 3],长度 = 2
minLen = min(3, 2) = 2
sum = 7 - 4 = 3, left = 5
sum < target,退出while

最终结果:minLen = 2

代码分支覆盖分析

上述可视化演示覆盖了代码的所有分支:

  1. for循环遍历:right从0到5,遍历整个数组
  2. sum < target分支:步骤1-3展示了sum小于target时继续扩展的情况
  3. sum >= target分支:步骤4-6展示了满足条件时的处理
  4. while循环收缩:展示了多次收缩窗口的过程
  5. minLen更新:展示了长度从4→3→2的更新过程
  6. 最终返回:返回找到的最小长度2

时间复杂度分析

  • 时间复杂度:O(n)

    • 虽然有嵌套的while循环,但每个元素最多被访问两次(一次加入,一次移除)
    • left和right指针都只会单调递增,总的移动次数不超过2n
  • 空间复杂度:O(1)

    • 只使用了常数个额外变量

边界情况处理

  1. 无解情况:如果数组所有元素和都小于target,返回0

    java 复制代码
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
  2. 单元素解:如果数组中某个元素 >= target,最小长度为1

  3. 整个数组:如果需要整个数组才能满足条件,返回数组长度

算法优势

  1. 效率高:一次遍历,O(n)时间复杂度
  2. 空间优:O(1)空间复杂度
  3. 逻辑清晰:双指针滑动窗口,易于理解和实现
  4. 适用性强:可以应用于类似的子数组问题

这个算法体现了滑动窗口技巧的精髓:动态调整窗口大小,在满足条件的前提下寻找最优解

进阶解法:前缀和 + 二分查找(O(n log n))

算法思路

虽然滑动窗口已经是最优解,但题目要求的O(n log n)解法也有其价值:

  • 前缀和:预处理数组,快速计算任意子数组的和
  • 二分查找:对于每个起始位置,二分查找最小的结束位置

Java实现

java 复制代码
public class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        
        // 构建前缀和数组
        int[] prefixSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefixSum[i + 1] = prefixSum[i] + nums[i];
        }
        
        int minLen = Integer.MAX_VALUE;
        
        // 枚举每个起始位置
        for (int i = 0; i < n; i++) {
            // 二分查找最小的结束位置j,使得sum[i...j] >= target
            int targetSum = target + prefixSum[i];
            int pos = binarySearch(prefixSum, targetSum);
            
            if (pos <= n) {
                minLen = Math.min(minLen, pos - i);
            }
        }
        
        return minLen == Integer.MAX_VALUE ? 0 : minLen;
    }
    
    // 二分查找第一个 >= target的位置
    private int binarySearch(int[] arr, int target) {
        int left = 0, right = arr.length - 1;
        int result = arr.length;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] >= target) {
                result = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        return result;
    }
}

算法可视化演示(O(n log n)解法)

示例:target = 7, nums = [2,3,1,2,4,3]

第1步:构建前缀和数组

复制代码
原数组:     [2, 3, 1, 2, 4, 3]
索引:        0  1  2  3  4  5
前缀和数组: [0, 2, 5, 6, 8, 12, 15]
索引:        0  1  2  3  4  5   6

第2步:枚举起始位置i=0

复制代码
需要找到最小的j,使得 prefixSum[j] - prefixSum[0] >= 7
即:prefixSum[j] >= 7 + 0 = 7

二分查找prefixSum数组中第一个 >= 7的位置:
[0, 2, 5, 6, 8, 12, 15]
             ↑
            pos=4
子数组长度 = 4 - 0 = 4

第3步:枚举起始位置i=1

复制代码
需要找到最小的j,使得 prefixSum[j] - prefixSum[1] >= 7
即:prefixSum[j] >= 7 + 2 = 9

二分查找prefixSum数组中第一个 >= 9的位置:
[0, 2, 5, 6, 8, 12, 15]
                ↑
               pos=5
子数组长度 = 5 - 1 = 4

第4步:枚举起始位置i=2

复制代码
需要找到最小的j,使得 prefixSum[j] - prefixSum[2] >= 7
即:prefixSum[j] >= 7 + 5 = 12

二分查找prefixSum数组中第一个 >= 12的位置:
[0, 2, 5, 6, 8, 12, 15]
                ↑
               pos=5
子数组长度 = 5 - 2 = 3

第5步:枚举起始位置i=3

复制代码
需要找到最小的j,使得 prefixSum[j] - prefixSum[3] >= 7
即:prefixSum[j] >= 7 + 6 = 13

二分查找prefixSum数组中第一个 >= 13的位置:
[0, 2, 5, 6, 8, 12, 15]
                    ↑
                   pos=6
子数组长度 = 6 - 3 = 3

第6步:枚举起始位置i=4

复制代码
需要找到最小的j,使得 prefixSum[j] - prefixSum[4] >= 7
即:prefixSum[j] >= 7 + 8 = 15

二分查找prefixSum数组中第一个 >= 15的位置:
[0, 2, 5, 6, 8, 12, 15]
                    ↑
                   pos=6
子数组长度 = 6 - 4 = 2

最终结果:minLen = 2

二分查找过程详解(以i=4为例)

查找prefixSum数组中第一个 >= 15的位置:

复制代码
数组: [0, 2, 5, 6, 8, 12, 15]
索引:  0  1  2  3  4  5   6

第1次二分:
left=0, right=6, mid=3
arr[3]=6 < 15,left = mid+1 = 4

第2次二分:
left=4, right=6, mid=5
arr[5]=12 < 15,left = mid+1 = 6

第3次二分:
left=6, right=6, mid=6
arr[6]=15 >= 15,result=6,right = mid-1 = 5

left > right,结束,返回result=6

两种解法对比

特性 滑动窗口(O(n)) 前缀和+二分(O(n log n))
时间复杂度 O(n) O(n log n)
空间复杂度 O(1) O(n)
实现难度 中等 较高
适用场景 一般推荐 多次查询、离线处理

O(n log n)解法的应用场景

  1. 多次查询:如果需要对同一数组进行多次不同target的查询
  2. 教学价值:展示前缀和与二分查找的结合使用
  3. 变种问题:某些子数组问题可能更适合这种方法

虽然在这个问题中O(n)的滑动窗口解法更优,但掌握O(n log n)的解法有助于理解算法设计的多样性。

相关推荐
珍珠是蚌的眼泪5 小时前
LeetCode_数学
leetcode·计数质数·3的幂·七进制数·十六进制数·三个数的最大乘积
limengshi1383925 小时前
人工智能学习:LR和SVM的联系与区别?
人工智能·算法·机器学习·支持向量机
点云SLAM10 小时前
PyTorch 中.backward() 详解使用
人工智能·pytorch·python·深度学习·算法·机器学习·机器人
only-qi11 小时前
146. LRU 缓存
java·算法·缓存
梁辰兴12 小时前
数据结构:排序
数据结构·算法·排序算法·c·插入排序·排序·交换排序
Lris-KK12 小时前
【Leetcode】高频SQL基础题--1731.每位经理的下属员工数量
sql·leetcode
野犬寒鸦12 小时前
力扣hot100:搜索二维矩阵 II(常见误区与高效解法详解)(240)
java·数据结构·算法·leetcode·面试
菜鸟得菜12 小时前
leecode kadane算法 解决数组中子数组的最大和,以及环形数组连续子数组的最大和问题
数据结构·算法·leetcode
楼田莉子13 小时前
C++算法专题学习——分治
数据结构·c++·学习·算法·leetcode·排序算法