目录
给定
n
个非负整数表示每个宽度为1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路:接雨水的问题是单调栈中比较经典的问题,这里分为三种思路来讲解。
1. 暴力解法
首先我们需要明确一点,是按照行还是列来进行计算,因为如果没有确定行或者列,那么就很难进行后续操作,一会行,一会列,会造成思维混乱。
这里的暴力解法和双指针法我们都采用列来计算,单调栈采用行来计算。
接雨水总体来说涉及到三个位置的影响,遍历元素,遍历元素的左边最高的元素,遍历元素的右边最高的元素。
因为按列计算,所以列宽恒为1,所以我们需要计算高度差,即遍历元素左边的最大元素与遍历元素右边的最大元素中的最小值与当前遍历元素做差,乘上宽度(恒为1),即为最大所接到的雨水。
但是如果是在遍历过程中再去找每个元素的最大值,那么会重复遍历一些位置,导致超时。
如下图代码所示。
所以需要对暴力解法进行优化。
int trap(vector<int>& height) {
int sum = 0;//记录最终结果
for(int i = 0; i < height.size(); i ++){
if(i == 0 || i == height.size() - 1) continue;
int maxLeft = height[i - 1];//记录左边最大值
int maxRight = height[i + 1];//记录右边最大值
//开始寻找下标为i的元素的左边最大值
for(int j = i - 1; j >= 0; j --){
if(height[j] > maxLeft) maxLeft = height[j];
}
//开始寻找下标为i的元素的右边最大值
for(int k = i + 1; k < height.size(); k ++){
if(height[k] > maxRight) maxRight = height[k];
}
int volume = min(maxLeft, maxRight) - height[i];//计算体积
if(volume >= 0) sum += volume;//注意这里的volume一定要大于等于0才能加
}
return sum;
}
时间复杂度:O(n^2)
空间复杂度:O(1)
2. 双指针法
这里的双指针法是指在主循环外面提前将每个元素左右边的最大元素算出,存储在数组里,等到进入主循环,直接调用即可。
int trap(vector<int>& height) {
int sum = 0;//记录最终结果
vector<int> maxLeft(height.size());//记录下每个元素的左边最大值
vector<int> maxRight(height.size());//记录下每个元素的右边最大值
//开始记录左边最大值
int left = height[0];
for(int i = 1; i < height.size(); i ++){
maxLeft[i] = left;
if(height[i] > left) left = height[i];//这里及时更新左边指针所指向的左边最大值
}
//开始记录右边最大值
int right = height[height.size() - 1];
for(int i = height.size() - 2; i >= 0; i --){
maxRight[i] = right;
if(height[i] > right) right = height[i];//这里及时更新右边指针所指向的右边最大值
}
for(int i = 0; i < height.size(); i ++){
if(i == 0 || i == height.size() - 1) continue;
int volume = min(maxLeft[i], maxRight[i]) - height[i];//计算体积
if(volume >= 0) sum += volume;//注意这里的volume一定要大于等于0才能加
}
return sum;
}
时间复杂度:O(n)
空间复杂度:O(n)
3. 单调栈
采用单调栈方法的话,首先要弄清楚是递增栈还是递减栈。
很明显,这里是递增栈,也就是从栈顶到栈底元素递增的状态。
当遍历元素小于等于栈顶元素的时候,可以直接将它们加入栈里面。
当遍历元素大于栈顶元素的时候,遍历元素,栈顶元素,以及栈顶元素的下一个元素,组成了一个大小大的一个凹字的状态,而这也就是我们所要求的雨水体积。
前面说了,单调栈是按行来计算的,于是我们要计算宽,计算高度,最后相乘累加。
高度等于栈顶元素两边较大元素中的较小值减去栈顶元素所对应的高度值,而宽呢就等于从右边第一个最大元素对应下标到左边第一个最大元素对应下标相减,但是这里注意因为是只计算中间的宽度,所以还需要多减一个1。
最后累加求和即可。
int trap(vector<int>& height) {
stack<int> st;//存放遍历过的下标
int sum = 0;//记录最终结果
st.push(0);
for(int i = 1; i < height.size(); i ++){
if(height[i] < height[st.top()]){//遍历元素小于栈顶元素
st.push(i);
}else if(height[i] == height[st.top()]){//遍历元素等于栈顶元素
st.pop();//这里可以不用删除,直接添加,效果是一样的
st.push(i);
}else{//遍历元素大于栈顶元素
while(!st.empty() && height[i] > height[st.top()]){
int mid = st.top();//取出栈顶元素作为容器底部
st.pop();
if(!st.empty()){//这里需要判断栈是否为空
int h = min(height[st.top()], height[i]) - height[mid];//计算容器高度
int w = i - st.top() - 1;//计算容器宽度
sum += h * w;//添加雨水体积
}
}
st.push(i);
}
}
return sum;
}
将上面的代码精简化后如下图所示。
int trap(vector<int>& height) {
stack<int> st;//存放遍历过的下标
int sum = 0;//记录最终结果
st.push(0);
for(int i = 1; i < height.size(); i ++){
while(!st.empty() && height[i] > height[st.top()]){
int mid = st.top();
st.pop();
if(!st.empty()){//注意这里需要判断栈是否为空
int h = min(height[i], height[st.top()]) - height[mid];
int w = i - st.top() - 1;//这里只需要计算中间部分的长度,所以还需要多减1
sum += h * w;
}
}
st.push(i);
}
return sum;
}
时间复杂度:O(n)
空间复杂度:O(n)
LeetCode84.柱状图中的最大矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路:这里和上面的题目大体相似,但是一些细节上有一些区别。
这里分为双指针法以及单调栈方法讲解。
1. 双指针法
这里如果采用暴力方法的话就是对每一个元素求出它左边元素的第一个最小值的下标,以及右边元素的第一个最小值下标,两个下标一减,即得到了宽度(记得这里还是需要多减一个1,因为计算的是中间的宽度),然后高度选择这个遍历的元素即可。
暴力方法容易超时,所以我们选择在主循环外面开始进行计算。
这里注意初始值的设置,不管是求左边元素的第一个最小值下标,还是右边元素第一个最小值下标,这里都要注意初始值的设置,否则会进入死循环。
当统计完成后,在主循环中直接调用计算值,保留最大值即可。
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeft(heights.size());
vector<int> minRight(heights.size());
//记录每个元素左边第一个最小值下标
minLeft[0] = -1;//初始化为-1,防止下面的while循环无限循环
for(int i = 1; i < heights.size(); i ++){
int t = i - 1;
while(t >= 0 && heights[t] >= heights[i]) t = minLeft[t];//这里如果使用t--逻辑上没错,但是会超限
minLeft[i] = t;
}
//记录每个元素右边第一个最小值下标
minRight[heights.size() - 1] = heights.size();//同样的道理,这里的设置是为了防止下面的循环陷入死循环
for(int i = heights.size() - 2; i >= 0; i --){
int t = i + 1;
while(t < heights.size() && heights[t] >= heights[i]) t = minRight[t];//同样这里使用t++逻辑上没错,但是会超限
minRight[i] = t;
}
int result = 0;//记录最终结果
for(int i = 0; i < heights.size(); i ++){
int sum = heights[i] * (minRight[i] - minLeft[i] - 1);//这里在计算每个元素所在位置的最大值,长*宽
result = max(sum, result);//及时更新最大值
}
return result;
}
2. 单调栈
采用单调栈的话,和上面接雨水的区别就在于这里选择的是递减栈了。 也就是从栈顶到栈底的元素呈递减的状态。
当遍历元素大于等于栈顶元素时,直接入栈即可;
当遍历元素小于栈顶元素的时候,遍历元素,栈顶元素,栈顶元素的下一个元素,构成了求体积的三个关键位置参数,形成一个凸结构。
宽度就等于右边元素减去左边元素,还需要多减一个1;长度也就是高度就等于栈顶元素的高度,这样就能得到遍历这个元素时的体积,不断更新result的最大值,最后返回result即可。
注意这里在heights元素的前后都添加了一个0。
在首位置添加0是因为当heights中的元素为倒序比如[8,7,6,5]时,首先进入8,当遍历7时,由于7比8小,那么会进入第三种情况,开始计算体积,但是当8弹出栈后,由于栈里面没有元素了,那么就会跳过这种情况,7入栈,然后开始遍历6,会重复上面的情况,最后体积完全没有计算到,返回的result等于0,显然这种情况是错误的,所以这里在首位置添加了一个0;
在末尾位置添加0是因为当heights中的元素为正序[6,7,8,9]时,由于每个元素都碰不到比自己小的元素,所以没办法进入第三种情况计算体积,最后result还是为0。所以这里在最后添加一个0,能够开始计算体积,返回最终结果。
int largestRectangleArea(vector<int>& heights) {
int result = 0;//记录最终结果
stack<int> st;
st.push(0);
heights.insert(heights.begin(), 0);//在heights的首元素之前添加一个0元素
heights.push_back(0);//在heights的末尾元素添加一个0元素
for(int i = 1; i < heights.size(); i ++){
if(heights[i] > heights[st.top()]){//遍历元素大于栈顶元素
st.push(i);
}else if(heights[i] == heights[st.top()]){//遍历元素等于栈顶元素
st.push(i);
}else{//遍历元素小于栈顶元素
while(!st.empty() && heights[i] < heights[st.top()]){
int mid = st.top();
st.pop();
if(!st.empty()){
int w = i - st.top() - 1;
int h = heights[mid];
result = max(result, h * w);
}
}
st.push(i);
}
}
return result;
}
下面的代码是将上面的代码精简后的。
int largestRectangleArea(vector<int>& heights) {
int result = 0;//记录最终结果
stack<int> st;
st.push(0);
heights.insert(heights.begin(), 0);//在heights的首元素之前添加一个0元素
heights.push_back(0);//在heights的末尾元素添加一个0元素
for(int i = 1; i < heights.size(); i ++){
while(!st.empty() && heights[i] < heights[st.top()]){
int mid = st.top();
st.pop();
if(!st.empty()){
int h = heights[mid];
int w = i - st.top() - 1;
result = max(h*w, result);
}
}
st.push(i);
}
return result;
}
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!