学习二分查找和双指针的应用
方法一:前缀和 + 二分查找
使用二分查找,则可以将时间优化到 O ( log n ) O(\log n) O(logn)。
为了使用二分查找,需要额外创建一个数组 sums \text{sums} sums 用于存储数组 nums \text{nums} nums 的前缀和,其中 sums [ i ] \text{sums}[i] sums[i] 表示从 nums [ 0 ] \text{nums}[0] nums[0] 到 nums [ i − 1 ] \text{nums}[i-1] nums[i−1] 的元素和。得到前缀和之后,对于每个开始下标 i i i,可通过二分查找得到大于或等于 i i i 的最小下标 bound \textit{bound} bound,使得 sums [ bound ] − sums [ i ] ≥ s \text{sums}[\textit{bound}]-\text{sums}[i] \ge s sums[bound]−sums[i]≥s,并更新子数组的最小长度(此时子数组的长度是 bound − i \textit{bound}-i bound−i)。
因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。
在很多语言中,都有现成的库和函数来为我们实现这里二分查找大于等于某个数的第一个位置的功能,比如 C++ 的 lower_bound
,Java 中的 Arrays.binarySearch
,C# 中的 Array.BinarySearch
,Python 中的 bisect.bisect_left
。
import bisect
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
minLen = n + 1
# 累加数组
sums = [0]
for i in range(n):
sums.append(sums[-1] + nums[i])
for i in range(n):
target = s + sums[i]
bound = bisect.bisect_left(sums, target)
if bound != len(sums):
minLen = min(minLen, bound - i)
return minLen if minLen != n + 1 else 0
复杂度分析
- 时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组的长度。需要遍历每个下标作为子数组的开始下标,遍历的时间复杂度是 O ( n ) O(n) O(n),对于每个开始下标,需要通过二分查找得到长度最小的子数组,二分查找得时间复杂度是 O ( log n ) O(\log n) O(logn),因此总时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
- 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。额外创建数组 sums \text{sums} sums 存储前缀和。
方法二:双指针
定义两个指针 start \textit{start} start 和 end \textit{end} end 分别表示子数组的开始位置和结束位置,维护变量 sum \textit{sum} sum 存储子数组中的元素和(即从 nums [ start ] \text{nums}[\textit{start}] nums[start] 到 nums [ end ] \text{nums}[\textit{end}] nums[end] 的元素和)。
初始状态下, start \textit{start} start 和 end \textit{end} end 都指向下标 0 0 0, sum \textit{sum} sum 的值为 00。
每一轮迭代,将 nums [ e n d ] \text{nums}[end] nums[end] 加到 sum \textit{sum} sum,如果 sum ≥ s \textit{sum} \ge s sum≥s,则更新子数组的最小长度(此时子数组的长度是 end − start + 1 \textit{end}-\textit{start}+1 end−start+1),然后将 nums [ s t a r t ] \text{nums}[start] nums[start] 从 sum \textit{sum} sum 中减去并将 start \textit{start} start 右移,直到 sum < s \textit{sum} < s sum<s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将 end \textit{end} end 右移。
自己写的代码,运行效率不高,可能是由于多次切片求和操作
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
left = 0
right = 1
n = len(nums)
minLen = 2**31
while (left < right) and (left < n) and (right <= n):
cur_sum = sum(nums[left:right])
if cur_sum >= s:
if right - left < minLen:
minLen = right - left
left += 1
else:
right += 1
return minLen if minLen != 2**31 else 0
官方代码
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
left = 0
right = 0
n = len(nums)
minLen = n + 1
cur_sum = 0
while right < n:
cur_sum += nums[right]
while cur_sum >= s:
minLen = min(minLen, right - left + 1)
cur_sum -= nums[left]
left += 1
right += 1
return minLen if minLen != n + 1 else 0
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。指针 start \textit{start} start 和 end \textit{end} end 最多各移动 n n n 次。
- 空间复杂度: O ( 1 ) O(1) O(1)。