滑动窗口是什么?
- 滑动窗口是一种想象出来的数据结构。
- 滑动窗口有左边界L和有边界R。
- 在数组或者字符串或者一个序列上,记为S,窗口就是S[L..R]这一部分。
- L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口
L和R都只能往右滑。
滑动窗口能做什么?
- 滑动窗口、首尾指针等技巧,说白了是一种求解问题的流程设计。
滑动内最大值和最小值的更新结构
- 窗口不管L还是R滑动之后,都会让窗口呈现新状况,
- 如何能够更快的得到窗口当前状况下的最大值和最小值?
- 最好平均下来复杂度能做到O(1)
- 利用单调双端队列!
例题
- 假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值。例如,arr = [4,3,5,4,3,3,6,7], W = 3,返回:[5,5,5,4,6,7]
/**
* 求滑动窗口的最大值和最小值流程(L,R分别是窗口的边界)
* <p>
* 求窗口内最大值(利用双端队列实现)
* 1.滑动窗口R向右移动时,窗口增加,如果此时队列为空那么直接从尾部加入队列,如果队列不为空
* 那么比较队列尾部元素和当前元素大小,如果当前元素比队列尾部元素小那么从尾部加入当前元素,
* 如果当前元素比队列尾部元素大于等于那么,那么弹出队列尾部元素,继续比较当前元素和队列尾部元素,
* 知道队列尾部元素大于当前元素。此时队头元素就是此时形成的窗口的最大值。
* 2.滑动窗口L向右移动时,窗口减小。比较此时移除窗口的元素是否是队列中的元素如不不是,什么也不用做
* 如果窗口移出元素刚好等于队列头部元素,那么从队列头部移出改元素,此时队列中的新头部元素就是此时
* 窗口的最大值。
* <p>
* 假设一个固定大小为W的窗口,依次划过arr,
* 返回每一次滑出状况的最大值
* 例如,arr = [4,3,5,4,3,3,6,7], W = 3
* 返回:[5,5,5,4,6,7]
*/
public class SlidingWindow {
//形成一个窗口为3的滑动窗口,每次求出最大值,最后返回
public static int[] slidingWindowsMax(int[] arr, int w) {
if (arr == null || arr.length == 0 || w <= 0 || arr.length - w < -1) {
return null;
}
int len = arr.length;
int[] res = new int[len - w +1];
int index = 0;
LinkedList<Integer> queueMax = new LinkedList<>();
for (int R = 0; R < len; R++) {
// 加入双端队列
while (!queueMax.isEmpty()
&& arr[R] >= arr[queueMax.peekLast()]) {
queueMax.pollLast();
}
queueMax.offerLast(R);
if((R - w >=0) &&(R - w) == queueMax.peekFirst()){
queueMax.pollFirst();
}
if(R - w >= -1){
// 形成了w窗口,取出最大值
res[index++] = arr[queueMax.peekFirst()];
}
}
return res;
}
public static void main(String[] args) {
// [5,5,5,4,6,7]
int[] arr = new int[]{4,2,3,3,5,1,1,1,1,8};
int w = 3;
int[] ints = slidingWindowsMax(arr, w);
for(int i : ints){
System.out.print(i+" ");
}
}
}
- 滑动窗口内最小值:
1.滑动窗口R向右移动,如果队列为空,直接加入队列,否则比较队尾元素和当前元素,若当前元素大于等于队尾元素则加入,否则弹出队尾元素再次进行比较直到当前元素大于等于队尾元素,加入当前元素。
2.滑动窗口L向右移动,若当前元素不等于队列中的队头元素那么不用管,此时队列中的队头元素就是此时窗口的最小值。若当前元素等于队列头部元素则删除队头元素,此时队列的新头就是此时滑动窗口的最小值。
- 给定一个整型数组arr,和一个整数num
某个arr中的子数组sub,如果想达标,必须满足:
sub中最大值 – sub中最小值 <= num,
返回arr中达标子数组的数量
/**
* 给定一个整型数组arr,和一个整数num
* 某个arr中的子数组sub,如果想达标,必须满足:
* sub中最大值 – sub中最小值 <= num,
* 返回arr中达标子数组的数量
*/
public class SubArray {
// 首先如果在L -> R 上是达标子数组,那么在L -> R 内的的子数组都是达标的,因为在这个
// 范围上最大值,只可能比L -> R上的小,最小值只可能比L -> R上的大,所以还是满足达标子数组的要求
// 如果在L ->R上不满足,那么在大于L -> R的范围上也不会存在达标的子数组
// 因为最大值只可可能更大,最小值只可能更小,那么本来就不满足,现在差距跟大了,一定不满足
// 现在可以这样做以数组汇总每个位置为开头,一直滑动R形成一个窗口找出以当前位置为头的最大的
// 达标的子数组,然后 R - L + 1就是以当前位置开头的达标子数组的所有。
// 然后在换开头,遍历完所有的位置结果就出来了
public static int subArray(int[] arr,int num){
if(arr == null || arr.length == 0){
return 0;
}
// 窗口最大值
LinkedList<Integer> maxQ = new LinkedList<>();
// 窗口最小值
LinkedList<Integer> minQ = new LinkedList<>();
int len = arr.length;
int L = 0;
int R = 0;
int res = 0;
while (L < len){
while (!maxQ.isEmpty() && arr[R] >= arr[maxQ.peekLast()]){
maxQ.pollLast();
}
maxQ.offerLast(R);
while (!minQ.isEmpty() && arr[R] <= arr[minQ.peekLast()]){
minQ.pollLast();
}
minQ.offerLast(R);
// 如果此时最大值和最小值不达标,开始结算
if(arr[maxQ.peekFirst()] - arr[minQ.peekFirst()]> num){
res += R - L;
}else{
if(R == len - 1){
res += R - L + 1;
L ++;
}else {
R ++;
}
continue;
}
// 结算完以后让L 前进一步,并且调整 最大最小队列
if(maxQ.peekFirst() == L){
maxQ.pollFirst();
}
if(minQ.peekFirst() == L){
minQ.pollFirst();
}
L ++;
}
return res;
}
public static int subArray2(int[] arr,int num){
if(arr == null || arr.length == 0){
return 0;
}
// 窗口最大值
LinkedList<Integer> maxQ = new LinkedList<>();
// 窗口最小值
LinkedList<Integer> minQ = new LinkedList<>();
int len = arr.length;
int L = 0;
int R = 0;
int res = 0;
while (L < len){
// 每个L位置尝试最大子数组
while (R < len){
// R不断扩大直到破坏子数组条件
while (!maxQ.isEmpty() && arr[R] >= arr[maxQ.peekLast()]){
maxQ.pollLast();
}
maxQ.offerLast(R);
while (!minQ.isEmpty() && arr[R] <= arr[minQ.peekLast()]){
minQ.pollLast();
}
minQ.offerLast(R);
if(arr[maxQ.peekFirst()] - arr[minQ.peekFirst()]> num){
// 跳出R向右扩循环
break;
}
R ++;
}
res += R - L;
// 结算完以后让L 前进一步,并且调整 最大最小队列
if(maxQ.peekFirst() == L){
maxQ.pollFirst();
}
if(minQ.peekFirst() == L){
minQ.pollFirst();
}
L ++;
}
return res;
}
}
单调栈是什么?
一种特别设计的栈结构,为了解决如下的问题:
给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?
2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?
如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。
那么到底怎么设计呢?
/**
* 单调栈
* 一种特别设计的栈结构,为了解决如下的问题:
* 给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
* 1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?
* 2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?
* 如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。
*/
public class MonotonousStack {
// 数组中无重复元素
public static int[][] monotonousStack(int[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
int len = arr.length;
// 准备一个栈,存放元素下标
Stack<Integer> stack = new Stack<>();
// 返回数组,每个元素占据一行,每一行有两列,第一列表示离当前元素最近的左边小的元素
// 第二列表示离当前元素最近的右边小的元素
int[][] res = new int[len][2];
int index = 0;
while (index < len) {
while (!stack.isEmpty() && arr[stack.peek()] > arr[index]){
// 收集答案,弹出栈顶元素 pop 是元素的下标
int pop = stack.pop();
int temp = stack.isEmpty() ? -1:stack.peek();
res[pop][0] = temp;
res[pop][1] = index;
}
stack.push(index++);
}
while (!stack.isEmpty()) {
int cur = stack.pop();
int temp = stack.isEmpty() ? -1:stack.peek();
// 没有右边最近小的元素用-1代替
res[cur][1] = -1;
res[cur][0] = temp;
}
return res;
}
// 数组中有重复元素
public static int[][] monotonousStack2(int[] arr){
if (arr == null || arr.length == 0) {
return null;
}
int len = arr.length;
Stack<List<Integer>> stack = new Stack<>();
int[][] res = new int[len][2];
int index = 0;
while (index < len){
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[index]){
List<Integer> pop = stack.pop();
for(Integer item : pop){
int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
res[item][0] = temp;
res[item][1] = index;
}
}
if(!stack.isEmpty() && arr[stack.peek().get(0)] == arr[index]){
stack.peek().add(index);
}else {
ArrayList<Integer> list = new ArrayList<>();
list.add(index);
stack.push(list);
}
index ++;
}
while (!stack.isEmpty()) {
List<Integer> list = stack.pop();
for(Integer item : list){
int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
res[item][0] = temp;
res[item][1] = -1;
}
}
return res;
}
}
- 给定一个只包含正数的数组arr,arr中任何一个子数组sub,
一定都可以算出(sub累加和 )* (sub中的最小值)是什么,
那么所有子数组中,这个值最大是多少?
滑动窗口、单调栈怎么用?
想用滑动窗口,要想办法把具体的问题转化为滑动窗口的处理流程
想用滑动窗口最值的更新结构,就看看处理流程下,是否需要最值这个信息
想用单调栈,要想办法把具体的问题转化为单调栈所解决的原问题
滑动窗口及其最大值和最小值的更新结构、单调栈,都是重要算法原型
- 给定一个只包含正数的数组arr,arr中任何一个子数组sub,
一定都可以算出(sub累加和 )* (sub中的最小值)是什么,
那么所有子数组中,这个值最大是多少?
public class MaxSum {
public static int maxSum(int[] arr){
int[] sum = new int[arr.length];
sum[0] = arr[0];
for (int i = 1;i < arr.length ;i++){
sum[i] = sum[i -1] + arr[i];
}
int[][] ints = monotonousStack2(arr);
int max = Integer.MIN_VALUE;
for (int i = 0;i < ints.length;i++){
// 以当前元素为最小值的子数组
int left = ints[i][0];
int right = ints[i][1];
// left 和 right 之间的和
int he = 0;
if(left == -1){
if(right == -1){
he = sum[sum.length-1];
}else{
he = sum[right - 1];
}
}else {
if(right == -1){
he = sum[sum.length-1] - sum[left];
}else {
he = sum[right -1 ] - sum[left];
}
}
max = Math.max(max,he*arr[i]);
}
return max;
}
// 数组中有重复元素
public static int[][] monotonousStack2(int[] arr){
if (arr == null || arr.length == 0) {
return null;
}
int len = arr.length;
Stack<List<Integer>> stack = new Stack<>();
int[][] res = new int[len][2];
int index = 0;
int max = Integer.MIN_VALUE;
while (index < len){
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[index]){
List<Integer> pop = stack.pop();
for(Integer item : pop){
int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
// 左边最近最小元素
res[item][0] = temp;
// 右边最近最小元素
res[item][1] = index;
}
}
if(!stack.isEmpty() && arr[stack.peek().get(0)] == arr[index]){
stack.peek().add(index);
}else {
ArrayList<Integer> list = new ArrayList<>();
list.add(index);
stack.push(list);
}
index ++;
}
while (!stack.isEmpty()) {
List<Integer> list = stack.pop();
for(Integer item : list){
int temp = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size()-1);
res[item][0] = temp;
res[item][1] = -1;
}
}
return res;
}
public static int max2(int[] arr) {
int size = arr.length;
int[] sums = new int[size];
sums[0] = arr[0];
for (int i = 1; i < size; i++) {
sums[i] = sums[i - 1] + arr[i];
}
int max = Integer.MIN_VALUE;
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < size; i++) {
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
}
return max;
}
}