单调栈
1.单调栈实现结构
单调栈解决的问题:给你一个数组,想要用尽可能低的代价知道数组中每一个元素的左边元素比它大的或者右边元素比他大的信息是什么。如果用暴力方法,左边遍历一次右边遍历一次,时间复杂度为O(N^2),用单调栈的方法可以使得时间复杂度变成O(N)。
【例】求数组[5 4 6 7]每个元素左、右两边比他大最近的元素下标。
5左边比他大的无、右边比它大的元素6对应的2;
4左边比它大的5对应的下标0、右边比它大的元素6对应的下标2;
6左边比它大的无、右边比它大的元素7对应的下标3;
7左边比它大的无、右边比它大的无。
1.1单调栈实现过程
以求数组中每个元素左右最近的比它大的元素下标为例(小的同理),**栈从栈底到栈顶要保持单调递减。**过程如下:
- 依次遍历数组元素,栈顶元素下标对应的数大于当前的元素,则将当前元素的下标放入栈中。
- 如果栈顶元素下标对应的数小于当前的元素,则弹出栈顶元素,记录栈顶元素下标的左边比它大的元素下标为弹出后的栈顶元素或者无,右边比它大的元素的下标为当前元素的下标。循环往复,直到栈顶元素对应的数大于当前元素,将当前元素的下标放入栈中。
- 当遍历完数组后,进入清算阶段,栈中还剩下的元素依次弹出,并如步骤2记录每个弹出的元素的左右比它大的元素的下标。
【例】数组[5 4 3 6 1 2 0],用单调栈找到数组中每一个元素,左右离它最近的元素的下标。
过程如下:
1.下标0、1、2元素入栈,由于遵循单调栈的单调性,直接插入栈中。
2.轮到3下标对应元素6准备入栈,但栈顶元素2下标对应元素3小于6,2下标对应元素3出栈。此时,记录下标2元素左边比它大的元素下标为1,右边比它大的元素下标为3.
3.轮到3下标对应元素6准备入栈,但栈顶元素1下标对应元素4小于6,1下标对应元素4出栈。此时,记录下标1元素左边比它大的元素下标为1,右边比它大的元素下标为3.
4.轮到3下标对应元素6准备入栈,但栈顶元素0下标对应元素5小于6,0下标对应元素5出栈。此时,记录下标0元素左边比它大的元素下标为无,右边比它大的元素下标为3.
5.下标4元素遵从单调栈的单调性,入栈。
6.轮到5下标对应元素2准备入栈,但栈顶元素4下标对应元素1小于2,4下标对应元素1出栈。此时,记录下标4元素左边比它大的元素下标为3,右边比它大的元素下标为5.
7.下标6元素遵从单调栈的单调性,入栈。
8.数组遍历结束,进入清算阶段,栈中元素依次弹出,记录每个弹出元素的左右下标。
1.2代码
1.数组中有重复值的情况。
思路:考虑在上述过程的基础上,将放入栈中的下标换成链表即可,如果栈顶下标元素和数组遍历到的数相同,就将相同的下标压到链表中。
map<int, pair<int, int>> getRes(vector<int>& nums) {
map<int, pair<int, int>> res; //存放每个元素左右离它最近的元素的下标
stack<list<int>> st;
//进入遍历阶段
for (int i = 0; i < nums.size(); ++i) {
if (st.empty() || nums[i] < nums[st.top().back()]) {
st.push(list<int>{i});
} else if (nums[i] == nums[st.top().back()]) {
st.top().push_back(i);
} else {
while (!st.empty() && nums[i] > nums[st.top().back()]) {
list<int> tmp = st.top();
st.pop();
for (int k : tmp) {
int left = st.empty() ? -1 : st.top().back();
res[k] = {left, i};
}
}
if (st.empty() || nums[st.top().back()] != nums[i]) {
st.push(list<int>{i});
} else {
st.top().push_back(i);
}
}
}
//进入清算阶段
while (!st.empty()) {
list<int> tmp = st.top();
st.pop();
for (int k : tmp) {
int left = st.empty() ? -1 : st.top().back();
res[k] = {left, -1};
}
}
return res;
}
2.数组中无重复值的情况
//待放入单调栈的数组
map<int, pair<int, int>> getRes(vector<int>& nums) {
map<int, pair<int, int>> res; //存放每个元素左右离它最近的元素的下标
stack<int> st;
//进入遍历阶段
for (int i = 0; i < nums.size(); ++i) {
if (nums[st.top()] > nums[i]) {
st.push(i);
continue;
}
while (!st.empty() && nums[st.top()] < nums[i]) {
int tmp = st.top();
st.pop();
int left = st.empty() ? -1 : st.top();
res[tmp] = { left, i };
}
st.push(i);
}
//进入清算阶段
while (!st.empty()) {
int tmp = st.top();
st.pop();
int left = st.empty() ? -1 : st.top();
res[tmp] = { left, -1 };
}
return res;
}
2.例题
单调栈中问题可以根据题目的不同进行简化,但主要思想是通过求解每个位置左右比它大(小)位置的信息来求解问题。可能只需要用到左边信息,可能只用到右边信息,解题时可以随着题目的不同来简略上述代码。
【例1】接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
- 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
- 输出:6
- 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
【思路】
可以是单调栈的经典题目。思路如下:
- 可以看出只有左右都存在比当前位置都高的柱子才能够放得下雨水。即需要知道每个位置元素的比它左右都大的元素下标。
- 那么所有的雨水该怎么计算?由于木桶效应,能存放的水由最短的那根木桶决定,这题也是一样,如果一个位置左右都存在最大值,那么能存放的雨水由左右比它大的最低的那根柱子决定。 即第i个位置左右比它高的柱子下标为left和right,存放的雨水为(right - left - 1) × min(height[left], height[right]).
- 但如果存在连续的左右更大值的位置,如事例中第4、5、6位置,用这个方法会重复算了一部分面积,这主要是因为连续位置中存在两个位置的下标的高度相同,导致重复算了这部分的雨水面积。那我们对连续位置中存在相同位置的下标只算一次就行。
- 回到代码部分,由于清算部分一定不存在右边比它大的位置,所以这部分肯定不能存放雨水可以忽略掉。
- 只有遍历部分存在左右都有比它高的位置,我们就找出这部分位置,求出这些位置所对应的雨水。
- 而对于连续位置中存在不同位置下标高度相等的柱子,由于栈中存放的是下标的链表,如果出现上述情况,这些不同位置的下标会被放在同一个链表中,那么我们只要对链表中的高度计算一次雨水面积就可以。
class Solution {
public:
int trap(vector<int>& nums) {
int res = 0; //存放每个元素左右离它最近的元素的下标
stack<list<int>> st;
//遍历阶段
for (int i = 0; i < nums.size(); ++i) {
if (st.empty() || nums[i] < nums[st.top().back()]) {
st.push(list<int>{i});
} else if (nums[i] == nums[st.top().back()]) {
st.top().push_back(i);
} else {
while (!st.empty() && nums[i] > nums[st.top().back()]) {
list<int> tmp = st.top();
st.pop();
int left = st.empty() ? -1 : st.top().back();
int right = i;
if (left == -1) {
continue;
}
int w = right - left - 1;
int h = min(nums[left], nums[right]) - nums[tmp.back()];
res += w * h;
}
if (st.empty() || nums[st.top().back()] != nums[i]) {
st.push(list<int>{i});
} else {
st.top().push_back(i);
}
}
}
return res;
}
};
【例2】柱状图中的最大矩形
【思路】
- 这题与接雨水相反,是要找到每个元素左右两边比它小的元素的下标。
- 如何求最大的元素呢?我们可以这么想,每个元素的高度可以往左右延申多宽,用它的宽度 × 这个元素的高度就是这个元素所能延申最大的面积,遍历求出每个元素所能延申的最大面积就能求出整个的最大面积了。
- 那么问题就转变成如何求每个元素的最大延申宽度,那么我们找到左右比它小的元素的下标,再用right - left - 1即是最大延伸宽度,如果左右不存在比它小的元素,则令left = - 1,right = height.size()即可。
class Solution {
public:
map<int, pair<int, int>> getRes(vector<int> &nums) {
stack<list<int>> st;
map<int, pair<int, int>> res;
//遍历阶段
for (int i = 0; i < nums.size(); ++i) {
if (st.empty() || nums[i] > nums[st.top().back()]) {
st.push(list<int>{i});
} else if (nums[i] == nums[st.top().back()]) {
st.top().push_back(i);
} else {
while (!st.empty() && nums[i] < nums[st.top().back()]) {
list<int> tmp = st.top();
st.pop();
int left = st.empty() ? -1 : st.top().back();
for (int k : tmp) {
res[k] = {left, i};
}
}
if (st.empty() || nums[i] != nums[st.top().back()]) {
st.push(list<int>{i});
} else {
st.top().push_back(i);
}
}
}
//清算结点
while (!st.empty()) {
list<int> tmp = st.top();
st.pop();
int left = st.empty() ? -1 : st.top().back();
for (int k : tmp) {
int left = st.empty() ? -1 : st.top().back();
res[k] = {left, -1};
}
}
return res;
}
int largestRectangleArea(vector<int>& heights) {
map<int, pair<int, int>> map = getRes(heights);
int res = INT32_MIN;
for (int i = 0; i < heights.size(); ++i) {
int left = map[i].first;
int right = map[i].second == -1 ? heights.size(): map[i].second;
res = max(res, (right - left - 1) * heights[i]);
}
return res;
}
};