前言
我们在上文简单了解了一下JS中的队列以及使用队列来解决简单算法题后。在本文我们将介绍如何使用队列优化时间复杂度来解决一道困难难度的力扣算法题
正文
话不多说,上题目
看到这道题目,我们先聊聊我们第一眼看到这道题目时的解题思路1:
这是一个滑动窗口最大值问题,我们可以 定义一个函数
maxSlidingWindow
,接收一个整数数组 nums
和一个整数 k
,表示滑动窗口的大小。 计算数组 nums
的长度,并初始化一个空数组 res
用于存储结果。使用两个指针 i
和 j
分别表示滑动窗口的左右边界,然后使用 while
循环,当右边界 j
小于数组长度时,执行以下操作:
- 调用函数
calMax
计算当前窗口[i, j]
内的最大值。 - 将最大值添加到结果数组
res
中。 - 左右指针同时向右移动一位,即
i++
,j++
。1. - 使用
for
循环遍历窗口内的元素,更新max
变量为窗口内的最大值。
- 返回计算得到的最大值
max
。 - 最后调用
maxSlidingWindow
函数,并传入数组arr
和整数k
,得到滑动窗口中的最大值数组[3, 3, 5, 5, 6, 7]
。
var arr = [1,3,-1,-3,5,3,6,7]
var k = 3
var maxSlidingWindow= function(nums, k) {
const len = nums.length
const res = []
let i = 0, j = k - 1;
while(j < len){
const max = calMax(nums,i,j)
res.push(max)
i++
j++
}
return res
};
function calMax(arr,i,j){
let max = -Infinity
for (let m = i; m <= j ; m++){
if (arr[m] > max){
max = arr[m]
}
}
return max
}
maxSlidingWindow(arr,k) //[3,3,5,5,6,7]
这样我们根据以上的思路可以编辑出这样一段代码,然后我们就去提交运行一下
我们会发现这段代码运行时能通过两个用例,而当判题时系统给出一个长度为5位数的数组时会显示超时。这种方法的主要弊端在于其时间复杂度较高,具体来说有以下几点弊端:
- 时间复杂度高:算法的时间复杂度为 O(nk),其中 n 为数组
nums
的长度,k 为滑动窗口的大小。这是因为在每个窗口中,都需要遍历窗口内的元素来计算最大值,而窗口的大小为 k,因此总共需要遍历 n-k+1 个窗口,每个窗口内的遍历复杂度为 O(k),因此总体复杂度为 O(nk)。这种时间复杂度会在数组较大或滑动窗口较大时表现出较大的性能开销。 - 重复计算:在每个窗口中,都需要重新计算窗口内的最大值,即调用
calMax
函数。这意味着在相邻窗口之间会有大量元素重复计算,导致算法效率低下。 - 不必要的内存占用:虽然算法只需要一个数组来存储最终结果,但在每个窗口中都需要额外的计算空间来存储窗口内的元素,因此会占用额外的内存空间,尤其在数组较大或滑动窗口较大时,内存占用会比较高。
综上所述,虽然这种方法简单易懂,但在实际应用中由于时间复杂度高、重复计算和内存占用等问题,可能不太适合处理大规模数据或性能要求较高的场景。
既然这种方法不行,那我们看看方法2
我们就换一个思路,假设窗口不动,然后让数组里的数字“流”过从窗口
var arr = [1, 3, -1, -3, 5, 3, 6, 7]
var k = 3
//[6]
var maxSlidingWindow = function (nums, k) {
const len = nums.length
const res = []
const queue = [] //维护一个递减数列
for (let i = 0; i < len; i++) {
while (queue.length && nums[queue[queue.length - 1]] < nums[i]) {
queue.pop()
}
queue.push(i)
//需要有值流出窗口
while (queue.length && queue[0] <= i - k) {
queue.shift()
}
//该记录最大值了
if (i >= k - 1) {
res.push(nums[queue[0]])
}
}
return res
}
这段代码实现了一种利用单调队列求解滑动窗口最大值的算法。让我解释一下它的工作原理:
-
创建一个空数组
res
用于存储最终结果。 -
创建一个空队列
queue
,该队列用于维护一个递减的数列,队列中保存的是数组nums
中元素的索引。 -
循环遍历数组
nums
,对于每个元素nums[i]
,执行以下操作:- 首先,通过一个
while
循环,不断地将队列中小于当前元素nums[i]
的元素的索引从队列的尾部弹出,直到队列为空或者队尾元素对应的元素大于等于当前元素。 - 将当前元素的索引
i
入队。 - 接着,通过另一个
while
循环,检查队列的队首元素对应的索引是否已经超出了当前窗口的范围(即队首元素对应的索引小于等于i - k
)。如果是,则将队首元素出队,因为它已经不在当前窗口内。这一步操作保证了队列中的元素都在当前滑动窗口内。 - 如果当前遍历的索引
i
大于等于k - 1
,即窗口已经形成了一个完整的长度为k
的窗口,那么队列的队首元素对应的数组中的值就是当前窗口内的最大值。将该值加入到结果数组res
中。
- 首先,通过一个
-
循环结束后,返回结果数组
res
。
总结
这种方法的关键在于通过维护一个递减的数列来实时获取当前窗口内的最大值,避免了重复计算和额外的内存占用,因此在效率上更加优秀 单调队列作为一种经典的数据结构,在解决滑动窗口最大值问题上展现了其强大的应用价值。同时,这道题目也提醒我们,在解决实际问题时,需要不断地思考和尝试各种算法思路,以找到最优的解决方案。通过学习和掌握这些经典算法,我们可以更好地应对各种挑战,提高自己的编程能力和解决问题的能力。