解决的问题
窗口只能右边界或左边界向右滑的情况下,且左边界要小于右边界,维持窗口内部最大值或者最小值快速更新的结构。
窗口更新结构的原理
功能的实现用双端队列。
LinkedList<Integer> qmax = new LinkedList<>();//双端队列
/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
时间复杂度
时间复杂度:每个位置最多进一次,最多出一次,所以双端队列更新的总代价一定是O(N),单次平均更新的代价是O(N)/N也就是O(1)
代码结构
public static class WindowMax{
private int L;
private int R;
private int[] arr;//针对的数组
private LinkedList<Integer> qmax;//双端队列
public WindowMax(int[] a){
arr = a;
L = -1;
R = 0;
qmax = new LinkedList<>();
}
//将R一直扩到数组结尾,这个只是演示代码,具体的题目需要具体的定制
public void addNumFromRight(){
if (R == arr.length){
return;
}
//双端队列非空且位于尾部的值如果小于要加入的值则弹出
while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]){
qmax.pollLast();
}
//为空直接就加入
qmax.addLast(R);
R++;
}
//窗口的L依次滑动到R的左边一个
public void removeNumFromLeft(){
if (L >= R - 1){
return;
}
L++;
//滑动窗口左边地址和双端队列的头相等,弹出,否则不操作
if (qmax.peekFirst() == L){
qmax.pollFirst();
}
}
//返回当前滑动窗口的最大值
public Integer getMax(){
if (!qmax.isEmpty()){
return arr[qmax.peekFirst()];
}
return null;
}
}
相关题目
有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置(L、R同时移动)。
例如,数组为[4,3,5,4,3,3,6,7],窗口大小为3时:
[4 3 5]4 3 3 6 7 窗口中最大值为5
4[3 5 4]3 3 6 7 窗口中最大值为5
4 3[5 4 3]3 6 7 窗口中最大值为5
4 3 5[4 3 3]6 7 窗口中最大值为4
4 3 5 4[3 3 6]7 窗口中最大值为6
4 3 5 4 3[3 6 7] 窗口中最大值为7
请实现一个函数。 输入:整型数组arr,窗口大小为w。
输出:一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的 以本题为例,结果应该返回{5,5,5,4,6,7}。
思路分析
所用的方法是上面的结构,这个窗口是让R向右一步的时候,同时让L向右移动一步过期
相关代码
1-双端队列
用i - w以及及时弹出实现窗口的固定,体会这个方法,很多题都会用到
时间复杂度O(n)
//该代码没有刻意使用L、R。用i - w以及及时弹出实现窗口的固定,因为i-w+1就是窗口的左边界
public static int[] getMaxWindow(int[] arr, int w){
if (arr == null || w < 1 || arr.length < w){
return null;
}
LinkedList<Integer> qmax = new LinkedList<>();
int[] res = new int[arr.length - w + 1];
int index = 0;
for (int i = 0; i < arr.length; i++){
while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]){
qmax.pollLast();
}
//注意加入的是地址
qmax.addLast(i);
//i - w得到滑动窗口的左边界L,刚开始的时候滑动窗口还没铺满w个的时候,肯定是直接跳过
//队列中的头地址和i-w相等,弹出,注意i - w + 1才是窗口的左边界,这里是将滑动后残留 //的上一个窗口周期的最大值剔除掉,也就是这个值属于上一周期的,不是当前窗口周期的
//很妙的代码,可以结合下一个if()看
if (qmax.peekFirst() == i - w){
qmax.peekFirst();
}
//临界点表示的是滑动窗口已经铺满w个[4 3 5],i这个时候i是2,w是3,所以i + 1 = w
//i大于这个值就可以存入最大值数组了,i代表arr[2]的时候就可以,因为w = 3,所以满足滑 //动窗口的大小了
if (i >= w - 1){
//注意双端队列中存的是地址,所以arr[]
res[index++] = arr[qmax.peekFirst()];
}
}
return res;
}
public static void printArray(int[] arr) {
for (int i = 0; i != arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = { 4, 3, 5, 4, 3, 3, 6, 7 };
int w = 3;
printArray(getMaxWindow(arr, w));
}
2-优先级队列(堆)
时间复杂度O(nlogn)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] pair1, int[] pair2) {
//维持大根堆,如果两个值相等,就按照位置的大小进行比较
return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
}
});
for (int i = 0; i < k; ++i) {
pq.offer(new int[]{nums[i], i});
}
int[] ans = new int[n - k + 1];
ans[0] = pq.peek()[0];
for (int i = k; i < n; ++i) {
pq.offer(new int[]{nums[i], i});
//看看是不是位置不在规定的窗口内,不在就弹出
while (pq.peek()[1] <= i - k) {
pq.poll();
}
ans[i - k + 1] = pq.peek()[0];
}
return ans;
}
}