hot100_滑动窗口最大值

滑动窗口最大值题解

hot100_滑动窗口最大值

分析

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

很明显,我们可以统计窗口的最大值,然后再滑动的时候,去掉窗口最左侧的值并且加入下一个值后,再得出更新后的最大值。


题解

优先队列

为了统计最大值,我们很容易想到最大堆这种数据结构进行处理,这里使用优先队列priority_queue实现,但是它没办法随着窗口的移动即时删除窗口最左侧的元素,所以这里我们要采用添加元素的同时记录元素的下标,延迟删除元素:

  • 堆顶元素下标<= i - k→代表已经离开窗口,pop丢掉,直到堆顶合法。
  • 合法堆顶就是当前窗口最大值。

代码

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        priority_queue<pair<int, int>> pq;
        int n = nums.size();
        vector<int> ans;
        for(int i = 0; i < k; i++) pq.emplace(nums[i], i);
        ans.push_back(pq.top().first);
        for(int i = k; i < n; i++) {
            pq.emplace(nums[i], i);
            while(pq.top().second <= i - k) pq.pop();
            ans.push_back(pq.top().first);
        }
        return ans;
    }
};

时间复杂度:O(nlogn)O(nlogn),每个元素入堆一次,出堆最多一次。

单调队列

dq保存数组下标,保证队列里下标对应的数值从队头到队尾严格递减:

  • 队头:当前窗口最大值的下标
  • 新元素进来,把队列尾部所有 ≤\le 当前值的下标全部踢出去:那些数比当前小,又在当前数左边,永远不可能成为后面窗口的最大值,可以直接丢弃。

举例子:窗口 [3,-1,-3],队列存下标 0(3),1(-1),2(-3);遇到新元素 5,-3、-1都比 5 小,全部 pop_back,队列直接变成 [4(5)]。

代码

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> dq;
        vector<int> ans;
        int n = nums.size();
        for(int i = 0; i < n; i++) {
            while(!dq.empty() && nums[i] >= nums[dq.back()]) dq.pop_back();
            dq.push_back(i);
            while(dq.front() <= i - k) dq.pop_front();
            if(i >= k - 1) ans.push_back(nums[dq.front()]);
        }
        return ans;
    }
};

时间复杂度:O(n)O(n) 空间复杂度:O(k)O(k),deque最多保存窗口内k个小标

分块+预处理

思想来源:把数组按块大小 kk 分块,预处理两块数组:

  • prefix[i]:从块开头到 i的最大值
  • suffix[i]:从i 到块末尾的最大值

任意窗口 [L,R][L, R](窗口长度 kk),会跨最多两个块:

窗口最大值 = max(suffix[L],prefix[R])max(suffix[L], prefix[R])

时间 O(n)O(n),空间 O(n)O(n),不需要单调队列,思维角度不一样。

原理

把数组每 kk 个元素分成一块。

对于窗口 [L,R],R=L+k−1[L, R], R = L + k - 1

  1. 如果 LL 和 RR 在同一个块:直接遍历求最大值(极少情况)
  2. 如果跨两块:
  • suffix[L]suffix[L]:L 所在块,从L向右到块结尾的最大值
  • prefix[R]prefix[R]:R 所在块,从块开头到 R 的最大值

两者取大,就是整个滑动窗口最大值。

为什么会成立: 窗口被块边界切为两部分:

  • 左段:[L,块末尾][L, 块末尾],最大值就是 suffix[L]suffix[L]
  • 右段:[下一块开头,R][下一块开头, R],最大值就是 prefix[R]prefix[R]

窗口最大值一定出自这两段其中之一。

代码

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> prefix(n);
        vector<int> suffix(n);
        // prefix[i]:本块内,从块起点到i的最大值
        for(int i = 0; i < n; ++i) {
            // i是块起点:重新赋值
            if(i % k == 0) {
                prefix[i] = nums[i];
            } else {
                prefix[i] = max(prefix[i-1], nums[i]);
            }
        }
        // suffix[i]:本块内,从i到块末尾的最大值
        for(int i = n-1; i >= 0; --i) {
            // i是块末尾
            if(i % k == k-1 || i == n-1) {
                suffix[i] = nums[i];
            } else {
                suffix[i] = max(suffix[i+1], nums[i]);
            }
        }
        vector<int> ans;
        // 每个窗口左端点L,右端点 R = L + k -1
        for(int L = 0; L <= n - k; ++L) {
            int R = L + k - 1;
            ans.push_back(max(suffix[L], prefix[R]));
        }
        return ans;
    }
};

时间复杂度:O(n)O(n),三次线性遍历 空间复杂度:O(n)O(n),两个预处理数组

239. 滑动窗口最大值 - 力扣(LeetCode)

chengzi