LeetCode 209.长度最小的子数组【中等】

60 阅读3分钟

题干

给定一个含有 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

题解

最简单的是暴力法,遍历每一个可能的子数组起点,对每一个起点从左到右寻找第一个满足和大于等于target的终点下标,并维护一个minLen记录最小的满足条件的子数组长度。算法时间复杂度为O(n^2),空间复杂度为O(1)。

var maxLen int = int(math.Pow(10, 5)) + 1

func minSubArrayLen(target int, nums []int) int {
	n := len(nums)
	minLen := maxLen
	for i := 0; i < n; i++ {
		sum := 0
		for j := i; j < n; j++ {
			sum += nums[j]
			if sum >= target {
				minLen = min(j-i+1, minLen)
				break
			}
		}
	}
	if minLen == maxLen {
		return 0
	}

	return minLen
}

相较于暴力,更好一点的做法是在寻找满足条件的终点时,使用二分查找。二分查找要求数组有序,注意到数组中的每个元素都是非负的,所以数组的前缀和数组一定是递增的,满足二分查找的条件。设前缀和数组为sumssums[i]nums[0] ~ nums[i-1]的和,sums[0] = 0,对于任意一个起点i,可以使用二分查找寻找一个最小的终点j满足sums[j]-sums[i] >= target。算法的时间复杂度为O(nlogn),空间复杂度为O(n)。

var maxLen int = int(math.Pow(10, 5)) + 1

func minSubArrayLen(target int, nums []int) int {
	n := len(nums)
	minLen := maxLen
	// 计算前缀和数组,sums[0] = 0
	sums := make([]int, n+1)
	for i := 1; i <= n; i++ {
		sums[i] = sums[i-1] + nums[i-1]
	}

	// 遍历所有可能的子数组起点,进行二分搜索使得和>=target的最小终点
	for i := 0; i < n; i++ {
		left, right := i+1, n
		for left < right {
			mid := (left + right) >> 1
			if sums[mid]-sums[i] >= target {
				right = mid
			} else {
				left = mid + 1
			}
		}
		// 注意这里可能二分查找没有找到符合条件的终点,不能计入结果
		if sums[right]-sums[i] >= target {
			minLen = min(minLen, right-i)
		}
	}
	if minLen == maxLen {
		return 0
	}

最好的做法是滑动窗口,维护一个滑动窗口,滑动窗口的左右指针即为子数组的范围,对于每一个滑动窗口位置,我们计算子数组元素的和sum,当sum >= target时,说明当前滑动窗口是满足条件的,但是它不一定是最小的,所以我们将滑动窗口的左指针右移;当sum < target的时候,说明当前滑动窗口不满足条件,需要加上更多的数,因此我们将滑动窗口的右指针右移。算法的时间复杂度为O(n),空间复杂度为O(1)。

var maxLen int = int(math.Pow(10, 5)) + 1

// 滑动窗口
func minSubArrayLen(target int, nums []int) int {
	sum := 0
	// 维护滑动窗口的左右端点,初始值为0和-1
	left, right := 0, -1
	n := len(nums)
	minLen := maxLen
	for right < n {
		// 当前和>=target的时候,计算当前子数组长度,并将滑动窗口的左指针右移
		// 当前和<target的时候,将滑动窗口右指针右移
		if sum >= target {
			minLen = min(right-left+1, minLen)
			sum -= nums[left]
			left++
			if left > right {
				right = left
			}
		} else {
			right++
			if right == n {
				break
			}
			sum += nums[right]
		}
	}
	if minLen == maxLen {
		return 0
	}
	return minLen
}