算法练习之 862. 和至少为 K 的最短子数组

题目描述

返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。

如果没有和至少为 K 的非空子数组,返回 -1 。

示例 1:

输入:A = [1], K = 1
输出:1
示例 2:

输入:A = [1,2], K = 4
输出:-1
示例 3:

输入:A = [2,-1,2], K = 3
输出:3


提示:

1 <= A.length <= 50000
-10 ^ 5 <= A[i] <= 10 ^ 5
1 <= K <= 10 ^ 9

问题分析

1.暴力破解法

看到这个题目的第一想法就是给他几个循环,还能不出答案吗。答案是肯定会出的,只是效率就显得极其低下。
题目叫我们寻找大于等于K的最短连续子数组的长度,我们可以使用两层循环。第一层循环作为寻找子数组头i,第二次循环作为寻找子数组尾j。当A[j]-A[i] >= K时就找到了一个子数组。
暴力破解法也有优化点:
a.首先,当A[i] >=K 时,我们可以直接返回1,因为最短的子数组长度就是1
b.当A[i] <=0 时,我们可以跳过i开头的子数组,i+1开头的最短子数组必定小于等于i开头的最短子数组
c.当我们已经得到一个子数组的长度min时,如果j-i+1 >=min时还没有找到子数组的尾时,我们可以放弃对头为i的子数组的寻找,因为就算寻找到,他的长度也会大于min。

算法代码
func shortestSubarray(A []int, K int) int {
	minLen := -1
	for i := 0; i < len(A); i++ {
		sum := A[i]
		if sum >= K {
			return 1
		}
		if sum <= 0 {
			continue
		}
		for k := i + 1; k < len(A); k++ {
			if k-i+1 >= minLen && minLen != -1 {
				break
			}
			sum += A[k]
			if sum >= K { //如果从iStart到i处的和大于k,则移除istart处的值
				if minLen == -1 || k-i+1 <= minLen {
					minLen = k - i + 1
					break
				}
			}

		}
	}
	return minLen
}

算法的时间复杂度相当于O(n^3),使用数组长度为50000的测试用例给出的时间为 833051150 ns/op,每次操作833051150 纳秒。

2.求和法

暴力破解法我们可以看出对于子数组求和我们做了大量的重复的计算。那么如果我们对于求和这一块进行提前计算会不会降低时间复杂度呢。
在进行求最短子数组时,可以先创建一个数组sum,sum[i]中存放的是A中前i ([0,i))个的和,然后再遍历这个求和数组,找到最短子数组。

算法代码
func shortestSubarray(A []int, K int) int {
	sumA := make([]int, len(A)+1) //数组sumA[i]表示数组A前i个元素之和
	if A == nil || len(A) == 0 {
		sumA[0] = 0
	}
	//初始化sumA
	for i, v := range A {
		if v >= K {
			return 1
		}
		sumA[i+1] = v + sumA[i]
	}
	//fmt.Println(sumA)//{48,99,37,4,-31}
	minL := -1
	//遍历sumA,
	for i := 0; i < len(sumA); i++ {

		for j := i + 1; j < len(sumA); j++ {
			if minL != -1 && j-i >= minL { // 如果i的最短路径超过minL,则直接跳过i
				break
			}
			if sumA[j] <= sumA[i] {
				i = j - 1
				break
			}
			if sumA[j]-sumA[i] >= K { //i到j之间的和是否大于K
				//fmt.Println(sumA[j] -v)
				if minL == -1 || j-i < minL {
					minL = j - i
					//fmt.Println(" ",sumA[j] -v, " ", j," ",i)
					break
				}
			}

		}
	}
	//fmt.Println(minL)
	return minL

}

虽然求和后减少了子数组和的计算,但是时间复杂度并没以后降低多少,几乎还是O(n^3)。使用数组长度为50000的测试用例给出的时间为 833051150 ns/op,每次操作574669700纳秒。

3.求和后的双端队列法

从暴力破解法和求和法的测试我们看出,时间上是有所降低的,说明求和这一部分我们方向是可行的,问题就出在我们对sum这数组的遍历上没有做出优化。
在求和法中,遍历sum数组的主要操作就是判断i-j之间的和是否大于K,还有对A中起点为负数的子数组的剔除。不难发现,我们一次遍历时只能进行一种操作,无法使用上一次遍历所生成的数据(这里生成的数据指的是:子数组起点到结束点之间数据)。要使得寻找子数组操作和剔除起点为负数的子数组同时进行,我们就需要维护一个起点数组head,这个数组中存放了可以当做子数组起点的A的索引。
1.遍历sum数组,将sum数组中的索引当做子数组尾点。再在其中遍历head数组,如果sum[i]-head[0] >=k,则找到一个子数组,并且删除head[0],防止下次遍历。
2.然后就是寻找起点为负数的子数组,我们知道如果sum[i] <= sum[j] (i <j),则j-1必定为负数(或0),此时我们就可以跳过i到j之间的起点,直接对j进行判断。所以我们可以直接删除head中i-j的元素。为了使得j>i,所以我们需要对head数组反向遍历(从数组尾开始遍历),找到sum[i] <= sum[head[len(head)-1]], 然后移除head[len(head)-1]。这样我们就达到了移除起点为负数的子数组了。

算法示意图

在这里插入图片描述

算法代码
func shortestSubarray(A []int, K int) int {
	l := len(A)
	sumA := make([]int, l+1) //数组sumA[i]表示数组A前i个元素之和
	//初始化sumA
	for i, v := range A {
		if v >= K {
			return 1
		}
		sumA[i+1] = v + sumA[i]
	}
	fmt.Println(sumA)
	//fmt.Println(sumA)//{48,99,37,4,-31}
	minL := l + 1
	head := make([]int, 0, 0) //用于存放A中可以作为头进行最短路寻找的索引
	//遍历sumA,
	for i := 0; i < l+1; i++ {

		//遍历head头,寻找A中的最短路
		for len(head) > 0 && sumA[i]-sumA[head[0]] >= K {
			//判断当前head[0]的最短路径是否小于minl
			if minL > i-head[0] {
				minL = i - head[0]
			}
			head = head[1:]
		}
		//移除hand中对应A下的值为负的索引
		for len(head) > 0 && sumA[i] <= sumA[head[len(head)-1]] {
			head = head[:len(head)-1]
		}
		//将索引i添加入head
		head = append(head, i)
	}
	if minL >= l+1 {
		return -1
	}
	return minL
}

上述算法的时间复杂度为O(nlog(n)),使用数组长度为50000的测试用例给出的时间为 563015 ns/op,每次操作563015纳秒。
可以发现时间提高的不止一点两点。但是同样的算法,对于java代码时间却是比go高,这是因为在维护双端队列head时,进行了大量的数组删除操作,所以可以对head进行改进。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值