每天一道LeetCode-----计算给定序列中所有长度为k的滑动窗的最大值集合
2017-12-07 17:10
726 查看
原题链接Sliding Window Maximum
给定一个数组,从左到右每k个位置算作一个滑动窗,每到达一个滑动窗,都需要找到这个滑动窗中最大的元素并记录下来,最后返回所有最大元素组成的数组。要求时间复杂度在O(n)
首先,如果每到达一个滑动窗都计算一遍这个滑动窗覆盖的区域的最大值,那么复杂度应该是….额…O(n*k - k*k)?k是滑动窗大小,n是数组元素个数
有没有什么方法可以不用每次都计算一遍最大值呢
对于滑动窗而言,首先想到的就是向右移动的过程中是进行”左边减,右边加”的操作,即从滑动窗中删除离开窗口的元素,并增加进去窗口的元素
利用这个特性,假设已经知道第一个滑动窗nums[0 : k - 1]中的最大值N,那么在向右移动时,删掉nums[0],增加nums[k],然后将N和nums[k]的较大者作为第二个滑动窗的最大值
一切看起来非常完美,但是一旦N是nums[0],上面的想法突然变得,唔…,不太完善
不过现在任务倒是明朗不少,既然最大值N从窗口中离开,那么就需要知道在第一个滑动窗中第二大元素的值,拿它和nums[k]的较大者作为第二个滑动窗的最大值
问题是该怎么记,同时还要保证复杂度尽量低
假设…假设,有这么一个容器D,它记录的是某些元素的下标,同时这个容器保证在这些下标上的元素的大小顺序是递减的,也就是nums[D[0]] > nums[D[1]] > nums[D[2]] ….
另外,对于这个容器,规定
可以获得第0的元素,即有成员函数D.front()
可以获得最后一个元素,即有成员函数D.back()
可以弹出第一个元素,即有成员函数D.pop_front()
可以弹出最后一个元素,即有成员函数D.pop_back()
可以插入一个元素到末尾,即有成员函数D.push_back()
而对于这个容易的操作,规定
插入的是元素下标而非元素的值
插入元素下标到末尾时,从容器末尾开始依次弹出指向的元素值小于要插入下标代表的元素值的那些下标
最后一个规定多少有些绕口,用数字表示可能会清楚些,假设某一时刻容器D中的元素为
I1, I2, I3, I4, I5
注意根据上面的规定,可以知道nums[I1] > nums[I2] > nums[I3] > nums[I4] > nums[I5]
此时遍历到一个元素M,它的下标为Im,那么如果要将Im插入D中时,需要从D的末尾开始,找到第一个下标Ii满足nums[Ii] > M,至于Ii+1, …, I5就都pop掉,然后将Im追加到D的末尾。
假设I3是第一个满足上面要求的值,那么插入Im后,D中元素为I1, I2, I3, Im。
I4和I5在向前找nums[Ii] > M的过程中已经被pop掉了
先提结论吧,根据上述规则,D中的下标分别是当前滑动窗第一大的元素,第二大的元素,…第n大的元素的下标。nums[D.front()]就是当前滑动窗覆盖区域中的最大值
为什么是记录下标而不是记录元素的值,因为通过记录下标,可以判断上一个滑动窗的最大值是否已经被移出了
假设某一时刻的滑动窗位于nums[i-k+1, i-k+2, …, i-2, i-1, i],此时nums[i-k+1]是这个滑动窗的最大值,同时也知道D.front() == i-k+1。
那么当向右移动一步时,滑动窗变为nums[i-k+2, i-k+3, …, i-1, i, i+1]
要怎么才可以知道上一个滑动窗的最大值已经被移出了呢,通过(i+1) - D.front() == k啦:)
此时就可以把D.front()弹出,通过调用D.pop_front()。随后把滑动窗新加进来的元素nums[i+1]追加到D中,追加之后,D.front()是当前滑动窗的最大值所在的位置
那么有没有上面这样的容器呢,没有,唔…我是说C++标准库里没有,但是有个容器,它有所有容器D需要的成员函数,啊终于轮到deque出场了(一直以为它只是作为幕后工作者的身份用来实现stack, queue,没想到还有这样的用处)
deque,俗话叫双端队列,在队列的两端都可以进行插入删除操作,并且复杂度是O(1)。不过对于上面的插入操作规则,需要手动去实现
实现完叫什么,单调队列:)
代码如下
本题主要利用deque实现单调队列,不容易理解的地方在为什么可以在插入一个下标时可以将其他指向值小于当前值的下标都pop掉
考虑上面的D容器,容器内部情况为I1, I2, I3, I4, I5
这5个值表示当前滑动窗的第一大的元素是nums[I1],第二大的元素是nums[I2],…, 第五大的元素是nums[I5]
同时,当前的滑动窗为nums[I1, I2, I3, …, I5],假设右移一次后滑动窗为nums[I2, I3, …, I5, I],根据D可知右移前滑动窗的最大值nums[I1]已经被移走,第二大的元素是nums[I2],此时D的情况为
I2, I3, I4, I5
假设nums[I] > nums[I2],根据上述规则,更新后的D为
I
没错只有I一个,因为其他的下标代表的值都小于nums[I],都被pop掉了。
为什么可以只有I一个呢,因为I的位置是当前滑动窗最右边的位置,只有右移k次后才会被移走,而在右移1, 2, …, k - 1时,都可以知道前k个滑动窗的最大值
给定一个数组,从左到右每k个位置算作一个滑动窗,每到达一个滑动窗,都需要找到这个滑动窗中最大的元素并记录下来,最后返回所有最大元素组成的数组。要求时间复杂度在O(n)
首先,如果每到达一个滑动窗都计算一遍这个滑动窗覆盖的区域的最大值,那么复杂度应该是….额…O(n*k - k*k)?k是滑动窗大小,n是数组元素个数
有没有什么方法可以不用每次都计算一遍最大值呢
对于滑动窗而言,首先想到的就是向右移动的过程中是进行”左边减,右边加”的操作,即从滑动窗中删除离开窗口的元素,并增加进去窗口的元素
利用这个特性,假设已经知道第一个滑动窗nums[0 : k - 1]中的最大值N,那么在向右移动时,删掉nums[0],增加nums[k],然后将N和nums[k]的较大者作为第二个滑动窗的最大值
一切看起来非常完美,但是一旦N是nums[0],上面的想法突然变得,唔…,不太完善
不过现在任务倒是明朗不少,既然最大值N从窗口中离开,那么就需要知道在第一个滑动窗中第二大元素的值,拿它和nums[k]的较大者作为第二个滑动窗的最大值
问题是该怎么记,同时还要保证复杂度尽量低
假设…假设,有这么一个容器D,它记录的是某些元素的下标,同时这个容器保证在这些下标上的元素的大小顺序是递减的,也就是nums[D[0]] > nums[D[1]] > nums[D[2]] ….
另外,对于这个容器,规定
可以获得第0的元素,即有成员函数D.front()
可以获得最后一个元素,即有成员函数D.back()
可以弹出第一个元素,即有成员函数D.pop_front()
可以弹出最后一个元素,即有成员函数D.pop_back()
可以插入一个元素到末尾,即有成员函数D.push_back()
而对于这个容易的操作,规定
插入的是元素下标而非元素的值
插入元素下标到末尾时,从容器末尾开始依次弹出指向的元素值小于要插入下标代表的元素值的那些下标
最后一个规定多少有些绕口,用数字表示可能会清楚些,假设某一时刻容器D中的元素为
I1, I2, I3, I4, I5
注意根据上面的规定,可以知道nums[I1] > nums[I2] > nums[I3] > nums[I4] > nums[I5]
此时遍历到一个元素M,它的下标为Im,那么如果要将Im插入D中时,需要从D的末尾开始,找到第一个下标Ii满足nums[Ii] > M,至于Ii+1, …, I5就都pop掉,然后将Im追加到D的末尾。
假设I3是第一个满足上面要求的值,那么插入Im后,D中元素为I1, I2, I3, Im。
I4和I5在向前找nums[Ii] > M的过程中已经被pop掉了
先提结论吧,根据上述规则,D中的下标分别是当前滑动窗第一大的元素,第二大的元素,…第n大的元素的下标。nums[D.front()]就是当前滑动窗覆盖区域中的最大值
为什么是记录下标而不是记录元素的值,因为通过记录下标,可以判断上一个滑动窗的最大值是否已经被移出了
假设某一时刻的滑动窗位于nums[i-k+1, i-k+2, …, i-2, i-1, i],此时nums[i-k+1]是这个滑动窗的最大值,同时也知道D.front() == i-k+1。
那么当向右移动一步时,滑动窗变为nums[i-k+2, i-k+3, …, i-1, i, i+1]
要怎么才可以知道上一个滑动窗的最大值已经被移出了呢,通过(i+1) - D.front() == k啦:)
此时就可以把D.front()弹出,通过调用D.pop_front()。随后把滑动窗新加进来的元素nums[i+1]追加到D中,追加之后,D.front()是当前滑动窗的最大值所在的位置
那么有没有上面这样的容器呢,没有,唔…我是说C++标准库里没有,但是有个容器,它有所有容器D需要的成员函数,啊终于轮到deque出场了(一直以为它只是作为幕后工作者的身份用来实现stack, queue,没想到还有这样的用处)
deque,俗话叫双端队列,在队列的两端都可以进行插入删除操作,并且复杂度是O(1)。不过对于上面的插入操作规则,需要手动去实现
实现完叫什么,单调队列:)
代码如下
class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { vector<int> res; deque<int> dq; for(int i = 0; i < nums.size(); ++i) { /* 上一个滑动窗的最大值被移出了,从容器中删掉 */ if(!dq.empty() && i - dq.front() == k) dq.pop_front(); /* 将容器末尾的所有指向的元素值小于当前值的下标都删掉 */ while(!dq.empty() && nums[dq.back()] < nums[i]) dq.pop_back(); /* 将刚遍历到的元素下标放进容器中 */ dq.push_back(i); /* dq.front()是当前滑动窗第一大的元素的下标 */ /* i >= k-1时才到达第一个滑动窗 */ if(i >= k - 1) res.push_back(nums[dq.front()]); } return res; } };
本题主要利用deque实现单调队列,不容易理解的地方在为什么可以在插入一个下标时可以将其他指向值小于当前值的下标都pop掉
考虑上面的D容器,容器内部情况为I1, I2, I3, I4, I5
这5个值表示当前滑动窗的第一大的元素是nums[I1],第二大的元素是nums[I2],…, 第五大的元素是nums[I5]
同时,当前的滑动窗为nums[I1, I2, I3, …, I5],假设右移一次后滑动窗为nums[I2, I3, …, I5, I],根据D可知右移前滑动窗的最大值nums[I1]已经被移走,第二大的元素是nums[I2],此时D的情况为
I2, I3, I4, I5
假设nums[I] > nums[I2],根据上述规则,更新后的D为
I
没错只有I一个,因为其他的下标代表的值都小于nums[I],都被pop掉了。
为什么可以只有I一个呢,因为I的位置是当前滑动窗最右边的位置,只有右移k次后才会被移走,而在右移1, 2, …, k - 1时,都可以知道前k个滑动窗的最大值
相关文章推荐
- 每天一道LeetCode-----给定序列中2/3/4个元素的和为target的所有集合,或3个元素的和最接近target的集合
- 每天一道LeetCode-----找到给定序列中所有和为某个值的集合或集合个数,序列中可以有/无重复项,集合元素顺序不同算不同集合等
- 每天一道LeetCode-----计算给定范围内所有数的与运算结果
- 每天一道LeetCode-----计算两个序列最长的公共子序列长度
- 每天一道LeetCode-----计算最长的元素连续序列长度
- 每天一道LeetCode-----二叉树逐层遍历,每一层存在一个序列中,返回所有序列集合
- 每天一道LeetCode-----找出给定序列的所有子序列
- 每天一道LeetCode-----计算直方图中最大矩形的面积
- 每天一道LeetCode-----计算二叉树的最大深度及最小深度,判断二叉树是否是高度平衡二叉树
- 每天一道LeetCode-----计算字符串s中有多少个子序列和字符串t相等
- 每天一道LeetCode-----在给定序列中找到满足nums[i]>nums[i-1]&&nums[i]>nums[i+1]的位置,要求时间复杂度是O(logN)
- 每天一道LeetCode-----将字符串切分成若干单词,使得每个单词都在给定的字典中,求出所有的切分结果
- 每天一道LeetCode-----买卖商品问题,计算最大利润,分别有一次交易,两次交易,多次交易的情况
- 每天一道LeetCode-----给定二维数组代表海域和岛屿,计算有多少个孤岛
- 每天一道LeetCode-----最长回文子串/序列,从头开始的最长回文子串长度
- 每天一道LeetCode-----对表达式添加括号并求值,返回所有可能的计算结果
- 每天一道LeetCode-----计算从二维数组的左上角到达右下角的所有路径数及最短的那条,如果存在障碍物时又是多少
- 每天一道LeetCode-----计算二叉树所有根节点到叶子节点的和
- 每天一道LeetCode-----找到二叉树所有和为给定值的路径
- 每天一道LeetCode-----在给定数组中找到一个子数组,使得这个子数组的元素乘积最大