算法训练营【1】 单调栈和窗口

本文详细介绍了滑动窗口和单调栈的概念,并通过具体例子展示了它们在处理数组最大值、最小值更新以及寻找达标子数组数量等问题中的应用。还探讨了柱状图中最大矩形面积的计算方法,以及如何优化算法实现平均时间复杂度为O(1)。文章提供了多种解题思路,包括使用单调双端队列和前缀和数组等数据结构来提高效率。
摘要由CSDN通过智能技术生成

单调栈和窗口

左老师课程Git笔记

滑动窗口

滑动窗口是一种想象出来的数据结构:
滑动窗口有左边界L和有边界R
在数组或者字符串或者- -个序列上,记为S,窗口就是S[…R]这一部分

L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口,L和R都只能往右滑

滑动内最大值和最小值的更新结构

窗口不管L还是R滑动之后,都会让窗口呈现新状况,

如何能够更快的得到窗口当前状况下的最大值和最小值?

最好平均下来复杂度能做到O(1)

利用单调双端队列

窗口本质:哪些数会依次成为最大数的优先级

平均时间复杂度为o(1)

剑指 Offer 59 - I. 滑动窗口的最大值

假设一个固定大小为W的窗口,依次划过arr,

返回每一次滑出状况的最大值

例如,arr = [4,3,5,4,3,3,6,7], W = 3

返回:[5,5,5,4,6,7]

剑指 Offer 59 - I. 滑动窗口的最大值

public static int[] getMaxWindow(int[] arr, int w) {
		if (arr == null || w < 1 || arr.length < w) {
			return null;
		}
		// qmax 窗口最大值的更新结构
		// 放下标
		LinkedList<Integer> qmax = new LinkedList<Integer>();
		int[] res = new int[arr.length - w + 1];
		int index = 0;
		for (int R = 0; R < arr.length; R++) {
			while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) {
				qmax.pollLast();
			}
			qmax.addLast(R);
			if (qmax.peekFirst() == R - w) {
				qmax.pollFirst();
			}
			if (R >= w - 1) {
				res[index++] = arr[qmax.peekFirst()];
			}
		}
		return res;
	}

求达标子数组的数量

给定一个整型数组arr,和一个整数num

某个arr中的子数组sub,如果想达标,必须满足:

sub中最大值 – sub中最小值 <= num,

返回arr中达标子数组的数量

public static int num(int[] arr, int sum) {
		if (arr == null || arr.length == 0 || sum < 0) {
			return 0;
		}
		int N = arr.length;
		int count = 0;
  // 存放数组下标,可以用下标取值
		LinkedList<Integer> maxWindow = new LinkedList<>();
		LinkedList<Integer> minWindow = new LinkedList<>();
		int R = 0;
  // 从0位置一直往后遍历
		for (int L = 0; L < N; L++) {
			while (R < N) {
				while (!maxWindow.isEmpty() && arr[maxWindow.peekLast()] <= arr[R]) {
					maxWindow.pollLast();
				}
				maxWindow.addLast(R);
        
				while (!minWindow.isEmpty() && arr[minWindow.peekLast()] >= arr[R]) {
					minWindow.pollLast();
				}
				minWindow.addLast(R);
        
				if (arr[maxWindow.peekFirst()] - arr[minWindow.peekFirst()] > sum) {
					// 已经到了不达标的位置
          break;
				} else {
					R++;
				}
			}
      
      //从 l开始有几个达标的数组
			count += R - L;
			if (maxWindow.peekFirst() == L) {
				maxWindow.pollFirst();
			}
			if (minWindow.peekFirst() == L) {
				minWindow.pollFirst();
			}
		}
		return count;
	}

优化一个题

​ 数据状况

​ 问题本身

对于 此题,当前范围内若达标,则缩小范围必达标

若不达标,则扩大范围必不达标,可以及时break

遇到题,先看看问题本身和范围是否能够建立单调性

然后选择对应流程及流程中所要的信息

单调栈

求解一个集合中,每个元素左边离自己最近且比自己小的元素是?,右边离自己最近且比自己小的元素是?

没有重复值:

准备一个栈

栈从底到顶,严格遵守从小到大

元素入栈,若栈空,直接进栈

​ 若非空,检查栈顶是否大于当前元素,若大于栈顶,入栈

​ 若小于栈顶,则将栈顶出栈,

​ 出栈时记录信息,栈顶元素左边最小最近的元素就是栈顶元素的下一个元素

​ 栈顶元素右边最小最近的元素就是当前元素(因为是它使栈顶出栈的)

​ 然后将当前元素入栈

​ 循环到数组结束

进行出栈

​ 栈顶元素左边最小最近的元素就是栈顶元素的下一个元素

​ 栈顶元素右边最小最近的元素就是null(即没有)

// arr = [ 3, 1, 2, 3]
	//         0  1  2  3
	//  [
	//     0 : [-1,  1]
	//     1 : [-1, -1]
	//     2 : [ 1, -1]
	//     3 : [ 2, -1]
	//  ]
	public static int[][] getNearLessNoRepeat(int[] arr) {
		int[][] res = new int[arr.length][2];
		// 只存位置!
		Stack<Integer> stack = new Stack<>();
		for (int i = 0; i < arr.length; i++) { // 当遍历到i位置的数,arr[i]
			while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
				int j = stack.pop();
				int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
				res[j][0] = leftLessIndex;
				res[j][1] = i;
			}
			stack.push(i);
		}
		while (!stack.isEmpty()) {
			int j = stack.pop();
			int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
			res[j][0] = leftLessIndex;
			res[j][1] = -1;
		}
		return res;
	}
有重复值:

栈中元素为一个list

元素入栈,若栈空,直接进栈

​ 若非空,检查栈顶是否大于当前元素,若大于栈顶list的最后一个元素,入栈

​ 若小于栈顶,则将栈顶出栈,

​ 出栈时记录信息,

​ 对于出栈元素的list,遍历每个元素进行记录

​ 若栈空,则比他们小的数就是-1(即没有)

​ 若非空,则是出栈元素的下一个元素的list的最后一个元素

​ 栈顶元素右边最小最近的元素就是当前元素(因为是它使栈顶出栈的)

​ 然后将当前元素入栈,若当前元素与栈顶元素的list的最后一个元素相等,则加入到这个list中(相当于压缩操作)

​ 循环到数组结束

进行出栈

​ 栈顶元素左边最小最近的元素就是栈顶元素的下一个元素

​ 栈顶元素右边最小最近的元素就是null(即没有)

public static int[][] getNearLess(int[] arr) {
		int[][] res = new int[arr.length][2];
		Stack<List<Integer>> stack = new Stack<>();
		for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈
			while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
				List<Integer> popIs = stack.pop();
				int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
				for (Integer popi : popIs) {
					res[popi][0] = leftLessIndex;
					res[popi][1] = i;
				}
			}
      
			if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
				stack.peek().add(Integer.valueOf(i));
			} else {
				ArrayList<Integer> list = new ArrayList<>();
				list.add(i);
				stack.push(list);
			}
		}
		while (!stack.isEmpty()) {
			List<Integer> popIs = stack.pop();
			int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
			for (Integer popi : popIs) {
				res[popi][0] = leftLessIndex;
				res[popi][1] = -1;
			}
		}
		return res;
	}

题目三 子树组特定信息的问题

给定一个只包含正数的数组arr,arr中任何一个子数组sub,

一定都可以算出(sub累加和 )* (sub中的最小值)是什么,

那么所有子数组中,这个值最大是多少?

暴力求解:

枚举所有子树组,不断更新最大值

//暴力求解
public static int max1(int[] arr) {
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			for (int j = i; j < arr.length; j++) {
				int minNum = Integer.MAX_VALUE;
				int sum = 0;
				for (int k = i; k <= j; k++) {
					sum += arr[k];
					minNum = Math.min(minNum, arr[k]);
				}
				max = Math.max(max, minNum * sum);
			}
		}
		return max;
	}
预处理技巧:

前缀和数组

​ 前缀和数组中,0位置上是arr【0】,sum【i】=arr【i】+sum【i】

​ 一次遍历即可生成前缀和数组

​ 若求 L- R 上的前缀和,则 sum【R】-sum【L】 即可得到

先找出以当前元素为最小值的 最大数组。

3 4 5 6 3 2

最大数组为 3 4 5 6 ,因为如果扩大到 2 最小值就变了

所以0 位置的3 做最小值的 满足题目要求的值为 (3+4+5+6)*3

且前缀和可以去前缀和数组中找 (减少累加操作)

然后以每个元素去求解,就可以找到最大的值

对于每个元素,找到前面等于自己的位置,找到后面等于自己的位置,

满足。 前面的元素》= 当前元素 《= 当前元素

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;
	}

84. 柱状图中最大的矩形

给定一个非负数组arr,代表直方图

返回直方图的最大长方形面积

求以i位置为高的时候的最大长方形面积

​ 利用单调栈很容易的得到其以i位置为高的最左边界和最右边界

​ 求所有位置为高的长方形面积中的最大值

法一
class Solution {
    public int largestRectangleArea(int[] heights) {
        int res = 0;
        Deque<Integer> stack = new ArrayDeque<>();
        int[] new_heights = new int[heights.length + 2];
        for (int i = 1; i < heights.length + 1; i++) new_heights[i] = heights[i - 1];
        //System.out.println(Arrays.toString(new_heights));
        for (int i = 0; i < new_heights.length; i++) {
            //System.out.println(stack.toString());
            while (!stack.isEmpty() && new_heights[stack.peek()] > new_heights[i]) {
                int cur = stack.pop();
                res = Math.max(res, (i - stack.peek() - 1) * new_heights[cur]);
            }
            stack.push(i);
        }
        return res;  
    }
}

作者:powcai
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhao-liang-bian-di-yi-ge-xiao-yu-ta-de-zhi-by-powc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
法二:
class Solution {
    public int largestRectangleArea(int[] heights) {
        if (heights == null || heights.length == 0) {
			return 0;
		}
		int maxArea = 0;
		Stack<Integer> stack = new Stack<Integer>();
  	    for(int i=0;i<heights.length;i++){
              // 栈不为空时进入此分支
            while(!stack.isEmpty() &&  heights[i] <= heights[stack.peek()] ){
                // 长
                int j = stack.pop();
                // 最大的宽
                int k = stack.isEmpty() ? -1 : stack.peek();
                // i 代表使栈内最大元素弹出前的位置, k 代表 最大元素弹出的位置,  所以宽为 i -k - 1 
				int curArea = (i - k - 1) * heights[j];
				maxArea = Math.max(maxArea, curArea);
            }
            // 当前元素下标进入栈
            stack.push(i);
        }
        System.out.println(heights.length);
        while (!stack.isEmpty()) {
            // 长
            int j = stack.pop();
            // 宽
			int k = stack.isEmpty() ? -1 : stack.peek();
            // 进入到此while 时, 宽度 i 已经到达数组最大值 对应到坐标中。就是 数组的长度
            // 为了计算宽 ,补了一个虚拟的块 
			int curArea = (heights.length - k - 1) * heights[j];
            System.out.println(curArea);
			maxArea = Math.max(maxArea, curArea);
		}   
        return maxArea;
        
    }
} 

85. 最大矩形

相当于躺到的最大矩形,可以前用前缀和的思想建立一个二维数组,其中的元素代表当前列往后连续的1 的个数为多少

法一:
class Solution {
    public int maximalRectangle(char[][] matrix) {
        //f(matrix.length==1 && matrix[0].length==1 &&matrix[0][0]==1)return 1;
        //if(matrix.length==1 && matrix[0].length==1 &&matrix[0][0]==0)return 0;
        if(matrix.length==0 || matrix==null )return 0;

        int [][] temp = new int[matrix.length][matrix[0].length];
        for(int i=0;i<matrix.length;i++){
            for(int j=0;j<matrix[0].length;j++){
                int k = show(i,j,matrix);
                temp[i][j]=k;
            }
        }
        // for(int i=0;i<matrix.length;i++){
        //     for(int j=0;j<matrix[0].length;j++){
        //         System.out.print(temp[i][j]+" ");
        //     }
        //     System.out.println();
        // }
        int max=0;
        for(int i=0;i<matrix[0].length;i++){
            int []a = new int[matrix.length];
            for(int j=0;j<a.length;j++){
                a[j]=temp[j][i];
            }
            int res= largestRectangleArea(a);
            //System.out.println(" "+res);
            max = Math.max(max,res);
        }
        return max;
    }
    public int show(int i,int j,char [][]matrix){
        int sum=0;
        for(int k=j;k<matrix[0].length;k++){
            if(matrix[i][k]-'0'==1){
                sum++;
            }else{
                break;
            }
        }
        return sum;
    }
    public int largestRectangleArea(int[] heights) {
        int res = 0;
        Deque<Integer> stack = new ArrayDeque<>();
        int[] new_heights = new int[heights.length + 2];
        for (int i = 1; i < heights.length + 1; i++) new_heights[i] = heights[i - 1];
        //System.out.println(Arrays.toString(new_heights));
        for (int i = 0; i < new_heights.length; i++) {
            //System.out.println(stack.toString());
            while (!stack.isEmpty() && new_heights[stack.peek()] > new_heights[i]) {
                int cur = stack.pop();
                res = Math.max(res, (i - stack.peek() - 1) * new_heights[cur]);
            }
            stack.push(i);
        }
        return res;  
    }
}
题解中看到的更好的解法:
public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    int[] heights = new int[matrix[0].length];
    int maxArea = 0;
    for (int row = 0; row < matrix.length; row++) {
        //遍历每一列,更新高度
        for (int col = 0; col < matrix[0].length; col++) {
            if (matrix[row][col] == '1') {
                heights[col] += 1;
            } else {
                heights[col] = 0;
            }
        }
        //调用上一题的解法,更新函数
        maxArea = Math.max(maxArea, largestRectangleArea(heights));
    }
    return maxArea;
}
public int largestRectangleArea(int[] heights) {
    int maxArea = 0;
    Stack<Integer> stack = new Stack<>();
    int p = 0;
    while (p < heights.length) {
        //栈空入栈
        if (stack.isEmpty()) {
            stack.push(p);
            p++;
        } else {
            int top = stack.peek();
            //当前高度大于栈顶,入栈
            if (heights[p] >= heights[top]) {
                stack.push(p);
                p++;
            } else {
                //保存栈顶高度
                int height = heights[stack.pop()];
                //左边第一个小于当前柱子的下标
                int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
                //右边第一个小于当前柱子的下标
                int RightLessMin = p;
                //计算面积
                int area = (RightLessMin - leftLessMin - 1) * height;
                maxArea = Math.max(area, maxArea);
            }
        }
    }
    while (!stack.isEmpty()) {
        //保存栈顶高度
        int height = heights[stack.pop()];
        //左边第一个小于当前柱子的下标
        int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
        //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算
        int RightLessMin = heights.length;
        int area = (RightLessMin - leftLessMin - 1) * height;
        maxArea = Math.max(area, maxArea);
    }
    return maxArea;
}
作者:windliang
链接:https://leetcode-cn.com/problems/maximal-rectangle/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-1-8/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

参考1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值