「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」
今天说一下如何计算滑动窗口中的最大值,LeetCode 239
实现思路
暴力解法
不管窗口的长度 k 为多少,在整个整数数组中,窗口的数量为 n - k + 1 (n 为数组长度),我们需要得知窗口的最大值,那么我们可以计算每个窗口中的最大值,就可以得出结果。
但是这种方法存在许多重复计算的地方,因为每次滑动窗口只移动一个元素,所以 k 的值越大,重复计算的越多。所以这种方法不可取。
队列解法
第一步,在窗口不断滑动中,也就是在遍历数组时,如果$nums[j] > $nums[i] (j > i), 那么在窗口滑动的同时,包含j和i的过程中,由于 j > i ,i在j的左侧,并且$nums[j] > $nums[i],所以有i存在时,窗口的最大值为$nums[j],$nums[i]就可以去掉。
我们使用队列存储窗口滑动过程中的下标值,每遍历一个新的元素,我们需要把该元素放入队列中。但是我们先要把新元素和队尾的元素进行比较,如果新元素大于等于队尾元素,我们就可以把队尾元素移除,不断循环这个操作,直到队列为空或者新元素小于队尾元素时。
第二步,由于我们需要的窗口所覆盖的数组内的最大值,并不是遍历到当前数值之前所有的最大值,所以当此数值不在窗口范围内时,也就不需要了,可以去掉。
第三步,我们把每个所在窗口的最大值存储起来,就是最后结果。
完整代码
第345-348行代码,判断数组和窗口长度是否符合规则,如果数组为空,窗口长度小于等于0,窗口长度大于数组长度,则返回空数组。
第350行代码,定义一个队列。
第351行代码,定义一个数组,存储每个窗口的最大值。
第352-366行代码,遍历整个数组。
第353-355行代码,由于遍历第一个元素时,队列为空,所以需要判断队列不为空时才可以循环。如果当前遍历的元素大于等于队列的队尾元素,则把队列的队尾元素移除。
第357行代码,把当前遍历的元素下标放入队列的队尾。
通过前面两个操作,队列内保持一种什么样的形式呢? 由于存储的是元素的下标,下标是按照从小到大的顺序存储在队列中,然后队列中下标对应到数组内的数值,则是从大到小的顺序。
不知道大家对于先把当前元素于队列的队尾比较大小,还是先把当前遍历的元素放入队列中,这两个操作的顺序有没有疑惑?为什么先把当前遍历的元素放入队列中放在后面呢? 是因为如果先把当前遍历的元素放入队列中,则会造成当前元素与自己比较大小,从第一个元素开始,每次都会清空队列,然后队列内一直都是空的,后面就无法进行了。
第359-361行代码,在窗口的移动过程中,队列的队首位置所在的下标有可能不在窗口覆盖的范围内,这时候就需要移除队首位置的下标。通过判断队首位置的下标比较当前元素的下标 $i + 1 - $k, 当窗口滑动到 $i 时,当前元素就是窗口的右边界,那么左边界就是 $i + 1 - $k,小于左边界就是不在窗口范围内。
第363-365行代码,窗口的长度时 k,那么第一个窗口的右边界就是 k - 1,所以从第一个窗口的右边界开始,就可以得出窗口内的最大值,往后每遍历一个元素都可以得到一个新的窗口的最大值。而窗口的最大值就是队列的队首位置。
使用sqlqueue标准库
不知道大家是否了解 sqlqueue,它是一个php中的标准库,SplQueue类就是实现队列操作,通过使用一个双向链表来提供队列的主要功能。
我通过上面的思路,使用sqlqueue实现了一遍,发现使用的时间要少很多。大家可以测试一下。
完整代码
function maxSlidingWindowForth($nums, $k) {
$length = count($nums);
$queue = new \SplQueue();
$result = [];
for ($i = 0; $i < $length; $i++) {
while(!$queue->isEmpty() && $nums[$i] > $nums[$queue->top()]){
$queue->pop();
}
$queue->enqueue($i);
if ($queue->bottom() < ($i + 1 - $k)) {
$queue->dequeue();
}
if ($i + 1 >= $k) {
$result[] = $nums[$queue->bottom()];
}
}
return $result;
}