文章目录
- 739. 每日温度
- 496.下一个更大元素 I
- 503.下一个更大元素II
- 方法一 数组拼接
- 方法二 模拟遍历两边nums
- 42. 接雨水
- 双指针 暴力法
- 双指针优化
- 单调栈
- 84.柱状图中最大的矩形
- 暴力解法
- 双指针解法
- 单调栈
- 总结
739. 每日温度
-
暴力解法,双层for循环,时间复杂度是O(n^2)。
-
什么时候用单调栈?
通常是一维数组,要找任一个元素的右边(左边)第一个比自己大(小)的元素的位置,此时优先考虑单调栈,时间复杂度为O(n)。 -
使用单调栈时,首先要明确如下几点:
1)单调栈里存放的元素是什么?
单调栈只需要存放元素的下标i,T[i]直接获取对应的元素。
2)单调栈里元素是递增还是递减?
本题使用递增顺序(从栈头到栈底的顺序)。当单调栈递增,元素i入栈时,才能保证 元素i 是 栈顶元素 在数组中右边第一个比自己大的元素,也就是说找到栈顶元素 右边第一个比自己大的元素是 元素i。
3)三种情况:
当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
- C++实现
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
//递增栈
stack<int> st;//存放对应元素的下标
vector<int> result(temperatures.size(), 0);
st.push(0);//初始化
for(int i=1; i<temperatures.size(); i++)
{
if(temperatures[i] < temperatures[st.top()]) st.push(i);//情况1 当前元素比栈顶元素小 当前元素入栈
else if(temperatures[i] == temperatures[st.top()]) st.push(i);//情况2 当前元素等于栈顶元素 当前元素入栈
else//情况3 当前元素比栈顶元素大 保存结果 栈顶出栈 当前元素入栈
{
while(!st.empty() && temperatures[i] > temperatures[st.top()])
{
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
}
return result;
}
};
简化
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
//递增栈 简化
stack<int> st;
vector<int> result(temperatures.size(), 0);
st.push(0);
for(int i=1; i<temperatures.size(); i++)
{
while(!st.empty() && temperatures[i] > temperatures[st.top()])
{
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return result;
}
};
496.下一个更大元素 I
需要注意点:
-
result数组初始化应该为多少?
题目提示,如果不存在下一个更大元素就输出 -1,所以result数组初始化为-1。 -
遍历nums2时需要判断nums2[i]在nums1中是否出现过,因为最后是根据nums1元素的下标来更新result数组。
-
题目提示nums1 和 nums2是不重复的数组,因此可以用map做映射,根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。使用集合来解决哈希问题时,优先使用unordered_set,它的查询和增删效率是最优的。
-
单调递增栈顺序,栈头到栈底元素顺序要从小到大。栈内元素递增才能找到右边第一个比自己大的元素。
-
三种情况:
- 当前遍历的元素T[i]小于栈顶元素T[st.top()],满足递增栈,直接入栈。
- 当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况,直接入栈,求的是右边第一个比自己大的元素,而不是大于等于
- 当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况,不满足递增栈,找到右边第一个比自己大的元素,对元素进行处理:栈里的元素是nums2的元素,判断栈顶元素是否在nums1里出现过。如果出现过就记录,此时栈顶元素在nums2数组中右面第一个大的元素是当前遍历元素nums2[i]。
C++实现
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> result(nums1.size(), -1);//处理的是nums1的元素
if(nums1.size() == 0) return result;
//没有重复元素 map做映射 根据数值快速找到下标,判断nums2[i]是否在nums1中出现过。
unordered_map<int, int> umap;// key:下标元素,value:下标 为了快速查找元素
for(int i=0; i<nums1.size(); i++) umap[nums1[i]] = i;
st.push(0);
for(int i=1; i<nums2.size(); i++)
{
if(nums2[i] < nums2[st.top()]) st.push(i);
else if(nums2[i] == nums2[st.top()]) st.push(i);
else{
while(!st.empty() && nums2[i] > nums2[st.top()])
{
if(umap.count(nums2[st.top()]) > 0)//判断nums2[i]是否在nums1中出现过
{
int index = umap[nums2[st.top()]];
result[index] = nums2[i];//nums2[i]在nums1出现过,记录结果
}
st.pop();
}
st.push(i);
}
}
return result;
}
};
简化
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
//三种情况合并写
stack<int> st;
vector<int> result(nums1.size(), -1);
if(nums1.size() == 0) return result;
st.push(0);
//映射容器
unordered_map<int, int> umap;
for(int i=0; i<nums1.size(); i++) umap[nums1[i]] = i;
//遍历nums2
for(int i=1; i<nums2.size(); i++)
{
while(!st.empty() && nums2[i] > nums2[st.top()])
{
if(umap.count(nums2[st.top()]) > 0)
{
int index = umap[nums2[st.top()]];
result[index] = nums2[i];
}
st.pop();
}
st.push(i);
}
return result;
}
};
503.下一个更大元素II
在题496变形,一个循环数组找下一个更大的元素
方法一 数组拼接
把nums数组首尾拼接组成新的nums来操作,最后resize再返回结果。
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
//拼接数组
vector<int> nums1(nums.begin(), nums.end());
nums.insert(nums.end(), nums1.begin(), nums1.end());
vector<int> result(nums.size(), -1);
if(nums.size() == 0) return result;
stack<int> st;
st.push(0);
for(int i=1; i<nums.size(); i++)
{
if(nums[i] < nums[st.top()]) st.push(i);
else if(nums[i] == nums[st.top()]) st.push(i);
else
{
while(!st.empty() && nums[i] > nums[st.top()])
{
result[st.top()] = nums[i];
st.pop();
}
st.push(i);
}
}
result.resize(nums.size() / 2);
return result;
}
};
方法二 模拟遍历两边nums
在遍历的过程中模拟走了两边nums,用i % nums.size()来代替i 操作
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
//遍历中模拟走了两边nums 用i % nums.size()来操作
vector<int> result(nums.size(), -1);
if(nums.size() == 0) return result;
stack<int> st;
st.push(0);
for(int i=1; i<nums.size() * 2; i++)
{
if(nums[i % nums.size()] < nums[st.top()]) st.push(i % nums.size());
else if(nums[i % nums.size()] == nums[st.top()]) st.push(i % nums.size());
else
{
while(!st.empty() && nums[i % nums.size()] > nums[st.top()])
{
result[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
}
return result;
}
};
简化
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
//简化 遍历中模拟走了两边nums 用i % nums.size()来操作
vector<int> result(nums.size(), -1);
if(nums.size() == 0) return result;
stack<int> st;
st.push(0);
for(int i=1; i<nums.size() * 2; i++)
{
while(!st.empty() && nums[i % nums.size()] > nums[st.top()])
{
result[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
return result;
}
};
42. 接雨水
双指针 暴力法
按照列来计算,宽度w=1,每一列雨水的高度取决于该列左侧最高的柱子和 右侧最高的柱子 ,两者中较矮的柱子高度。
- 列4的雨水高度 = min(列3高度, 列7高度) — 列4高度,height = min(lefth, righth) - h,列4雨水体积 = w * height。
- 列4 左侧最高的柱子是列3,高度为2,lefth;
- 列4 右侧最高的柱子是列7,高度为3,righth;
- 列4 柱子的高度h为1;
- 从头遍历所有列,求出每一列的雨水体积,再累加就是总雨水体积。注意第一个柱子和最后一个柱子不接雨水。
- 双指针-超时 时间复杂度是 O ( n 2 ) O(n^2) O(n2)
class Solution {
public:
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 leftheight = height[i];// 记录左边柱子的最高高度
int rightheight = height[i];// 记录右边柱子的最高高度
for(int l=i-1; l>=0; l--)
{
if(height[l] > leftheight) leftheight = height[l];
}
for(int r=i+1; r<height.size(); r++)
{
if(height[r] > rightheight) rightheight = height[r];
}
//统计高度差 累加
int h = min(rightheight, leftheight) - height[i];
if(h > 0) sum += h;
}
return sum;
}
};
双指针优化
暴力解法中,通过列来计算每一列的雨水体积再累加求出雨水总体积,其中,当前列雨水面积 = 1 * (min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度)。双指针遍历时,每到一个柱子都向两边遍历一遍,有重复计算。
优化:把每一个位置的左边最高高度记录在一个数组上maxLeft,右边最高高度记录在一个数组上maxRight,避免了重复计算:
- 从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
- 从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
- 时间复杂度是 O ( n ) O(n) O(n)
class Solution {
public:
int trap(vector<int>& height) {
//双指针法
if(height.size() <= 2) return 0;
//统计当前柱子左右两侧柱子最高高度
vector<int> maxleft(height.size(), 0);
vector<int> maxright(height.size(), 0);
int size = maxright.size();
//左边柱子 正序 当前柱子高度 容器上一个柱子高度
maxleft[0] = height[0];//for循环从1开始遍历
for(int i=1; i<size; i++) maxleft[i] = max(height[i], maxleft[i-1]);
//右边柱子 倒叙 当前柱子高度 容器上一个柱子高度
maxright[size-1] = height[size-1];
for(int i=size-2; i>=0; i--) maxright[i] = max(height[i], maxright[i+1]);
//累计高度 左右柱子中较矮的柱子高度-当前柱子高度=雨水长度
//求的是面积 宽度=1 相当于统计长度
int sum = 0;
int h = 0;
for(int i=0; i<size; i++)
{
h = min(maxright[i], maxleft[i]) - height[i];
if(h>=0) sum += h;
}
return sum;
}
};
单调栈
- 需要找一个元素,及其右边最大元素和左边最大元素来计算雨水面积
- 按照行方向来计算雨水,当前柱子左右两边柱子的高度差就是雨水面积的长度,下标差就是宽度
- 单调栈内元素的顺序是从小到大,如果当前柱子高度大于栈头元素高度,出现凹槽,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,添加元素就是凹槽右边的柱子。
- 栈里要保存什么数值:使用单调栈,也是通过 长 * 宽 来计算雨水面积的。长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算
- 单调栈处理逻辑,主要有三种情况
- 情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()]
- 情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()]
- 情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()]
- 注意点:
- 遇到相同高度的柱子,即相同的元素时,更新栈内下标,弹出栈内元素(旧下标),栈内加入新元素(新下标),如5、5、1、3,如果添加第二个5时,先将第一个5的下标弹出,再把第二个5入栈
- 栈内存放元素下标,通过height[stack.top()]直接获取对应下标的高度
- 栈中存放遍历过的元素,因此先将下标0入栈,
st.push(0);
class Solution {
public:
int trap(vector<int>& height) {
//单调栈
if(height.size() <= 2) return 0;
stack<int> st;//柱子下标-雨水长度
st.push(0);
int sum = 0;
int mid = 0;//凹槽最低
int h = 0, w = 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()])
{
mid = st.top();//注意这是柱子下标
st.pop();//弹出凹槽底部
if(!st.empty())//如果还有元素 该栈顶元素才是凹槽左边高度
{
h = min(height[st.top()], height[i]) - height[mid];
w = i - st.top() - 1; // 注意减一,只求中间宽度
sum += h * w;
}
}
st.push(i);
}
}
return sum;
}
};
简化
class Solution {
public:
int trap(vector<int>& height) {
//单调栈 简化
if(height.size() <= 2) return 0;
stack<int> st;
st.push(0);
int sum = 0;
int mid = 0, h = 0, w = 0;
for(int i=1; i<height.size(); i++)
{
while(!st.empty() && height[i] > height[st.top()])
{
mid = st.top();//凹槽底部
st.pop();
if(!st.empty())
{
h = min(height[i], height[st.top()]) - height[mid];
w = i - st.top() - 1;//减1才是凹槽底部的宽度
sum += h * w;
}
}
st.push(i);
}
return sum;
}
};
84.柱状图中最大的矩形
暴力解法
时间复杂度是 O ( n 2 ) O(n^2) O(n2)
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//暴力解法 双指针 找左右最高柱子
int sum = 0;
for(int i=0; i<heights.size(); i++)
{
int left = i;
int right = i;
for(; left>=0; left--)
{
if(heights[left] < heights[i]) break;
}
for(; right<heights.size(); right++)
{
if(heights[right] < heights[i]) break;
}
int w = right - left - 1;
int h = heights[i];
sum = max(sum, h*w);
}
return sum;
}
};
双指针解法
重点在于要记录每个柱子的左边第一个小于该柱子的下标,而不是高度。所以需要循环查找,使用while
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//双指针 优化
//记录的是左右两边最矮柱子中离得最近的柱子的下标
int size = heights.size();
vector<int> minleftindex(size);
vector<int> minrightindex(size);
// 记录每个柱子 左边第一个小于该柱子的下标
minleftindex[0] = -1;//初始化,防止下面while死循环
for(int i=1; i<size; i++)
{
int t = i-1;
//找到离得最近的左边最矮柱子 不用if,而是不断向左寻找的过程
while(t>=0 && heights[t] >= heights[i]) t = minleftindex[t];//记录该柱子的下标
minleftindex[i] = t;//容器中更新下标
}
// 记录每个柱子 右边第一个小于该柱子的下标
minrightindex[size-1] = size;
for(int i=size-2; i>=0; i--)
{
int t = i+1;
//不用if,而是不断向左寻找的过程
while(t<size && heights[t] >= heights[i]) t = minrightindex[t];
minrightindex[i] = t;
}
int sum = 0;
int result = 0;
for(int i=0; i<size; i++)
{
sum = heights[i] * (minrightindex[i] - minleftindex[i] - 1);
result = max(sum, result);
}
return result;
}
};
单调栈
- 题42是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子高度的柱子,因此单调栈的元素顺序是从大到小,这样才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
- 要求的最大面积是由当前元素(要入栈元素)、栈顶元素、栈顶下一个元素组成凸性面积:
- 当前元素是height[i] = 40,凸形的右边(相对于柱子60下标)
- 栈顶元素是height[st.top()] = 60,凸形的顶部,中间位置
- 栈顶下一个元素是50,凸形的左边(相对于柱子60下标)
- 单调栈处理逻辑,主要有三种情况
- 情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况
- 情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
- 情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//单调栈
int result = 0;
stack<int> st;
//原数组头尾加入数字0分别为了避免数组是降序 升序时无法去三个值组成面积
heights.insert(heights.begin(), 0);
heights.push_back(0);
st.push(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 left = st.top();
int right = i;
int w = right - left - 1;//只要最高柱子的宽度
result = max(result, w * heights[mid]);
}
}
st.push(i);
}
}
return result;
}
};
化简
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//单调栈 简化
int result = 0;
stack<int> st;
heights.insert(heights.begin(), 0);
heights.push_back(0);
st.push(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 left = st.top();
//int right = i;
//right - left - 1
int w = i - st.top() - 1;
result = max(result, heights[mid] * w);
}
}
st.push(i);
}
return result;
}
};
总结
- 什么时候用单调栈?
- 通常是一维数组,要找任一个元素的右边(左边)第一个比自己大(小)的元素的位置,此时优先考虑单调栈,时间复杂度为O(n)
- 找右边第一个比自己大的元素位置,单调递增栈——题739、题496、题503
- 找左边和右边第一个比自己大的元素位置,单调递增栈——题42
- 找左边和右边第一个比自己小的元素位置,单调递递减——题84
- 单调栈的原理是什么?为什么时间复杂度是O(n)?
- 单调栈的本质是空间换时间,因为在遍历过程中需要用一个栈来记录右边第一个比当前元素大的元素。
- 优点是整个数组只需要遍历一次,因此时间复杂度是O(n)。
- 单调栈是用来记录遍历过的元素。遍历数组时,如果不知道哪些元素是遍历过的,会导致无法得知是否有一个比当前元素更小的元素被遍历了。所以要用一个容器——单调栈,来记录遍历过的元素。
- 使用单调栈时,首先要明确如下几点:
1)单调栈里存放的元素是什么?
单调栈只需要存放元素的下标i,T[i]直接获取对应的元素。
2)单调栈里元素是递增还是递减?
如果求一个元素右边第一个更大元素,单调栈是递增的,如果求一个元素右边第一个更小元素,单调栈是递减的。
当单调栈递增,元素i入栈时,才能保证 元素i 是 栈顶元素 在数组中右边第一个比自己大的元素,也就是说找到栈顶元素 右边第一个比自己大的元素是 元素i。
3)三种情况:
当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况