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 j−i+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[i−1] 的元素和。
- 得到前缀和之后, 对于每个开始下标 i i i, 可通过二分查找得到 大于或等于 i i i 的最小下标 bound, 使得 sums [ [ [ bound ] − sums [ i − 1 ] ≥ s ]-\\operatorname{sums}[i-1] \\geq s ]−sums[i−1]≥s,
- 并更新子数组的最小长度 (此时子数组的长度是 bound − ( i − 1 ) -(i-1) −(i−1) ) 。
因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确 性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。
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 项。
-
- l l l 和 r r r 都初始化为 0
- 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
参考
以上是关于209. 长度最小的子数组的主要内容,如果未能解决你的问题,请参考以下文章