Rust算法题:用双端队列解决滑动窗口最大值
题目:
滑动窗口最大值是一个经典的算法问题,通常在数据流处理和时间序列分析中遇到。这个问题的目标是从一个整数数组中,找到每个大小为 k
的滑动窗口的最大值。给定一个数组 nums
和窗口大小 k
,你需要返回一个数组,包含每个窗口的最大值。
示例
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
再看文章讲解之前,自己可以尝试写一下:力扣题目链接
题目分析
这个问题可以用几种方法解决,但一个非常高效的方法是使用双端队列(deque)。双端队列可以从两端高效地添加和移除元素,这使得它非常适合解决滑动窗口问题。下面是具体的算法步骤:
双端队列的主要操作包括:
- push_front: 在队列的前端添加一个元素。
- pop_front: 移除并返回队列前端的元素。
- push_back: 在队列的后端添加一个元素。
- pop_back: 移除并返回队列后端的元素。
- front: 访问队列前端的元素。
- back: 访问队列后端的元素。
Rust中的双端队列
在 Rust 语言中,双端队列的实现是通过标准库中的 std::collections::VecDeque
提供的。VecDeque
是基于一个可增长的环形缓冲区,这使得它在两端添加或删除元素时都具有较高的效率。VecDeque
支持上述所有双端队列操作,并且其性能通常很适合在需要快速从两端修改数据的场合。
算法实现
-
初始化:使用一个双端队列来存储数组
nums
中的元素的索引,而不是元素值本身。这样可以更方便地知道何时一个元素已经不在窗口内了。 -
维护队列:遍历数组
nums
,对于每个元素做以下操作:- 如果队列不为空,且队列头部的索引已经不在窗口内(即索引小于当前元素索引减去窗口大小),则从队列头部移除该索引。
- 从队列尾部开始,移除所有比当前元素小的元素的索引,因为这些元素不会是窗口的最大值。
- 将当前元素的索引添加到队列尾部。
- 如果窗口已经形成(即遍历的元素数量大于等于窗口大小
k
),则队列的头部元素就是当前窗口的最大值。
-
输出结果:将队列头部的元素(当前窗口的最大值)添加到结果数组中。
现在,让我们用 Rust 来实现这个算法:
pub fn max_sliding_window(nums: Vec<i32>, k: i32) -> Vec<i32> {
use std::collections::VecDeque;
let mut res = vec![];
let mut queue = VecDeque::with_capacity(k as usize);
for (i, &v) in nums.iter().enumerate() {
// 如果队列长度超过 k,那么需要移除队首过期元素
if i - queue.front().unwrap_or(&0) == k as usize {
queue.pop_front();
}
while let Some(&index) = queue.back() {
if nums[index] >= v {
break;
}
// 如果队列第一个元素比当前元素小,那么就把队列第一个元素弹出
queue.pop_back();
}
// 将当前元素索引加入队尾
queue.push_back(i);
// 当窗口的长度为k时,队首元素即为当前窗口的最大值
if i >= k as usize - 1 {
res.push(nums[queue[0]]);
}
}
res
}
在这段代码中,我们使用了 Rust 的标准库中的 VecDeque
来作为双端队列。通过在遍历数组的每一步中维护队列,我们能够有效地计算每个窗口的最大值,并将结果存储在 result
向量中。 下面我们继续对其进行时间复杂度和空间复杂读的分析
下面我将用表格的形式说明如何一步步使用双端队列处理数组 [1, 3, -1, -3, 5, 3, 6, 7]
,窗口大小为3的例子。
滑动窗口最大值演示
Step | Current Window | Deque State (Indexes) | Deque Values | Output Value (Max) |
---|---|---|---|---|
1 | [1] | [0] | [1] | - |
2 | [1, 3] | [1] | [3] | - |
3 | [1, 3, -1] | [1, 2] | [3, -1] | 3 |
4 | [3, -1, -3] | [1, 2, 3] | [3, -1, -3] | 3 |
5 | [-1, -3, 5] | [4] | [5] | 5 |
6 | [-3, 5, 3] | [4, 5] | [5, 3] | 5 |
7 | [5, 3, 6] | [ 6] | [6] | 6 |
8 | [3, 6, 7] | [7] | [7] | 7 |
分步解释
- Step 1-2: 队列初始化并添加元素。当遇到元素
3
时,由于3
大于1
,所以1
被从队列中移除。 - Step 3: 完成第一个窗口,此时窗口内元素为
[1, 3, -1]
,队列为[1, 2]
,表示窗口中最大值为3
。 - Step 4: 窗口向右移动一位,加入
-3
,移除1
,队列依然保持[1, 2, 3]
,输出仍为最大值3
。 - Step 5: 窗口移动,加入
5
,由于5
大于窗口内所有其他元素,所有前面的元素索引被从队列中移除,只剩[4]
。 - Step 6: 继续移动窗口,加入
3
。由于3
小于5
,所以队列变为[4, 5]
。 - Step 7: 再次移动窗口,加入
6
,此时6
大于3
但小于5
,队列调整为[4, 6]
,但窗口移动使得5
不再在窗口内,因此队列变为[6]
。 - Step 8: 最后一个窗口为
[3, 6, 7]
,加入7
后,所有其他索引都被从队列中移除,因为7
是新的最大值。
时间复杂度
时间复杂度是 O(n) ,其中 n 是数组的长度。这是因为每个元素只会被加入和移除队列各一次。尽管在每个窗口位置,队列可能会多次进行添加和移除操作,但每个元素最终只会进入和离开队列一次。因此,所有操作加起来的总时间复杂度是线性的。这种算法的效率在于我们维护了一个单调递减的队列,使得我们可以快速得到每个窗口的最大值。
空间复杂度
空间复杂度是 O(k) ,其中 k 是滑动窗口的大小。这是因为双端队列在最坏情况下会存储接近 k 个元素(即当窗口完全滑过数组时)。我们只在队列中保存当前窗口可能的最大值的候选元素的索引,而这些候选元素的数量最多不会超过窗口的宽度 k。Pomelo_刘金。转载请注明原文链接。感谢!
转载自:https://juejin.cn/post/7362065407137300519