结论:
- 求最大值时使用双端对维护递减数据,即队列中数据依次递减,队列头部数据始终为最大值,每次将遍历到的数据与队列尾部的数据进行比较
如果不违反队列的递减规律(遍历到的数据元素小于队列尾部数据)就直接插入队列尾部
如果违反了队列的递减规律就依次从队列尾部弹出数据,直到找到能够保持队列递减规律的位置。 - 求最小值时使用双端对维护递增数据,即队列中数据依次递增,队列头部数据始终为最小值,每次将遍历到的数据与队列尾部的数据进行比较
如果不违反队列的递减少规律(遍历到的数据元素小于队列尾部数据)就直接插入队列尾部
如果违反了队列的递增规律,就依次从队列尾部弹出数据,直到找到能够保持队列递增规律的位置。 - 求最大值时,滑动窗口新进来的数据arr[i] ,窗口内的数据最大值一定是 ≥ arr[i] ,所以要弹出队列内小于 arr[i] 的数据,且窗口在包含的 arr[i] 区域内移动时,最大值也一定 ≥ arr[i],同理求最小值。
- 求最大值时,如果 arr[i] 比 队列尾部数据要小,直接加入到队列尾部,因为数组 arr[i] 之后如果一直在减少,arr[i] 就有可能会成为最大值,所以要将 arr[i] 加入到队列尾部。
- 时间复杂度为O(N)
题目:
有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边,窗口每次向右边 滑一个位置。
例如,数组为[4,3,5,4,3,3,6,7],窗口大小为 3 时:
解题思路:
窗口滑动过程中如果最大值位于窗口的最末尾,移除的是窗口的最大值,就需要找到新的最大值,所以可以用一个数组或队列记录下窗口内已排好续的数组索引,每次滑动窗口时比较是否是移除了最尾部的最大值。
可以用双端队列来优化,始终将最大值到队头,如果最大值移除了,就轮循队列,找到新的最大值。
使用了一个队列保存将窗口内的值的索引,且将索引对应的数组值进行由大到小的排序,队列头部放最大值索引,队列尾部放小值索引(队列中数据对应的数组值是由大到小的排序)每次新来一个值时,与队列尾部的数据进行比较,如果比尾部的数据大,就将尾部数据弹出,在比较尾部的下一个数据,直到找小于队列尾部数据的值或者队列为空的位置将新的arr[i]的索引插入到队列,之所以要弹出小于arr[i]的值,是因为在当前的窗口内最大值一定是 ≥ arr[i],并且在接下来的包含arr[i] 的 (w-1)个 窗口内,最大值也一定是 ≥ arr[i],所以没有必要保留小于arr[i]的值,因为队列内部的数据是数组的索引 且是由小到大的,注意队列保存的不是数组的具体值而是数组的索引,每次添加完成后需要校验队列头部的数据即最大值的索引是否在 当前的窗口范围内,如果不在了,需要从队头弹出不在窗口内的数组索引。
假设遍历到 arr[i],qmax 的放入规则为:
.如果 qmax 为空,直接把下标 i 放进 qmax,放入过程结束。
.如果 qmax 不为空,取出当前 qmax 队尾存放的下标,假设为 j。
1)如果 arr[j]>arr[i],直接把下标 i 放进 qmax 的队尾,放入过程结束。
2)如果 arr[j]<=arr[i],把 j 从 qmax 中弹出,重复 qmax 的放入规则。
public static void main(String[] args) {
int[] arr = {4,3,5,4,3,3,6,7};
int[] res = getRes(arr, 3);
System.out.println(Arrays.toString(res));
}
/**
* 窗口滑动走掉的值是最大值时就得用新添加的值,与旧得窗口值比较找到最大得值
* 可以用双端队列来优化,始终将最大值到队头,如果最大值移除了,就轮循队列,找到新的最大值
*
* 假设遍历到 arr[i],qmax 的放入规则为:
* 1.如果 qmax 为空,直接把下标 i 放进 qmax,放入过程结束。
* 2.如果 qmax 不为空,取出当前 qmax 队尾存放的下标,假设为 j。
* 1)如果 arr[j]>arr[i],直接把下标 i 放进 qmax 的队尾,放入过程结束。
* 2)如果 arr[j]<=arr[i],把 j 从 qmax 中弹出,重复 qmax 的放入规则。
*/
public static int[] getRes(int[] arr, int w) {
int[] res = new int[arr.length - w + 1];
// 使用双端队列
LinkedList<Integer> maxIndexQueue = new LinkedList<>();
for (int i = 0; i < arr.length; i++) {
while(true) {
if (maxIndexQueue.isEmpty() || arr[maxIndexQueue.peekLast()] > arr[i]) {
maxIndexQueue.add(i);
break;
} else{
maxIndexQueue.removeLast();
}
}
if (maxIndexQueue.peekFirst() == i-w ) {
maxIndexQueue.removeFirst();
}
if (i+1>=w) res[i+1-w] = arr[maxIndexQueue.peekFirst()];
}
return res;
}
/**
* 优化中间的while循环,优化思路就是取if的反值
*/
public static int[] getRes2(int[] arr, int w) {
int[] res = new int[arr.length - w + 1];
// 使用双端队列 队头维护最大值 队列内数据依次递减
LinkedList<Integer> maxIndexQueue = new LinkedList<>();
for (int i = 0; i < arr.length; i++) {
// 队列内数据递减的,新的数据不违反递减规律就直接加入到队列中
while (!maxIndexQueue.isEmpty() && arr[maxIndexQueue.peekLast()] > arr[i]) {
// while 循环是删除比当前值要小的值,因为在包含arr[i] 的滑动窗口内 最大值一定 ≥ arr[i]
maxIndexQueue.removeLast();
}
// 如果arr[i] 小于最末尾值值时,直接加入队列,
// 数组 arr[i]之后的数据一直在递减,某一时刻 arr[i]就会成为最大值
maxIndexQueue.add(i);
if (maxIndexQueue.peekFirst() == i-w ) {
maxIndexQueue.removeFirst();
}
if (i+1>=w) res[i+1-w] = arr[maxIndexQueue.peekFirst()];
}
return res;
}
/**
* 滑动窗口最小值
*/
public static int[] getMin(int[] arr, int w){
int[] result = new int[arr.length - w +1];
// 使用双端队列 队头维护最小值,队列数据依次递增
LinkedList<Integer> minque = new LinkedList<>();
for (int i = 0; i < arr.length; i++) {
// 队列内数据递增的,新的数据不违反递增规律就直接加入到队列中
while(!minque.isEmpty() && arr[i]<= arr[minque.getLast()] ){
minque.removeLast();
}
minque.addLast(i);
if (minque.size() > w) minque.removeFirst();
if (i + 1 >= w) {
result[i + 1 - w] = arr[minque.getFirst()];
}
}
return result;
}