回溯法
动态规划
滑动窗口
贪心算法
排列组合
单调栈
以例子来分析
例1:
题目:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。(Leetcode84题)
解题思路:我们可以遍历每根柱子,以当前柱子 i 的高度作为矩形的高,那么矩形的宽度边界即为向左找到第一个高度小于当前柱体 i 的柱体,向右找到第一个高度小于当前柱体 i 的柱体。对于每个柱子我们都如上计算一遍以当前柱子作为高的矩形面积,最终比较出最大的矩形面积即可。
暴力法:
class Solution {
//由中心向两侧扩散
public int largestRectangleArea(int[] heights) {
if(heights.length==0){
return 0;
}
if(heights.length==1){
return heights[0];
}
int max=0;
for(int i=0;i<heights.length;i++){
int left=1;
int right=1;
while(i-left>=0&&heights[i-left]>=heights[i]){
left++;
}
while(i+right<heights.length&&heights[i+right]>=heights[i]){
right++;
}
if((right+left-1)*heights[i]>max){
max=(right+left-1)*heights[i];
}
}
return max;
}
}
运行结果:用时1350ms,内存消耗41.6MB
单调栈:
解题思路:
以上暴力写法 Java 可以通过,但我们不妨想一下这里的双重循环是否可以优化?
我们每遍历到当前柱体 i 时:
- 上述写法中,我们需要再嵌套一层 while 循环来向左和向右找到第一个比柱体 i 高度小的柱体,这个过程是 O(N)的;
- 那么我们可以 O(1)的时间获取柱体 i 左边第一个比它小的柱体吗?答案就是单调增栈,因为对于栈中的柱体来说,栈中下一个柱体就是左边第一个高度小于自身的柱体。
解法:
因此做法就很简单了,我们遍历每个柱体,若当前的柱体高度大于等于栈顶柱体的高度,就直接将当前柱体入栈,否则若当前的柱体高度小于栈顶柱体的高度,说明当前栈顶柱体找到了右边的第一个小于自身的柱体,那么就可以将栈顶柱体出栈来计算以其为高的矩形的面积了。
class Solution {
//改进中心扩散法,使用单调栈对中心扩散法进行优化
//使用中心扩散法时,外层for循环,内层需要两个while循环,可以使用单调栈把内层时间优化为O(1)
public int largestRectangleArea(int[] heights) {
if(heights.length==0){
return 0;
}
//在原数组的头和尾部分别加一个元素0
// 这里为了代码简便,在柱体数组的头和尾加了两个高度为 0 的柱体。
int[] arr=new int[heights.length+2];
//将指定源数组中的数组从指定位置复制到目标数组的指定位置
System.arraycopy(heights,0,arr,1,heights.length);
//ArrayDeque是Deque接口的一个实现,使用了可变数组,所以没有容量上的限制
//ArrayDeque是线程不安全的,在没有外部同步的情况下,不能再多线程环境下使用
//ArrayDeque是Deque的实现类,可以作为栈来使用,效率高于Stack
Deque<Integer> stack=new ArrayDeque<>();//栈中存储的是数组的索引
int max=0;
for(int i=0;i<arr.length;i++){
// 对栈中柱体来说,栈中的下一个柱体就是其「左边第一个小于自身的柱体」;
// 若当前柱体 i 的高度小于栈顶柱体的高度,说明 i 是栈顶柱体的「右边第一个小于栈顶柱体的柱体」。
// 因此以栈顶柱体为高的矩形的左右宽度边界就确定了,可以计算面积\U0001f336️ ~
while(!stack.isEmpty()&&arr[i]<arr[stack.peek()]){
int h=arr[stack.pop()];
max=Math.max(max,(i-stack.peek()-1)*h);
}
stack.push(i);
}
return max;
}
}
运行结果:运行时间13ms,内存消耗40.7MB
例2:
题目:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。(LeetCode42)
解题思路:简单来说就是当前柱子如果小于等于栈顶元素,说明形不成凹槽,则将当前柱子入栈;反之若当前柱子大于栈顶元素,说明形成了凹槽,于是将栈中小于当前柱子的元素pop出来,将凹槽的大小累加到结果中。
代码:
class Solution {
//单调栈, 该题和最大矩形使用单调栈的方法不一样,求最大矩形需要把小于栈顶的元素入栈,这里需要把大于栈顶的元素入栈
//接雨水需要形成凹槽,所以栈里面的元素都是递减的,如果当前元素大于栈顶的元素,则就形成凹槽
//依次遍历每个元素,按照上述方法,求出最大值
public int trap(int[] height) {
if(height.length<3){
return 0;
}
//存储数组的索引
Deque<Integer> stack=new ArrayDeque<>();
int sum=0;
for(int i=0;i<height.length;i++){
while(!stack.isEmpty()&&height[i]>height[stack.peek()]){
//当前凹槽的底部
int mid=height[stack.pop()];
//如果栈顶下一个元素和栈顶元素相等则全部弹出栈
while(!stack.isEmpty()&&height[stack.peek()]==mid){
stack.pop();
}
// stack.peek()是此次接住的雨水的左边界的位置,右边界是当前的柱体,即i。
// Math.min(height[stack.peek()], height[i]) 是左右柱子高度的min,减去mid就是雨水的高度。
// i - stack.peek() - 1 是雨水的宽度。
if(!stack.isEmpty()){
sum+=((Math.min(height[i],height[stack.peek()])-mid)*(i-stack.peek()-1));
}
}
stack.push(i);
}
return sum;
}
}