单调栈(矩形面积/贡献法/最小字典序)

单调栈(矩形面积/贡献法/最小字典序)

如果当前元素比栈顶小,直接入栈;否则,把所有小于等于它的元素弹出,再将其放入。因此,栈中的元素一定是有序的,称之为单调栈。

每日温度

从右向左

如果当前元素比栈顶小,直接入栈,并且当前元素的下一个更高温度就是1;否则,把所有小于等于它的元素弹出,再将其放入,当前元素的下一个更高温度就是栈顶元素。即,找到第一个大于当前元素的栈中元素,其值应该是栈顶元素下标-当前元素下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n);
stack<int> stk;

for(int i = n-1; i >= 0; i--){
// 找到第一个大于当前温度的元素
while(!stk.empty() && temperatures[i] >= temperatures[stk.top()]){
stk.pop();
}
// 栈顶元素下标-当前元素下标
if(!stk.empty()){
ans[i] = stk.top() - i;
}
stk.push(i);
}
return ans;
}
};
  • 时间复杂度:O(n),每个元素最多入栈和出栈一次。
  • 空间复杂度:O(n),返回值不计入,仅考虑栈的最大空间消耗。

从左向右

从左向右遍历元素,如果当前元素比栈顶小,直接入栈,等待出现更大元素;否则,把所有小于等于它的元素弹出,并且可以更新每一个弹出的元素的结果,即i - stk.top(),再将当前元素放入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n);
stack<int> stk;

// 从左向右遍历
for(int i = 0; i < n; i++){
// 找到第一个比当前温度小或相等的元素,注意等于的时候无法更新
while(!stk.empty() && temperatures[i] > temperatures[stk.top()]){
// 更新
ans[stk.top()] = i - stk.top();
stk.pop();
}
stk.push(i);
}

return ans;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

接雨水

单调栈

对于接雨水问题,也可以用单调栈的方法。从左往右遍历,如果当前元素比栈顶小,直接入栈,因为当前无法接雨水,直到当前元素比栈顶大,当前坑可以接的水为:取栈顶的下一个元素作为left,当前元素为right,宽度就是right - left - 1,高度为左右高度的较小值-栈顶柱子的高度,即min(height[left], height[i]) - height[stk.top()]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
stack<int> stk;
int ans = 0;

// 从左往右遍历
for(int i = 0; i < n; i++){
// 找到第一个小于当前温度的栈顶
while(!stk.empty() && height[i] > height[stk.top()]){
int bottom = stk.top();
stk.pop();
// 前面没有柱子了,无法接水
if(stk.empty()){
break;
}
int left = stk.top();
int dh = min(height[left], height[i]) - height[bottom];
ans += dh * (i - left - 1);
}
stk.push(i);
}
return ans;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

商品折扣后的最终价格

单调栈

从左向右遍历,当前元素大于栈顶,就入栈,否则不断出栈,并更新当前出栈元素的折扣价,即prices[stk.top()] - prices[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> finalPrices(vector<int>& prices) {
int n = prices.size();
vector<int> ans = prices;
stack<int> stk;

// 从左向右遍历
for(int i = 0; i < n; i++){
while(!stk.empty() && prices[i] <= prices[stk.top()]){
int j = stk.top();
stk.pop();
// 更新价格
ans[j] = prices[j] - prices[i];
}
stk.push(i);
}

return ans;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

不得不说灵神讲得太好了,我居然也能看过视频之后自己把题目做出来了╰(°▽°)╯ 后面再多做几道题练习一下~

下一个更大元素I

单调栈

  • 先将nums2所有数字存到哈希表中,这样可以通过O(1)的时间找到其索引。
  • 用单调栈的方法处理nums2,找其下一个更大元素的下标,存到数组里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size();
int n2 = nums2.size();
// 单调栈处理nums2
vector<int> mono(n2, -1);
stack<int> stk;
for(int i = 0; i < n2; i++){
while(!stk.empty() && nums2[i] > nums2[stk.top()]){
int j = stk.top();
stk.pop();
mono[j] = i;
}
stk.push(i);
}
// 哈希表处理nums2
unordered_map<int, int> hash;
for(int i = 0; i < n2; i++){
hash[nums2[i]] = i;
}
// 遍历nums1
vector<int> ans(n1);
for(int i = 0; i < n1; i++){
int index = hash[nums1[i]];
ans[i] = mono[index] == -1 ? -1 : nums2[mono[index]];
}
return ans;
}
};
  • 时间复杂度:O(n1 + n2)
  • 空间复杂度:O(n2)

上面的解法是自己写出来的,力扣官方题解省略了哈希表存储nums2的部分,节省了空间。灵神的做法是省去了存储nums2中其他元素的方法,只存nums1中存在的元素。

下一个更大元素

单调栈

和上一题的区别是,这道题的数组可以循环,也就是说,先找到数组元素下标后边的第一个比它大的元素,如果没有,可以从下标0开始找。可以把 nums 复制一份,拼在 nums 右边,这样就把环形数组变成一般数组了。例如 [1,2,1] 变成 [1,2,1,1,2,1]。无需真的把数组复制一份,而是用下标模 n 的方式取到对应的元素值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size();
vector<int> ans(n, -1);
stack<int> stk;
for(int i = 0; i < 2 * n; i++){
int x = nums[i % n];
while(!stk.empty() && x > nums[stk.top()]){
int j = stk.top();
stk.pop();
ans[j] = x; // 只需记录元素值而不是下标
}
if(i < n){
stk.push(i);
}
}
return ans;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

链表中的下一个更大节点

单调栈

可以先遍历一遍链表得到其长度,确定答案数组的大小。然后将每个节点保存在栈中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
vector<int> nextLargerNodes(ListNode* head) {
unordered_map<ListNode*, int> hash;
int n = 0;
ListNode* node = head;
while(node){
hash[node] = n++;
node = node->next;
}

vector<int> ans(n);
stack<ListNode*> stk;
node = head;
int index = 0;
while(node){
while(!stk.empty() && node->val > stk.top()->val){
int i = hash[stk.top()];
stk.pop();
ans[i] = node->val;
}
stk.push(node);
node = node->next;
}

return ans;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

上面是自己写的解法,官方题解省略了第一次遍历得到长度以及哈希表,在栈中存储的是节点的值和它的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
vector<int> nextLargerNodes(ListNode* head) {
vector<int> ans;
stack<pair<int, int>> stk;

ListNode* node = head;
int index = 0;
while(node){
ans.push_back(0); // 这样到最后也能知道ans的长度
while(!stk.empty() && stk.top().first < node->val){
ans[stk.top().second] = node->val;
stk.pop();
}
stk.emplace(node->val, index);
index++;
node = node->next;
}

return ans;
}
};

思路是一样的,但是节省了空间。

总结

单调栈的题目大概做了6道,基本的思路已经理解并且能够自己做出来不太复杂的题目。先到这里,以后再刷。


单调栈(矩形面积/贡献法/最小字典序)
http://example.com/2024/11/20/posts/monoStack/
作者
Xuan Yang
发布于
2024年11月20日
许可协议