209. 长度最小的子数组

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了209. 长度最小的子数组相关的知识,希望对你有一定的参考价值。

209. 长度最小的子数组

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

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

示例 :

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

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

暴力法

暴力法是最直观的方法。初始化子数组的最小长度为无穷大,枚举数组 nums 中的每个下标作为子 数组的开始下标,对于每个开始下标 i i i, 需要找到大于或等于 i i i 的最小下标 j j j, 使得从 n u m s [ i ] \\mathrm{nums}[i] nums[i] nums ⁡ [ j ] \\operatorname{nums}[j] nums[j] 的元素和大于或等于 s s s, 并更新子数组的最小长度(此时子数组的长度是 j − i + 1 j-i+1 ji+1 )。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        if not nums:
            return 0

        n = len(nums)
        ans = n + 1
        for i in range(n):
            total = 0
            for j in range(i, n):
                total += nums[j]
                if total>=target: # 更新长度最小子数列
                    ans = min(ans,j-i+1) #j-i+1 :长度最小
                    break
        return ans

复杂度分析

  • 时间复杂度: O ( n 2 ) O\\left(n^{2}\\right) O(n2), 其中 n n n 是数组的长度。需要遍历每个下标作为子数组的开始下标, 对于 每个开始下标,需要遍历其后面的下标得到长度最小的子数组。
  • 空间复杂度:O(1)。

前缀和 + 二分查找

方法一的时间复杂度是 O ( n 2 ) O\\left(n^{2}\\right) O(n2), 因为在确定每个子数组的开始下标后, 找到长度最小的子数组需要 O ( n ) O(n) O(n) 的时间。如果使用二分查找,则可以将时间优化到 O ( log ⁡ n ) O(\\log n) O(logn)

  • 为了使用二分查找, 需要额外创建一个数组 sums 用于存储数组 nums 的前缀和,其中 sums ⁡ [ i ] \\operatorname{sums}[i] sums[i] 表示 从 nums [ 0 ] [0] [0] nums ⁡ [ i − 1 ] \\operatorname{nums}[i-1] nums[i1] 的元素和。
  • 得到前缀和之后, 对于每个开始下标 i i i, 可通过二分查找得到 大于或等于 i i i 的最小下标 bound, 使得 sums [ [ [ bound ] − sums ⁡ [ i − 1 ] ≥ s ]-\\operatorname{sums}[i-1] \\geq s ]sums[i1]s,
  • 并更新子数组的最小长度 (此时子数组的长度是 bound − ( i − 1 ) -(i-1) (i1)​​ ) 。

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

class Solution:
    def minSubArrayLen(self, target, nums):
        if not nums:
            return 0
        
        n = len(nums)
        ans = n + 1
        sums=[0] #sums[0] = 0 意味着前 0 个元素的前缀和为 0
        for i in range(n): 
            sums.append(nums[i]+sums[-1])
        print(sums)
        for i in range(1, n + 1):
            total = target+sums[i-1]
            bound = self.bisect_left(sums, total)

            if bound != len(sums):
                ans = min(ans, bound - (i - 1))

        return 0 if ans == n + 1 else ans

    def bisect_left(self,sums, target):
        l = 0 
        r = len(sums)-1
        while l<r:
            mid = l +(r-l)//2
            if sums[mid]<target:
                l = mid+1
            else:
                r = mid
        return l if sums[l]>= target else len(sums)

class Solution:
    def minSubArrayLen(self, target: int, nums: list) -> int:
        """
            因为题目给出 1 <= nums[i] <= 105, 那么可以使用前缀和 加二分查找做
            1. 因为元素都大于0,所以前缀和肯定是递增的
            2. 我们以每一个元素为起始位置,二分找第一个大于 target的重点位置, 比较那个最小
            二分查找一次是 logn,  一共查找n次
            那么时间复杂度为 O(nlogn)
        """
        n = len(nums)
        # 1. 计算前缀和, 初始长度为 n + 1, 方便计算. 任意 长度的和,均等于 prefix[i] - prefix[i - 1]
        prefix = [0] * (n + 1)
        for i in range(0, n):
            prefix[i + 1] = prefix[i] + nums[i]

        # 3. 寻找满足条件长度最小子数组
        minLength = n + 1
        # 使 nums 前n个元素都作为开始元素, 计算第一个 大于 target的子数组长
        for start in range(n):
            length = self.binarySreach(prefix, start, target)
            minLength = min(minLength, length)
        
        return minLength if minLength != n + 1 else 0


    # 2. 定义二分查找方法
    def binarySreach(self,nums, start, target):
        """
            1. 这里要理清楚, 假如 nums = 1 2 3 4,  prefix是 0 1 3 6 10 长度是 n + 1.
            2. 依次遍历 nums, start 假如为1, 对应 nums的元素2, 前缀和prefix里对应的是 3, 下标为 2! 计算nums里 2 + 3 + 4 的和, 对应到 prefix里是 prefix[n] - prefix[1], 所以搜索起来是从 start + 1开始到最后
            3. start对应 nums元素下标,  start + 1 对应的是 prefix中, 此元素的前缀和。
        """
        n = len(nums)
        left = start + 1
        # 这里的nums 是 prefix
        right = n - 1
        while left < right:
            middle = (left + right) // 2 
            if nums[middle] - nums[start] >= target:
                right = middle
            else:
                left = middle + 1
        
        # 看最后一个位置 是否符合,有可能小于
        return left - start if nums[left] - nums[start] >= target else n + 1

滑动窗口

  • 连续子数组可以表示为 [ l , r ] [l,r] [l,r]​ :从第 l l l​项到第 r r r​ 项。

    1. l l l r r r 都初始化为 0
    2. r r r 指针移动一步
  • 当窗口 [ l , r ] [l,r] [l,r] > = t a r g e t >= target >=target,如果此时扩张窗口,条件就依然满足,但背离“最小长度”的要求。

    • 所以选择收缩窗口: l l l​ 右移,直到条件不再满足(是一个循环),这是在优化可行解,并让窗口长度挑战最小纪录。
  • 当窗口 [ l , r ] [l,r] [l,r]的和 < t a r g e t < target <target,此时应该扩张窗口, r r r右移,直到条件重新满足。

总结

  • 扩张窗口:为了找到一个可行解,找到了就不再扩张,因为扩张不再有意义。
  • 收缩窗口:在长度上优化该可行解,直到条件被破坏。
  • 继续寻找下一个可行解,然后再优化到不能优化……

class Solution:
    def minSubArrayLen(self, target, nums):
        if not nums:
            return 0
        
        n = len(nums)
        ans = n + 1
        l , r =0,0 
        total = 0
        while r<n:
            total +=nums[r]
            while total >=target: # 收缩窗口
                ans = min(ans,r-l+1)
                total -= nums[l]
                l +=1
            r+=1
        
        return 0 if ans==n+1 else ans
class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left = 0
        sum_ = 0

        n = len(nums)
        ans = n + 1  # 记录最短长度

        for right, value in enumerate(nums):
            sum_ += value
            while sum_ >= target:
                ans = min(ans, right - left + 1)    # 比较之前最短的,和当前长度,取最小
                sum_ -= nums[left]  # 去掉最开头的数,并缩小窗口
                left += 1
        return 0 if ans==n+1 else ans               # 倘若遍历完都没有达到target,
                                                    # 说明没有满足的区间,直接返回0

参考

Krahets - 力扣(LeetCode) (leetcode-cn.com)

以上是关于209. 长度最小的子数组的主要内容,如果未能解决你的问题,请参考以下文章

leetcode 209. 长度最小的子数组

LeetCode.209 长度最小的子数组

LeetCode#209-长度最小的子数组

Java算法 每日一题 编号209:长度最小的子数组

Java算法 每日一题 编号209:长度最小的子数组

LeetCode 209. 长度最小的子数组c++/java详细题解