持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
今天的每日一题是862. 和至少为 K 的最短子数组 - 力扣(LeetCode),这道题其实可以指利用暴力的方法进行求解,但是利用前缀和和单调队列可以将时间复杂度降至线性级别的时间复杂度。
题目
给你一个整数数组 nums
和一个整数 k
,找出 nums
中和至少为 k
的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1
。
注:子数组是数组中连续的一部分。
示例
输入: nums = [1], k = 1
输出: 1
输入: nums = [1,2], k = 4
输出: -1
输入: nums = [2,-1,2], k = 3
输出: 3
方法
通过题意可以知道,题目希望我们找到一个连续子数组
并使它的和为0
。最常见的方法是我们可以对数组所有子数组进行枚举,即利用双指针的方式对数组进行遍历,在遍历过程中选择出和为k长度最小的子数组
。
但是这样虽然可以解决问题,但是它的时间复杂度是 ,开销过于庞大。因此如何进一步降低时间复杂度呢?
由于我们要计算数组每一段的和,因此为了方便计算。我们可以对数组的前缀和进行计算:
前缀和:假设当前的指向的元素是数组中第
i
个元素, 那么当前元素的前缀和就是从0
到i
的元素和
在得到数组中每一个元素的前缀和preSumArr
之后,任意一段子数组的和
就是前缀和数组中对应子数组起始
和终止
前缀和的差。
因此在遍历数组过程中,如果需要利用前缀和进行计算子数组的和,而和又必须满足大于等于K, 则必须满足所计算出的前缀和是单调递增。
因此在遍历过程中,我们可以利用定义的双端队列来维持前缀和差值的递增。在遍历假设当前指向的数组位置为 i
,根据前缀和数组可以知道它的前缀和为preSumArr[i]
。
- 由于队尾存储的是最早插入的元素,如果当前队列的队尾中存储的前缀和中有小于0的,则将当前的元素弹出。这样队列中一直所维持的就是能满足前缀和的差值递增。
- 由于队列中的元素一定保持递增,这样可以快速筛选出满足大于等于K的元素的位置。
具体代码如下:
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] preSumArr = new long[n + 1];
for (int i = 0; i < n; i++) {
preSumArr[i + 1] = preSumArr[i] + nums[i];
}
int res = n + 1;
Deque<Integer> queue = new ArrayDeque<Integer>();
for (int i = 0; i <= n; i++) {
long curSum = preSumArr[i];
// 队列中满足条件,来筛选出长度最小的数组。
while (!queue.isEmpty() && curSum - preSumArr[queue.peekFirst()] >= k) {
res = Math.min(res, i - queue.pollFirst());
}
// 如果队列存储的前缀和不满足递增,则将元素弹出。
while (!queue.isEmpty() && curSum - preSumArr[queue.peekLast()] <= 0) {
queue.pollLast();
}
queue.offerLast(i);
}
return res < n + 1 ? res : -1;
}