代码随想录算法训练营Day53|单调栈part02

70 阅读5分钟

LeetCode 42 接雨水

题目链接:leetcode.cn/problems/tr…

文档讲解:programmercarl.com/0042.接雨水.ht…

视频讲解:www.bilibili.com/video/BV1uD…

思路

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

暴力解法

对每一个索引i,计算在i之上承载的雨水量,再加和。注意第一根和最后一根柱子不接雨水。 对于索引i,对应的雨水量取决于其左侧最高的柱子和右侧最高的柱子中的更小者。因此只要对每根柱子分别向左向右寻找最高柱子即可。

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

双指针优化

根据暴力解法我们知道,只要记录左边柱子和右边柱子的最高高度,就可以计算当前位置的雨水。 当前列雨水面积=min(左侧柱子最高高度,右侧柱子最高高度) - 当前柱子高度 每次都向前向后遍历寻找最高的柱子很显然存在重复计算。

我们可以单独从前向后和从后向前遍历数组,得到每个位置上的左侧柱子最高高度maxLeft和右侧柱子最高高度maxRight。在计算雨水量时直接读取数组对应的数据即可。

从左向右遍历,左侧最高高度是前一个位置的左边最高高度和本高度的最大值 从右向左遍历,右侧最高高度是后一个位置的右边最高高度和本高度的最大值

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

单调栈

单调栈常用于找一个元素左边或者右边第一个比自己大或者小的元素。

本题本质上是寻找一个元素的左边最大元素和右边最大元素,来计算雨水量。

跟双指针法不同的是,如果寻找一个元素右侧第一个比他大的元素,就成了利用行来计算雨水量,即计算在一个水平高度上有多少雨水。

  1. 确定单调栈内存放的元素:存放元素的下标
  2. 确定单调栈内元素的顺序(从栈顶到栈底):递增。因为递增找的是更大的元素,有更高的柱子才能保证隔断当前水平线上的水。

考虑使用单调栈的逻辑:

  1. 当前遍历的柱子高于栈顶下标对应的柱子高度:
    1. 弹出栈顶元素记为mid,对应的高度为height[mid]。mid是水槽底部,此时的栈顶stack.top是水槽的左侧,i是水槽的右侧。
    2. 如果这时候栈空,说明在开头高度递增中,开头不接雨水,跳过
    3. 雨水量 = (min(height[stack.top], height[i]) - height[mid]) * (i - stack.top + 1)
    4. 如果还满足条件,从第1步继续
  2. 当前遍历的柱子等于栈顶下标对应的柱子高度:栈顶元素出栈,i入栈。计算雨水的方式是长度✖️宽度,需要右侧的柱子来计算宽度。
  3. 当前遍历的柱子小于栈顶下标对应的柱子高度:i入栈

解法

暴力解法

暴力解法超时。

双指针法

class Solution {
	public int trap(int[] height) {	
		int[] maxLeft = new int[height.length];		
		int[] maxRight = new int[height.length];		
		maxLeft[0] = height[0];		
		for (int i = 1; i < maxLeft.length; i++) {		
			maxLeft[i] = Math.max(maxLeft[i-1], height[i]);			
		}		
		maxRight[maxRight.length-1] = height[height.length-1];		
		for (int i = maxRight.length-2; i >= 0; i--) {		
			maxRight[i] = Math.max(maxRight[i+1], height[i]);		
		}		
		int sum = 0;		
		for (int i = 0; i < height.length; i++) {		
			int water = Math.min(maxLeft[i], maxRight[i]) - height[i];			
			sum += water;		
		}		
		return sum;	
	}
}

单调栈

class Solution {
	public int trap(int[] height) {	
		int sum = 0;		
		Stack<Integer> stack = new Stack<>();		
		stack.push(0);		
		for (int i = 1; i < height.length; i++) {		
			if (height[i] < height[stack.peek()]) {			
				stack.push(i);			
			}			
			else if (height[i] == height[stack.peek()]) {			
				stack.pop();				
				stack.push(i);			
			}			
			else {			
				while (!stack.isEmpty() && height[i] > height[stack.peek()]) {				
					int mid = stack.pop();					
					if (stack.isEmpty()) {					
						break;					
					}					
					sum += (Math.min(height[i], height[stack.peek()]) - height[mid]) * (i - stack.peek() - 1);				
				}				
				stack.push(i);			
			}		
		}		
		return sum;	
	}
}

LeetCode 84 柱状图中最大的矩形

题目链接:leetcode.cn/problems/la…

文档讲解:programmercarl.com/0084.柱状图中最大…

视频讲解:www.bilibili.com/video/BV1Ns…

思路

对于每个下标上的柱子,定义其对应的最大矩形为能够包含整个柱子的最大矩形。所以为了计算最大矩形的面积,应该找到柱子i左侧第一个矮于它的柱子和右侧第一个矮于它的柱子。所以我们可以用单调栈

  1. 单调栈中存放什么元素:数组下标
  2. 单调栈元素的顺序(从栈顶到栈底):递减。

使用单调栈时的三种情况:

  1. 遍历元素height[i]大于栈顶元素height[stack.top]:i入栈
  2. 遍历元素height[i]等于栈顶元素height[stack.top]:i入栈
  3. 遍历元素height[i]小于栈顶元素height[stack.top]
    1. 弹出栈顶元素为k,k对应的矩形面积 = height[k] * (i - stack.top - 1)
    2. 更新矩形最大值
    3. 如果还满足条件,继续第1步
    4. 不满足则i入栈

为了方便当矩形延伸到左侧边缘时的宽度计算,开始时向栈中推入下标-1,再推入起始下标0。当栈顶元素为-1时就视为栈空。

最后结束遍历时栈中还会有未计算对应矩形面积的下标,需要依次弹出按照情况3处理(相当于最后推入一个下标为height.length,对应高度为0的柱子)

解法

class Solution {	
	public int largestRectangleArea(int[] heights) {	
		Stack<Integer> stack = new Stack<>();		
		int max = 0;		
		stack.push(-1);		
		stack.push(0);		
		for (int i = 1; i < heights.length; i++) {		
			if (heights[i] >= heights[stack.peek()]) {			
				stack.push(i);			
			}			
			else {			
				while (stack.peek() != -1 && heights[i] < heights[stack.peek()]) {				
					int k = stack.pop();					
					max = Math.max(max, heights[k]*(i - stack.peek() - 1));				
				}				
				stack.push(i);			
			}			
		}		
		while (stack.peek() != -1) {		
			int k = stack.pop();			
			max = Math.max(max, heights[k]*(heights.length - stack.peek() - 1));		
		}		
		return max;	
	}
}

今日收获总结

今日学习三小时,单调栈的应用理解起来比较复杂,但是变化度不高。