掘金 后端 ( ) • 2024-04-27 15:25

题目:

滑动窗口最大值是一个经典的算法问题,通常在数据流处理和时间序列分析中遇到。这个问题的目标是从一个整数数组中,找到每个大小为 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 支持上述所有双端队列操作,并且其性能通常很适合在需要快速从两端修改数据的场合。

算法实现

  1. 初始化:使用一个双端队列来存储数组 nums 中的元素的索引,而不是元素值本身。这样可以更方便地知道何时一个元素已经不在窗口内了。

  2. 维护队列:遍历数组 nums,对于每个元素做以下操作:

    • 如果队列不为空,且队列头部的索引已经不在窗口内(即索引小于当前元素索引减去窗口大小),则从队列头部移除该索引。
    • 从队列尾部开始,移除所有比当前元素小的元素的索引,因为这些元素不会是窗口的最大值。
    • 将当前元素的索引添加到队列尾部。
    • 如果窗口已经形成(即遍历的元素数量大于等于窗口大小 k),则队列的头部元素就是当前窗口的最大值。
  3. 输出结果:将队列头部的元素(当前窗口的最大值)添加到结果数组中。

现在,让我们用 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 
}

fn main() {
    let nums = vec![1,3,-1,-3,5,3,6,7];
    let k = 3;
    let max_values = max_sliding_window(nums, k);
    println!("{:?}", max_values);  // 输出应为 [3, 3, 5, 5, 6, 7]
}

在这段代码中,我们使用了 Rust 的标准库中的 VecDeque 来作为双端队列。通过在遍历数组的每一步中维护队列,我们能够有效地计算每个窗口的最大值,并将结果存储在 result 向量中。最后,我们在 main 函数中测试了这个函数,以确保其正确性。这样我们就通过双端队列是西安了这个算法,下面我们继续对其进行时间复杂度和空间复杂读的分析

时间复杂度

时间复杂度是 O(n) ,其中 n 是数组的长度。这是因为每个元素只会被加入和移除队列各一次。尽管在每个窗口位置,队列可能会多次进行添加和移除操作,但每个元素最终只会进入和离开队列一次。因此,所有操作加起来的总时间复杂度是线性的。这种算法的效率在于我们维护了一个单调递减的队列,使得我们可以快速得到每个窗口的最大值。

空间复杂度

空间复杂度是 O(k) ,其中 k 是滑动窗口的大小。这是因为双端队列在最坏情况下会存储接近 k 个元素(即当窗口完全滑过数组时)。我们只在队列中保存当前窗口可能的最大值的候选元素的索引,而这些候选元素的数量最多不会超过窗口的宽度 k。Pomelo_刘金。转载请注明原文链接。感谢!