LeetCode(239.滑动窗口的最大值
- 2020 年 3 月 17 日
- 筆記
题目:
给定一个数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到最右侧,你只可以看到滑动窗口内的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
初始思路:刚开始没想太多,C++中求数组的最大值的函数:max_element可以解决此问题。果不其然,AC了而且时间复杂度也很高。显然不是这个题目的正常解法,一番苦思冥想后还是打开了题解区。卧槽!哟嘚斯内!下面我就能分析下大佬的方法。
单调队列解法:
一个普通队列输入什么数那位置就是什么数,而单调队列在输入一个数时会与前面的比较大小,删除部分元素来保证队列的数是单调递增或递减的。
这样在这个题目中,每次向右移动都会增删一个元素,但我们要在线性时间找到最大值,我们就可以考虑单调队列。
单调队列通常有这几个函数:
class MonotonicQueue { // 在队尾添加元素 n void push(int n); // 返回当前队列中的最大值 int max(); // 队头元素如果是 n,删除它 void pop(int n); }
然后我们假设已经有了单调队列这个数据结构来满足我们的要求,我们可以把此题的解题框架搭出来:
vector<int> maxSlidingWindow(vector<int>& nums, int k) { MonotonicQueue window; vector<int> res; for (int i = 0; i < nums.size(); i++) { if (i < k - 1) { //先把窗口的前 k - 1 填满 window.push(nums[i]); } else { // 窗口开始向前滑动 window.push(nums[i]); res.push_back(window.max()); window.pop(nums[i - k + 1]); // nums[i - k + 1] 就是窗口最后的元素 } } return res; }
框架实现还是很简单的,现在我们主要是怎么实现单调队列的那些函数:
实现单调队列:
在此之前我们先认识另一种数据结构:deque,即双端队列:
class deque { // 在队头插入元素 n void push_front(int n); // 在队尾插入元素 n void push_back(int n); // 在队头删除元素 void pop_front(); // 在队尾删除元素 void pop_back(); // 返回队头元素 int front(); // 返回队尾元素 int back(); }
这些操作的复杂度都是O(1)。我们可以利用这个数据结构来实现单调队列。
前面我们已经提到过单调队列就是队尾每增一个元素时,与前面的进行比较,把比它小的元素删掉:
void push(int n) { while (!data.empty() && data.back() < n) data.pop_back(); data.push_back(n); }
这样就保证了队列从头到尾是单调递减的。
由此我们知道当前最大的元素肯定是在队头,所以max()函数实现:
int max() { return data.front(); }
最后当窗口向右移动一步时,之前最左端的数需要pop掉,此时队列里的数都是单调递减的。最左端的数有两种情况:
1.在push时,因为比push的数小,所以已经被pop 2.若没被pop,说明他一直比后面push的数大,所以在队头位置。
所以pop函数实现:
void pop(int n) { if (!data.empty() && data.front() == n) data.pop_front(); }
自此我们已将单调队列构造出来,接下来就可以代入上面的框架来最后完善我们的代码:
class MonotonicQueue { private: deque<int> data; public: void push(int n) { while (!data.empty() && data.back() < n) data.pop_back(); data.push_back(n); } int max() { return data.front(); } void pop(int n) { if (!data.empty() && data.front() == n) data.pop_front(); } }; vector<int> maxSlidingWindow(vector<int>& nums, int k) { MonotonicQueue window; vector<int> res; for (int i = 0; i < nums.size(); i++) { if (i < k - 1) { //先填满窗口的前 k - 1 window.push(nums[i]); } else { // 窗口向前滑动 window.push(nums[i]); res.push_back(window.max()); window.pop(nums[i - k + 1]); } } return res; }
三、复杂度分析:
乍一看push中含有while循环,时间复杂度不是O(1)啊,所以本算法时间复杂度也不是线性时间吧?
单独看push操作确实不是O(1),但是整个算法的复杂度依然是O(N)。要这样想,nums中每个元素最多被push_back和pop_back一次,没有多余操作,所以整体复杂度还是O(N);
空间复杂度就很简单了,就是窗口的大小O(k)。
四、最后总结:
有些朋友会觉得「单调队列」和「优先级队列」比较像,实际上差别很大的。
单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或递减)的部分;而优先级队列(二叉堆)相当于自动排序,差别大了去了。
本文参考了labuladong的解法