单调栈的性质
- 单调栈里的元素具有单调性(单调递增单调递减)
- 元素在入栈之前,通过判断会先把栈顶元素破坏单调性的元素都删除
- 使用单调栈可以找到元素向左遍历第一个比它小的元素,也可以找到元素向右遍历第一个比它大的元素(能拓展的最大宽度确定)
它的时间复杂度是线性的,所有元素只会入栈一次,且出栈了就不会再入栈,所以时间复杂度是O(n)。
leetcode 496下一个更大元素
暴力
解题思路:若nums1的长度为n,nums2的长度为m,分析时间复杂度:双重循环O(m*n),空间复杂度,若不考虑返回值必要的存储空间,空间复杂度为O(1)
- 先在nums2中找到nums1[i]的位置,再在nums2中对应元素的右边找到第一个大于nums1[i]的元素
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size(),len2 = nums2.size();
vector<int> res(len1);
for(int i = 0;i < len1;i++){
int ans = -1,temp = INT_MAX;
for(int j = 0;j < len2;j++){
if(nums2[j] == nums1[i]){ //先在nums2中找到nums1[i]的相对位置
temp = nums1[i];
}
if(nums2[j] > temp){
ans = nums2[j];
break; //找到右边第一个大于的元素直接跳出循环
}
}
res[i] = ans;
}
return res;
}
};
单调栈
解题思路:考虑在暴力的基础上优化时间复杂度,因为nums1中的元素全都包含在nums2中,将问题转化为求nums2中每个元素下一个比它大的元素,将其对应关系存储在哈希表中,再从哈希表中找到以nums1[i]为key的value存入结果中
-
求nums2中下一个更大的元素,需要维护一个单调递减的栈,遇到比栈顶元素更大的元素,出栈,将栈顶元素与更大的元素的对应关系存入哈希表中
-
再遍历一遍哈希表,找到nums1[i]对应的更大的元素,存入结果数组
class Solution {
public:
//优化时间复杂度:O(n+m),以空间换时间,O(2n)->O(n)
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size(),len2 = nums2.size();
vector<int> res(len1,-1); //初始化为-1
stack<int> stk;
map<int,int> record;
for(int i = 0;i < len2;i++){ //求出在nums2中每一个元素右边第一个大于它的元素
while(!stk.empty() && nums2[i] > stk.top()){
record[stk.top()] = nums2[i]; //将对应关系存入map中
stk.pop();
}
stk.push(nums2[i]);
}
for(int i = 0;i < len1;i++){ //在map中找到nums1的key
map<int,int>::iterator it = record.find(nums1[i]);
if( it != record.end()){
res[i] = it->second;
}
}
return res;
}
};
leetcode 503下一个更大元素||
解题思路:与上一题中求下一个更大的元素不同之处在于,此题是循环数组,所以考虑在第一次遍历完数组后,栈中还有元素(说明没有在后面找到元素,还得循环遍历在该元素的前面找(这里可以直接再遍历一遍数组))
- 第一次遍历维护一个单调递减栈,栈中存储当前元素下标,当前元素大于栈顶下标对应元素,栈顶下标出栈,栈顶对应下标的元素的下一个更大元素就是当前元素(记录在结果中)
- 若栈不空再次遍历数组,目的为了找出栈中下标对应元素前面有无大于它的元素
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
int len = nums.size();
stack<int> record; //记录当前元素的下标
vector<int> res(len,-1); //初始化为-1
for(int i = 0;i < len;i++){
while(!record.empty() && nums[record.top()] < nums[i]){
res[record.top()] = nums[i];
record.pop();
}
record.push(i);
}
if(!record.empty()){
for(int i = 0;i < len;i++){ // 多一次遍历数组,时间复杂度依旧O(n)
while(!record.empty() && nums[record.top()] < nums[i]){
res[record.top()] = nums[i];
record.pop();
}
}
}
return res;
}
};
leetcode 1019链表中的下一个结点
解题思路:与以上几道题不同之处在于,此题给出的是链表,先直接将其转为数组,解题步骤与以上相同
class Solution {
public:
vector<int> nextLargerNodes(ListNode* head) {
if(!head){
return {}; //空处理
}
vector<int> listArray;
stack<int> stk;
//先将链表中的元素转为数组
while(head){
listArray.push_back(head->val);
head = head->next;
}
vector<int> res(listArray.size(),0);//默认初始值为0
for(int i = 0;i < listArray.size();i++){
while(!stk.empty() && listArray[i] > listArray[stk.top()]){
res[stk.top()] = listArray[i];
stk.pop(); //栈中保存的是元素对应下标
}
stk.push(i);
}
return res;
}
};
leetcode 739每日温度
解题思路:
-
需要找到升温等待的天数,所以可以维护一个单调递减栈,当温度大于栈顶温度,栈顶温度出栈,栈顶温度对应索引处需要等待的天数=升温处索引减去当前温度的索引
-
栈中保存的是当前温度以及相应的索引
-
在这之后都无升温的元素处不用处理,所以定义保存结果的数组时,初始化为0
暴力解法:
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
int len = T.size();
if(len == 1){
return {0};
}
vector<int> res(len,0), next(101,INT_MAX);
for(int i = len - 1;i >= 0;i--){
int warmerIndex = INT_MAX;
for(int t = T[i] + 1;t <= 100;t++){ //寻找所有可能大于当前的温度的索引中最小值
warmerIndex = min(warmerIndex, next[t]);
}
if(warmerIndex != INT_MAX){
res[i] = warmerIndex - i; //vector默认初始值为0
}
next[T[i]] = i; //记录的记录的是下一个升温出现的第一个索引 ,因为是反向遍历的
}
return res;
}
};
单调栈:栈中存的当前温度和索引
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
int len = T.size();
if(len == 1){
return {0};
}
vector<int> res(len,0);
stack<pair<int,int>> record; //维护一个单调递减的栈
for(int i = 0;i < len;i++){
while(!record.empty() && T[i] > record.top().first){
int cur = record.top().second; //栈顶元素的下标
res[cur] = (i - cur); //计算等待的天数
record.pop();
}
record.push(make_pair(T[i],i));
}
return res;
}
};
单调栈:栈中存储当前元素的下标
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
int len = T.size();
if(len == 1){
return {0};
}
vector<int> res(len,0);
stack<int> record; //维护一个单调递减的栈,栈中保存当前元素的下标值
for(int i = 0;i < len;i++){
while(!record.empty() && T[i] > T[ record.top()]){
res[record.top()] = (i - record.top());
record.pop();
}
record.push(i);
}
return res;
}
};
leetcode 31 下一个排列
解题思路:完全升序的字典序最小,完全降序的数组不存在下一个排列(它已经是所有元素构成的最大的字典序排列)
如果当前排列存在下一个排列,我们需要找到开始降序元素的前一个位置的元素(记为a[i]),a[i]需要从右边找到第一个大于它的元素a[j],交换这两个元素,并将新数组中从i + 1这个索引位置往后的元素变为升序(字典序号较小),得到的新排列就是当前排列的下一个排列
-
定义栈结构存元素对应的索引
-
反向遍历排列,维护一个单调递增栈(因反向遍历,则排列数组体现为降序),当遍历到排列中的元素小于栈顶下标对应的元素,说明当前元素是a[i],通过出栈向右找到第一个大于a[i]的元素 a[j] 下标j,找到了就退出循环
-
如果找到了 i 和 j 就先将其交换,然后再将a[i]以后的元素( i + 1 )翻转为升序,如果完全降序,直接翻转原排列
也可以使用两次遍历代替单调栈寻找a[i] a[j]
class Solution {
private:
void mySwap(vector<int>& nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
void myReverse(vector<int>& nums,int index){ //从index-末尾翻转
int i = index;
int j = nums.size()-1;
while(i < j){
mySwap(nums,i,j);
i++;
j--;
}
}
public:
void nextPermutation(vector<int>& nums) {
int len = nums.size();
stack<int> stk;
int nextPos = -1, prePos = -1;
for(int i = len - 1;i >= 0;i--){
while(!stk.empty() && nums[i] < nums[stk.top()]){ //维护一个单调递增栈
prePos = i; //nums[i]即是需要交换的元素,记录下标
nextPos = stk.top(); //记录右边第一个大于nums[i]的元素下标
stk.pop();
}
//找到了退出循环
if(nextPos != -1){
break;
}
stk.push(i); //栈中存的是当前元素的下标
}
if(nextPos != -1){ //完全降序排列,不用交换直接翻转
//交换
mySwap(nums,prePos,nextPos);
}
myReverse(nums,prePos+1); //将交换位置后下一个位置到排列尾都reverse
return;
}
};
leetcode 556 下一个更大元素
解题思路
- 此题实质求的是下一个排列,考虑将n的每一位存到数组中转为上题同样的问题,这里需要注意的是这里的下一个排列是一个数考虑溢出问题(使用long long 8个字节存储结果,判断溢出), 其余步骤与上题一致不赘述
class Solution {
private:
void mySwap(vector<int>& nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
void myReverse(vector<int>& nums,int pos){
int i = pos;
int j = nums.size()-1;
while(i < j){
mySwap(nums,i,j);
i++;
j--;
}
}
public:
int nextGreaterElement(int n){
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int len = nums.size();
myReverse(nums, 0); //数字顺序翻转
stack<int> stk;
int nextPos = -1, prePos = -1;
for(int i = len - 1;i >= 0;i--){
while(!stk.empty() && nums[i] < nums[stk.top()]){
prePos = i; //nums[i]即是需要交换的元素
nextPos = stk.top(); //找到右边第一个大于nums[i]的元素下标,有相同元素取的后一个
stk.pop();
}
if(prePos != -1){
break;
}
stk.push(i); //栈中存的是当前元素的下标
}
if(prePos != -1){ //找到nums[i]才交换
mySwap(nums,prePos,nextPos);
} else{
return -1; //完全降序
}
myReverse(nums,prePos+1); //将交换位置后下一个位置到排列尾都reverse
long long res = 0,count = 0; //将res定义为long型,能向下兼容
for(int i = len - 1;i >= 0 ;i--){
res += nums[i] * pow(10,count);
count++;
}
if(res > INT_MAX){ //判断是否溢出
return -1;
}
return res;
}
};
leetcode 42接雨水
解题思路:考虑能接雨水的条件,当一个位置的左右柱子高度都大于它的柱子高度时该位置可以储存雨水。维护一个单调递减栈,当前柱子高度大于栈顶柱子高度时,栈顶柱子位置处可以接水,此时的能接水的宽度的左边界为栈顶的下一个柱子位置,右边界为当前遍历到的柱子索引位置,接水的高度为左边界高度减去栈顶柱子高度与右边界高度减去栈顶柱子高度中的最小值。
-
定义一个栈用于存储当前柱子的索引坐标
-
遍历柱子数组,维护单调递减栈,当遍历到的柱子高度大于当前柱子高度,记录当前栈顶柱子索引,栈顶元素出栈,计算栈顶柱子能储水的宽度(右边界-左边界-1),储水量的高度(min(左边界的高度-栈顶高度,右边界-栈顶高度)),计算栈顶柱子处的储水量 = 高度*宽度
-
将当前储水量累加到结果中
-
满足单调递减栈的柱子索引入栈
class Solution {
public:
int trap(vector<int>& heights) {
int len = heights.size();
stack<int> stk;
int ans = 0;
for(int i = 0;i < len;i++){
while(!stk.empty() && heights[i] > heights[stk.top()]){
//右边界值为i
int cur = stk.top(); //满足接水的元素位置
stk.pop();
if(stk.empty()){ //当前栈空结束循环
break;
}
int left = stk.top(); //记录右边界的值
int curWidth = i - left - 1; //当前接数量的宽度
int curHeight = min((heights[i] - heights[cur]), (heights[left] - heights[cur]));
//计算接水量的面积
ans += curWidth * curHeight;
}
stk.push(i);
}
return ans;
}
};
leetcode 84柱状图中最大的矩形面积
解题思路:单调栈的解法与枚举高度类似,要使得当前高度能围成的矩形面积最大,需要向左右扩展能够到达的最大边界,因此我们需要维护一个单调递增的栈(栈中存放柱子的下标),当遍历到的柱子高度不满足单调递增时(大于栈顶柱子高度)说明它是栈顶柱子作为高度的右边界,左边界为栈顶元素的下一个元素,此时可以求出每个柱子作为高度时的最大面积,记录最大面积
- 定义一个栈用于存放所有柱子的下标
- 原数组首添0:便于计算左边界(避免对空栈处理),原数组末尾添0:使得栈中所有元素都能出栈(所有柱子高度能围成的最大面积都能求得)
- 遍历数组,不满足单调递增时,说明栈顶柱子找到了右边界,栈顶柱子位置出栈,它的前一个位置(现在的栈顶元素就是它的左边界),计算宽度,右边界-左边界-1
- 满足单调递增的柱子下标入栈
- 直到遍历完
int largestRectangleArea(vector<int>& heights){
int ans = 0;
stack<int> st;
heights.insert(heights.begin(), 0); //减少栈空的判断
heights.push_back(0); //使得前面所有元素都能出栈
for (int i = 0; i < heights.size(); i++){
while (!st.empty() && heights[st.top()] > heights[i]){
int cur = heights[st.top()]; //当前高度
st.pop();
ans = max(ans, (i - st.top() - 1) * cur); //左边界:st.top() 右边界i 在计算宽度的时候需要减1
}
st.push(i);
}
return ans;
}
leetcode 122买卖股票的最佳时机2
解题思路:寻找可以卖出(有收益)的结点,该结点卖出可以卖多个价格取最高的,将所有可以卖出结点的最高收益累加起来
- 维护一个单调递减栈,当前结点的价格高于栈顶元素,说明可以在这一天卖出,元素出栈,取最高收益
- 将所有收益累加
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
stack<int> stk; //维护一个单调递减栈,栈中存放第i天
int ans = 0;
for(int i = 0;i < len;i++){
int earnings = 0;
while(!stk.empty() && prices[i] > prices[stk.top()]){ //达到可以卖出股票时机
earnings = max(earnings,prices[i] - prices[stk.top()]); //取这天可以卖得的最高价格
stk.pop();
}
ans += earnings;
stk.push(i);
}
return ans;
}
};
leetcode 901股票价格跨度
解题思路:实质是当前价格向左边找第一个比当前价格大的位置(左边界),右边界就是当前位置,所以需要维护一个单调递减的栈,当前价格大于栈顶价格,左边界找到。跨度为右边界-左边界(包含当前位置),此题不能使用索引,可以使用pair记录当前价格以及对应的跨度
- 定义一个栈,栈中存放的pair(股票价格,对应的跨度)
- 调用next,当前栈空或者当前价格小于栈顶价格直接入栈,否则说明找到左边界,出栈、计算跨度
- 当前价格以及对应跨度入栈
class StockSpanner {
private:
stack<pair<int,int>> stk; //first->股票价格,second->跨度
public:
StockSpanner() {
}
int next(int price) {
if(stk.empty() || price < stk.top().first){
stk.push_back(price,1);
return 1;
}
int ret = 1; //包括当前元素自身
while(!stk.empty() && price >= s.top().first){ //维护一个单调递减栈
ret += stk.top().second; //将栈顶价格相应跨度累加
stk.pop();
}
stk.push(pair(price,ret));
return ret;
}
};