转自:https://zhuanlan.zhihu.com/p/34456480
问题描述:
给定一个整数数组和一个固定大小的窗口,在窗口沿着数组滑动的过程中寻找每一时刻窗口中的数组的最大值。
举例:
给定如下数组,设定窗口大小为size = 3。
第一步,最开始窗口位于数组的最左端,也就是数组的前三个元素在窗口中,此时窗口中最大值为2;
第二步,窗口向右滑动一个单位,此时窗口中的最大值为3;
第三步,窗口再向右滑动一个单位,此时窗口中的最大值为6,同时窗口到达数组最右端,不再滑动。
提示:
- 使用堆(heap)来做
- 必须存储每一时刻的窗口中的元素吗?
解法:
时间复杂度:O(n),线性复杂度;
空间复杂度:O(w),设窗口的大小为w。
1、最简单的思路,就是每一次窗口滑动之后都遍历一遍窗口中的元素来找出最大值,这样的话,时间复杂度就是O(nw)。
2、好一些的做法,使用堆(heap)来做,堆的大小自然就是窗口的大小w,这会帮助我们很快地找到堆中的最大元素。但是,每一次窗口滑动的时候,我们都需要把不在窗口中的元素从堆中移出去然后再添加一个新进入窗口的元素,这些操作的时间复杂度都是O(log w),因此总的计算下来,完成所有操作的时间复杂度就是O(n log w)。
3、更好的做法,为降低时间复杂度,我们可以使用双向链表或者是vector向量,这样就能在两边插入或者删除元素了。在举例中的第二步,也就是如下所示的这一步:
新元素3比窗口中的2和-5都要大,那么我们完全可以将2和-5从代表窗口的数据结构中移出来,因为不管后面新加进来的元素多大,只需要跟窗口中的最大值比较即可。这个特性对我们降低时间复杂度来说非常重要,基于此,我们有如下算法:
- 窗口大小为w,数组大小为n,初始状态下窗口为空;
- 遍历数组中的前w个元素,对每一个元素,执行下列操作:
- 如果窗口最右端(也就是tail部分)的元素小于或等于当前新加入的元素,那么就将窗口最右端的元素移出窗口,直到窗口最右端元素大于当前新加入元素或者窗口为空为止
- 将当前元素加入窗口。
用一个例子来解释上面的操作,下面这个数组大小为6,窗口大小为3:
第一步,遍历数组的前3个元素,首先是第一个-4,此时窗口为空,直接加入窗口。
第二步,遍历至第二个元素2,与窗口中最右边元素-4比较,显然2大于-4,因此将-4从窗口中移出,将2加入窗口中。
第三步,遍历至第三个元素-5,与窗口最右边元素2比较,显然2大于-5,直接将-5加入到窗口中。
上述操作保证了窗口中的最大值始终在窗口的最左端,也即是窗口的头部。下面我们继续描述算法:
- 遍历完前w个元素后,继续遍历数组中的剩余元素,执行下列操作:
- 将窗口中所有小于或等于当前新加入元素的元素移出,将当前元素加入窗口;
- 如果窗口最左端的元素的下标超出窗口的范围,则将其移出窗口;
- 每个时刻窗口中的元素最大值都在窗口的最左端,也就是头部,可以直接输出。
继续用上面的例子来解释剩余的操作:
第四步,遍历至第四个元素1,与窗口中最右端元素-5比较,显然1大于-5,将-5移出窗口,1加入窗口。
第五步,遍历至第五个元素-1,与窗口最右端元素1比较,显然1大于-1,直接将-1加入窗口;同时窗口最左端元素2超出窗口范围,从窗口中移出。
第六步,遍历至第六个元素6,显然6比窗口中的所有元素都打,因此将所有元素移出,将6加入到窗口中。
每一步窗口中元素的最大值,都在窗口的最左端,也就是窗口头部,直接输出即可。在上述算法中,每一个元素不管是被插入还是移出,都是只有一次,因此就是遍历一遍数组复杂度O(n)。
代码实现(C++版本):
void find_max_sliding_window(vector<int>& v, int window_size) {
if (window_size > v.size()) {
cout << "Error" << endl;
return;
}
list<int> window;
for (int i = 0; i < window_size; i ++) {
while (!window.empty() && v[i] > v[window.back()]) {
window.pop_back();
}
window.push_back(i);
}
cout << "max = " << v[window.front()] << " ";
for (int i = window_size; i < v.size(); i ++) {
while (!window.empty() && v[i] > v[window.back()]) {
window.pop_back();
}
if (!window.empty() && window.front() <= i - window_size) {
window.pop_front();
}
window.push_back(i);
cout << v[window.front()] << " ";
}
cout << endl;
}
综上所述,我们从最简单的思路讲起,最终使用线性复杂度解决了这个问题,尽管最终的结果是最好的,但是这个不断优化的过程还是值得去不断温习的。