单调栈总结+Leetcode实例
单调栈
1.模型识别
- 求左边第一个比当前数小/大的数
- 求右边第一个比当前数小/大的数
- 向前看比当前数都大/小的连续长度–等价于1
- 向后看比当前数都大/小的连续长度–等价于2
2.原理
例:求左边第一个比当前数小的数
维护一个单调递增栈,如果i<j, a[i] > a[j], 那么在j以后,a[i]永远不会作为答案被输出,因为a[j]比a[i]小,任意一个j以后的数向前看的时候会被a[j]挡住,看不到a[i]。
3.模板
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main() {
int n;
int x;
scanf("%d", &n);
while (n--) {
cin >> x;
while (stk[tt] >= x) tt--;
if (tt) cout << stk[tt] << " ";
else cout << -1 << " ";
stk[++tt] = x;
}
return 0;
}
4.例题基础版
1) LeetCode 739. 每日温度
-
题目:请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 -
思路:模板题,找右边第一个大于当前元素的位置,栈存储位置即可。
-
答案:
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& t) {
vector<int> res(t.size());
stack<int> st;
for (int i = t.size() - 1; i >= 0; i--) {
while (st.size() && t[i] >= t[st.top()]) st.pop();
if (st.size()) res[i] = st.top() - i;
st.push(i);
}
return res;
}
};
2)LeetCode 496. 下一个更大元素 I
- 题目:给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。 - 思路:对于nums2的每个元素,用vector记录右边第一个比他大的元素,然后建立哈希表存储这个结果,然后遍历nums1查找每个元素右边第一个比它大的元素。
- 答案:
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> hash; //用unordered_map哈希表来存,降低时间复杂度
vector<int> res;
stack<int> st;
for (int i = nums2.size() - 1; i >= 0; i--) {
int cur = nums2[i]; //该循环中反复使用的数,存下来降低时间复杂度
while(st.size() && st.top() <= cur) st.pop();
if(st.size()) hash[cur] = st.top();
else hash[cur] = -1;
st.push(cur);
}
for (int i = 0; i < nums1.size(); i++) {
res.push_back(hash[nums1[i]]);
}
return res;
}
};
3)LeetCode 503. 下一个更大元素 II
- 题目:给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
- 思路:破链成环,在原链后面把整个链复制一遍。新的链包含了答案需要的所有路径。
- 答案:
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
int n = nums.size();
vector<int> res(n);
nums.insert(nums.end(), nums.begin(), nums.end());
for (int i = nums.size() - 1; i >= 0; i--) {
int cur = nums[i];
while (st.size() && cur >= st.top()) st.pop();
if (i < n) { //遍历到对应元素再记录
if (st.empty()) res[i] = -1;
else {
res[i] = st.top();
}
}
st.push(cur);
}
return res;
}
};
4)LeetCode 901. 股票价格跨度
- 题目:编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。 - 思路:求过去小于等于今天价格的最大连续日数,转换成求过去第一个大于今天的位置。这道题要返回距离上一个大于今天数的位置,而不是数本身,并且写的是迭代器,得不到全部序列,所以需要存一下pair。
- 答案:
class StockSpanner {
public:
StockSpanner() {
}
stack<pair<int, int>> st;
int next(int price) {
int w = 1;
while(!st.empty() && st.top().first <= price) {
w += st.top().second;
st.pop();
}
st.push(pair<int, int>(price, w));
return w;
}
};
5)LeetCode 1019. 链表中的下一个更大节点
-
题目:给出一个以头节点 head 作为第一个节点的链表。链表中的节点分别编号为:node_1, node_2, node_3, … 。
每个节点都可能有下一个更大值(next larger value):对于 node_i,如果其 next_larger(node_i) 是 node_j.val,那么就有 j > i 且 node_j.val > node_i.val,而 j 是可能的选项中最小的那个。如果不存在这样的 j,那么下一个更大值为 0 。
返回整数答案数组 answer,其中 answer[i] = next_larger(node_{i+1}) 。 -
思路:我是先把链表存成了vector,然后用传统模板做的,但是似乎有遍历一遍的做法,空间复杂度会更低。到时候看Y总会不会讲啦啦啦。
-
答案:
class Solution {
public:
vector<int> nextLargerNodes(ListNode* head) {
stack<int> st;
vector<int> res;
ListNode* cur = head;
while(cur->next != NULL) {
res.push_back(cur->val);
cur = cur->next;
}
res.push_back(cur->val);
vector<int> ans(res.size(), 0);
for (int i = res.size() - 1; i >= 0; i--) {
while (!st.empty() && res[i] >= st.top()) st.pop();
if (!st.empty()) ans[i] = st.top();
st.push(res[i]);
}
return ans;
}
};
5.例题提高版
1)LeetCode 84. 柱状图中最大的矩形
-
题目:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。 -
思路:枚举每条边做上边界的情况,找出每条柱子左边和右边第一个低于它的柱子,算出它做上边界的面积。可以做常数优化:不用遍历两遍,在求左边界的同时,如果某条边出栈,意味着右边出现了第一个小于等于它的柱子,虽然我们想要的是小于它的柱子,但是在下一个等于它的柱子处可以求得它本身的正确答案,所以是正确的。
-
答案:
遍历两遍
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st1;
int n = heights.size();
vector<int> res1(n), res2(n);
for (int i = 0; i < heights.size(); i++) {
while(!st1.empty() && heights[i] <= heights[st1.top()]) st1.pop();
if(!st1.empty()) res1[i] = st1.top();
else res1[i] = -1;
st1.push(i);
}
st1 = stack<int>();
for (int i = n - 1; i >= 0; i--) {
while(!st1.empty() && heights[i] <= heights[st1.top()]) st1.pop();
if(!st1.empty()) res2[i] = st1.top();
else res2[i] = n;
st1.push(i);
}
int m = 0;
for (int i = 0; i < heights.size(); i++) {
m = max(m, heights[i] * (res2[i] - res1[i] - 1));
}
return m;
}
};
优化:虽然是常数优化,但是时间复杂度排名一下子提到90%以上了。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st1;
int n = heights.size();
vector<int> res1(n, -1), res2(n, n);
for (int i = 0; i < heights.size(); i++) {
while(!st1.empty() && heights[i] <= heights[st1.top()]) {
res2[st1.top()] = i;
st1.pop();
}
if(!st1.empty()) res1[i] = st1.top();
st1.push(i);
}
int m = 0;
for (int i = 0; i < heights.size(); i++) {
m = max(m, heights[i] * (res2[i] - res1[i] - 1));
}
return m;
}
};
2)LeetCode 42. 接雨水
-
题目:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
-
思路:我开始又找到了两边高于当前柱子的位置,然后枚举每条边当下边界的情况算,总的来说需要考虑的事情多一些,而且需要遍历两遍。只遍历一遍的最优解是出现凹槽的右端点再加上这段面积,和5一样,利用的原理是:当一个数出栈时说明找到了右边大于当前的数,这里注意没有取等号。
-
答案:
class Solution {
public:
int trap(vector<int>& h) {
stack<int> st1;
int n = h.size();
if (n == 0) return 0;
int ans = 0;
for (int i = 0; i < n; i++) {
while(!st1.empty() && h[i] > h[st1.top()]) {
int bot = st1.top();
st1.pop();
if (st1.empty()) break;
int left = st1.top();
ans = ans + (min(h[left], h[i]) - h[bot]) * (i - left - 1);
}
st1.push(i);
}
return ans;
}
};
3)LeetCode 85. 接雨水
-
题目:给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
-
思路:枚举每条边做下边界,以这条边界为底算柱状图中的最大矩形(利用LeetCode84);最后比较各条下边界做底的最大值。
-
答案:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st1;
int n = heights.size();
vector<int> res1(n, -1), res2(n, n);
for (int i = 0; i < heights.size(); i++) {
while(!st1.empty() && heights[i] <= heights[st1.top()]) {
res2[st1.top()] = i;
st1.pop();
}
if(!st1.empty()) res1[i] = st1.top();
st1.push(i);
}
int m = 0;
for (int i = 0; i < heights.size(); i++) {
m = max(m, heights[i] * (res2[i] - res1[i] - 1));
}
return m;
}
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> h(n, vector<int>(m));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (matrix[i][j] == '1') {
if (i) {
h[i][j] = 1 + h[i - 1][j];
}
else h[i][j] = 1;
}
}
}
int res = 0;
for (int i = 0; i < n; i++) {
res = max(res, largestRectangleArea(h[i]));
}
return res;
}
};
6.例题变形版(有限制的单调栈)
1)LeetCode 402. 移掉K位数字
-
题目:给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
-
思路:在去除k个数字之前,维护单调栈递增,直到已经删除k个数字;如果最后长度多于len-k个数字,只取前len-k个数字;记得去掉结果的所有前导零,在长度大于1的时候。如果删除长度等于字符串本身长度,直接返回0.
-
答案:
class Solution {
public:
string removeKdigits(string num, int k) {
if (num.size() == k) return "0";
vector<int> stk;
int targetLen = num.size() - k;
for (int i = 0; i < num.size(); i++) {
while (stk.size() && stk.back() > num[i] && k) {
stk.pop_back();
k--;
}
stk.push_back(num[i]);
}
string res;
for (int i = 0; i < targetLen; i++) {
res += stk[i];
}
while (res[0] == '0' && res.size() > 1) res = res.substr(1);
return res;
}
};
2)LeetCode 316. 去除重复字母
- 题目:给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
- 思路:维护单调栈递增,注意以下限制条件
– 当前字符的可删除数量小于等于1时,不能够再删除;
– 用一个set记录当前栈里字符串包含字符,当碰到栈里已有的字符时,跳过 。 - 答案:
用字符串来模拟栈,节省一些空间。
class Solution {
public:
string removeDuplicateLetters(string s) {
unordered_map<char, int> hash;
for (int i = 0; i < s.size(); i++) {
if (hash.count(s[i])) hash[s[i]]++;
else hash[s[i]] = 1;
}
string stk;
unordered_set<char> map;
for (int i = 0; i < s.size(); i++) {
if (map.count(s[i])) {
hash[s[i]] -= 1;
continue;
}
while (stk.size() && s[i] < stk.back() && hash[stk.back()] > 1) {
hash[stk.back()] -= 1;
map.erase(stk.back());
stk.pop_back();
}
map.insert(s[i]);
stk += s[i];
}
return stk;
}
};
3)LeetCode 321. 拼接最大数
-
题目:给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。
求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。
说明: 请尽可能地优化你算法的时间和空间复杂度。 -
思路:枚举两个数组中取数的个数,使其加起来为k;分别求两个数组中取i和j个数最大的取法;最后按照一定顺序合并;比较每种枚举方法拼接结果的大小。
-
答案:
class Solution {
public:
vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
vector<int> res;
if (k == nums1.size() + nums2.size()) return merge(nums1, nums2); //没有选择余地
//枚举从两个数组中取数的个数
for (int i = 0; i <= min(int(nums1.size()), k); i++) {
int j = k - i;
if (j <= min(int(nums2.size()), k)) {
//贪心:两个数组中的最大拼接起来就是全局最大
auto a = getMaxNum(nums1, i);
auto b = getMaxNum(nums2, j);
auto temp = merge(a, b);//合并
//更新
if (!res.size()) res = temp;
else if (compare(res, 0, temp, 0) <= 0) res = temp;
}
}
return res;
}
//调用lc402的结果计算给定数组中选k位最大的数
vector<int> getMaxNum(vector<int>& nums, int k) {
vector<int> stk;
int popNum = nums.size() - k;
for (int i = 0; i < nums.size(); i++) {
while (popNum && stk.size() && nums[i] > stk.back()) {
stk.pop_back();
popNum--;
}
stk.push_back(nums[i]);
}
while (popNum) {
stk.pop_back();
popNum--;
}
return stk;
}
//难点在于两个vector并非一定是严格降序的,因此需要比较两个数剩余部分的大小来决定先拼接谁
vector<int> merge(vector<int>& a, vector<int>& b) {
int i = 0, j = 0;
vector<int> res;
while (i < a.size() || j < b.size()) {
if (compare(a, i, b, j) > 0) {
res.push_back(a[i++]);
}
else {
res.push_back(b[j++]);
}
}
return res;
}
//比较两个vector某个index之后的部分大小
int compare(vector<int>& a, int index1, vector<int>& b, int index2) {
int i = 0;
while (index1 < a.size() && index2 < b.size()) {
int difference = a[index1++] - b[index2++];
if (difference != 0) return difference;
}
//跳出循环是因为某个vector已经遍历到末尾,那么谁还剩下数谁优先
return (a.size() - index1) - (b.size() - index2);
}
};