题目:

给定一个数组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的解法