力扣42、84(接雨水,柱状图中最大的矩形)
42.接雨水
双指针法
具体实现:
列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。
列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。
列4 柱子的高度为1(以下用height表示)
那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。
列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。
代码:
class Solution { public int trap(int[] height) { int sum = 0; for (int i = 0; i < height.length; i++) { //第一个柱子和最后一个柱子不接雨水 if (i == 0 || i == height.length - 1) continue; int rHeight = height[i];// 记录右边柱子的最高高度 int lHeight = height[i];// 记录左边柱子的最高高度 for (int r = i + 1; r < height.length; r++) { if (height[r] > rHeight) rHeight = height[r]; } for (int l = i - 1; l >= 0; l--) { if (height[l] > lHeight) lHeight = height[l]; } int h = Math.min(lHeight, rHeight) - height[i]; if (h > 0) sum += h; } return sum; } }
动态规划
基本思路:
上一种解法中,为了得到两边的最高高度,使用了双指针来遍历,
每到一个柱子都向两边遍历一遍,这其实是有重复计算的。
把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。
这样就避免了重复计算,这就用到了动态规划。
具体实现:
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
代码:
class Solution { public int trap(int[] height) { int length = height.length; if (length <= 2) return 0; int[] maxLeft = new int[length]; int[] maxRight = new int[length]; //记录每个柱子左边最大高度 maxLeft[0] = height[0]; for (int i = 1; i < length; i++) maxLeft[i] = Math.max(height[i], maxLeft[i - 1]); //记录每个柱子右边最大高度 maxRight[length - 1] = height[length - 1]; for (int i = length - 2; i >= 0; i--) maxRight[i] = Math.max(height[i],maxRight[i + 1]); //求和 int sum = 0; for (int i = 0; i < length; i++) { int count = Math.min(maxLeft[i],maxRight[i]) - height[i]; if (count > 0) sum += count; } return sum; } }
单调栈
基本思想:
单调栈就是要保持栈内元素有序
1.按照行方向来计算雨水
2.使用单调栈内元素的顺序
从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,
栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,
而添加的元素就是凹槽右边的柱子。
3.遇到相同高度的柱子
遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
4.栈内要保存什么数值
使用单调栈,其实是通过 长 * 宽 来计算雨水面积的。
长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,
栈里就存放int类型的元素,表示下标,
想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。
具体实现:
1.先将下标0的柱子加入到栈中,st.push(0);
。
2.开始从下标1开始遍历所有的柱子,for (int i = 1; i < height.size(); i++)
。
(1)如果当前遍历的元素(柱子)高度小于栈顶元素的高度,就把这个元素加入栈中,
因为栈里本来就要保持从小到大的顺序(从栈头到栈底)。
(2)如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,
因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。
(3)如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,
(a)取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,
下标记为mid,对应的高度为height[mid](就是图中的高度1)。
(b)此时的栈顶元素st.top(),就是凹槽的左边位置,
下标为st.top(),对应的高度为height[st.top()](就是图中的高度2)。
(c)当前遍历的元素i,就是凹槽右边的位置,
下标为i,对应的高度为height[i](就是图中的高度3)。
栈顶和栈顶的下一个元素以及要入栈的三个元素来接水!
雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,
代码为:int h = min(height[st.top()], height[i]) - height[mid];
雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度),
代码为:int w = i - st.top() - 1 ;
当前凹槽雨水的体积就是:h * w
。
代码:
class Solution { public int trap(int[] height){ int size = height.length; if (size <= 2) return 0; // in the stack, we push the index of array // using height[] to access the real height Stack<Integer> stack = new Stack<Integer>(); stack.push(0); int sum = 0; for (int index = 1; index < size; index++){ int stackTop = stack.peek(); if (height[index] < height[stackTop]){ stack.push(index); }else if (height[index] == height[stackTop]){ // 因为相等的相邻墙,左边一个是不可能存放雨水的,所以pop左边的index, push当前的index stack.pop(); stack.push(index); }else{ //pop up all lower value int heightAtIdx = height[index]; while (!stack.isEmpty() && (heightAtIdx > height[stackTop])){ int mid = stack.pop(); if (!stack.isEmpty()){ int left = stack.peek(); int h = Math.min(height[left], height[index]) - height[mid]; int w = index - left - 1; int hold = h * w; if (hold > 0) sum += hold; stackTop = stack.peek(); } } stack.push(index); } } return sum; } }
84.柱状图中最大的矩形
动态规划
基本思想:
同上题
要记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。
具体实现:
需要循环查找,也就是在寻找的过程中使用了while
代码:
class Solution { public int largestRectangleArea(int[] heights) { int length = heights.length; int[] minLeftIndex = new int[length]; int[] minRightIndex = new int[length]; //记录每个柱子左边第一个小于该柱子的下标 minLeftIndex[0] = -1; for (int i = 1; i < length; i++){ int t = i -1; //这里不是用if,而是不断向右寻找的过程 while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t]; minLeftIndex[i]= t; } //记录每个柱子右边第一个小于该柱子的下标 minRightIndex[length-1] = length; for (int i = length - 2; i >= 0; i--) { int t = i + 1; while (t <length && heights[t] >= heights[i]) t = minRightIndex[t]; minRightIndex[i] = t; } //求和 int result = 0; for (int i = 0; i < length; i++) { int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1); result = Math.max(sum,result); } return result; } }
单调栈
基本思想:
接雨水是找每个柱子左右两边第一个大于该柱子高度的柱子,
而本题是找每个柱子左右两边第一个小于该柱子的柱子。
这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小。
接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,
所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
栈顶和栈顶的下一个元素以及要入栈的三个元素组成了要求最大面积的高度和宽度
具体实现:
(1)如果当前遍历的元素(柱子)高度大于栈顶元素的高度,就把这个元素加入栈中,
因为栈里本来就要保持从大到小的顺序(从栈头到栈底)。
(2)如果当前遍历的元素(柱子)高度等于栈顶元素的高度,
可以更新栈顶元素,也可以不更新,效果一样,思路不同
此处选择更新
(3)如果当前遍历的元素(柱子)高度小于栈顶元素的高度,
这里是用while循环,直到当前元素的高大于了栈顶元素
因为矩形可能是宽宽的低低的
(a)取栈顶元素,将栈顶元素弹出,也就是中间元素
下标记为mid,对应的高度为height[mid]。
(b)此时的栈顶元素st.peek(),就是中间元素的左边位置,
下标为st.peek()。
(c)当前遍历的元素i,就是中间元素右边的位置,
下标为i。
矩形的高就是中间元素的高,宽就是右边位置-左边位置-1
举例
代码:
class Solution { public int largestRectangleArea(int[] heights) { Stack<Integer> st = new Stack<Integer>(); int[] newHeights = new int[heights.length + 2]; newHeights[0] = 0; newHeights[newHeights.length - 1] = 0; for (int index = 0; index < heights.length; index++){ newHeights[index + 1] = heights[index]; } heights = newHeights; st.push(0); int result = 0; for (int i = 1; i < heights.length; i++) { if (heights[i] > heights[st.peek()]) { st.push(i); } else if (heights[i] == heights[st.peek()]) { st.pop(); st.push(i); } else { while (heights[i] < heights[st.peek()]) { int mid = st.peek(); st.pop(); int left = st.peek(); int right = i; int w = right - left - 1; int h = heights[mid]; result = Math.max(result, w * h); } st.push(i); } } return result; } }