LeetCode —— 209. 长度最小的子数组

141 阅读3分钟

启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情

该题是数组长度最小的子数组题型第一题。

题目来源

34. 在排序数组中查找元素的第一个和最后一个位置(LeetCode)

题目描述(中等

给定一个含有 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<=1091 <= target <= 10^9
  • 1<=nums.length<=1051 <= nums.length <= 10^5
  • 1<=nums[i]<=1051 <= nums[i] <= 10^5

进阶

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

leetcode.cn/submissions…

题目解析

数组 nums 中的每个下标都可以作为子数组的开始下标,对于每个开始下标 i ,需要找到大于或等于 i 的最小下标 j ,使得 [i, j] 区间的所有元素和大于或等于 target ,并更新子数组的最小长度 j - i + 1

滑动窗口(双指针)

该方法定义慢指针 slow 和快指针 fast 两个指针,维护变量 sum 作为子数组的元素和( nums[slow]nums[fast] 的元素和 )。

明确快慢指针的用处:

  • 慢指针( slow ):子数组的 开始位置
  • 快指针( fast ):子数组的 结束位置遍历数组

初始状态下, slowfast 都指向下标 0sum 的值为 0 ,子数组最小长度 res 值为 Number.MAX_VALUE (便于判断数组是否有满足条件的最小长度)。

每次迭代中,将 nums[fast] 加到 sum ,如果 sum >= target ,则更新子数组的最小长度(判断此时 fast - slow + 1 是否比 res 小,如果 fast - slow + 1 小于 res ,更新 res 值为 fast -slow + 1 ,反之,不更新),然后将 nums[slow]sum 中减去,随后 slow 右移(加1),直到 sum < target 。每次迭代结束时,fast 右移(加1)。

代码

/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function(target, nums) {
    let slow = 0, fast = 0, length = nums.length, res = Number.MAX_VALUE, sum = 0
    while (fast <= length) {
        if (sum >= target) {
            res = Math.min(res, fast - slow)
            sum -= nums[slow]
            slow++
        }else {
            if (fast === length) {
                break
            }
            sum += nums[fast]
            fast++
        }
    }
    return res === Number.MAX_VALUE ? 0 : res
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

如图:

image.png

代码(优化)

/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function(target, nums) {
    let slow = 0, fast = 0, length = nums.length, res = Number.MAX_VALUE, sum = 0
    while (fast <= length) {
        sum += nums[fast]
        while (sum >= target) {
            res = Math.min(res, fast - slow + 1)
            sum -= nums[slow]
            slow++
        }
        fast++
    }
    return res === Number.MAX_VALUE ? 0 : res
};

如图:

image.png

前缀 + 二分查找

该方法为了使用二分查找,需要额外创建一个数组 sum 用于存储数组 nums 的前缀和,其中 sum[i] 表示从 nums[0]nums[i] 的元素和。得到前缀和之后,对于每个开始下标 i ,可通过二分查找得到大于或等于 i 的最小下标 bound ,使得 sum[bound] - sum[i] >= target ,并更新子数组的最小长度。

sum[0] 代表 0 个元素和。

因为该题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分查找的正确性。如果题目中没有说明数组中每个元素都为正,这里就不能使用二分查找找到这个位置了。

代码

/**
 * @param {number} target
 * @param {number[]} nums
 * @return {number}
 */
var minSubArrayLen = function(target, nums) {
    let sum = [0], length = nums.length, res = Number.MAX_VALUE
    for (let i = 1;i <= length;i++) {
        sum.push(sum[i - 1] + nums[i - 1])
    }
    for (let i = 0;i <= length;i++) {
        let bound = search(sum, 0, length, target + sum[i])
        if (bound != -1){
            res = Math.min(res, bound - i + 1)
        }
    }
    return res === Number.MAX_VALUE ? 0 : res
};

var search = function(sum, left, right, target) {
    if (sum[right] < target) {
        return -1
    }
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if (sum[mid] >= target) {
            right = mid - 1
        }else {
            left = mid + 1
        }
    }
    return left - 1
}
  • 时间复杂度:O(nlogn)O(n \log n)
  • 空间复杂度:O(n)O(n)

如图:

image.png

执行用时和内存消耗仅供参考,大家可以多提交几次。如有更好的想法,欢迎大家提出。