目录
一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时考虑用单调栈。时间复杂度为O(n)。
单调栈里存放的元素:元素的下标i,如果需要使用对应的元素,直接T[i]就可以获取。
单调栈里元素顺序:从栈头到栈底递增/递减。
使用单调栈主要有三个判断条件:
- 当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
- 当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
- 当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
739. 每日温度
vector<int> dailyTemperatures(vector<int>& temperatures) {
// 结果序列初始化为0,若未更新说明右侧没有比其更大的元素
vector<int> res(temperatures.size(), 0);
// 构建单调栈:从栈头到栈尾递增,装入元素下标
stack<int> st;
st.push(0);
for (int i = 1; i < temperatures.size(); i++) {
// 出现新元素大于栈顶元素,记录结果并弹出栈顶元素,保持栈的单调
// 注意栈不能为空,若为空新元素直接入栈
while (!st.empty() && (temperatures[i] > temperatures[st.top()])) {
// res记录右侧出现第一个更大元素的位置
res[st.top()] = i - st.top();
st.pop();
}
// 新元素小于等于栈顶元素时直接入栈
st.push(i);
}
return res;
}
本题构建了一个从栈头到栈尾递增的单调栈,注意栈内元素是数组元素的下标。新入栈元素对应温度一定比栈头元素对应温度小,否则更新结果(下标相减即为右侧第一次出现更大元素的位置),并弹出栈头,入栈更大的元素,从而保证栈的单调性。
496. 下一个更大元素 I
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
vector<int> res(nums1.size(), -1);
// 存放nums1中元素便于查找,需要得到元素的下标,因此用unordered_map【元素/下标】
unordered_map<int, int> index;
for (int i = 0; i < nums1.size(); i++) {
index.insert(make_pair(nums1[i], i));
}
// 创建单调栈:从栈头到栈尾递增
stack<int> st;
for (int i = 0; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (index.find(nums2[st.top()]) != index.end()) {
// index[nums2[st.top()]]:目标元素在nums1中的下标
res[index[nums2[st.top()]]] = nums2[i];
}
st.pop();
}
st.push(i);
}
return res;
}
本题和上一题基本类似,定义了一个unordered_map便于确认nums2中的元素是否在nums1中存在,同时获取其在nums1中下标,以便于更新结果。
503. 下一个更大元素 II
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> res(nums.size(), -1);
// 创建单调栈:从栈头到栈尾递增
stack<int> st;
// 循环数组:模拟执行两遍找右侧第一个出现的较大值的操作
for (int i = 0; i < nums.size() * 2; i++) {
while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
res[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
return res;
}
本题在nums数组后再追加一个nums数组,就可以利用前述单调栈的方法完成循环数组的求解。这里利用了i % nums.size()作为下标索引,来模拟执行两遍的操作。
42. 接雨水[*]
- 动态规划法:
int trap(vector<int>& height) {
// 动态规划法:利用状态转移求解每个位置左右两侧的最大高度
int res = 0;
if (height.size() <= 2) return res;
vector<int> maxLeftDP(height.size(), 0);
vector<int> maxRightDP(height.size(), 0);
// 求解右侧(含本柱子)最大高度:后一个位置的右侧最大高度和本高度的最大值
// 这样设置便于求解,若本柱子最高,本柱子积水结果直接为0
maxRightDP[height.size() - 1] = height[height.size() - 1];
for (int i = height.size() - 2; i >= 0; i--) {
maxRightDP[i] = max(height[i], maxRightDP[i + 1]);
}
// 求解左侧(含本柱子)最大高度:前一个位置的左侧最大高度和本高度的最大值
maxLeftDP[0] = height[0];
for (int i = 1; i < height.size(); i++) {
maxLeftDP[i] = max(height[i], maxLeftDP[i - 1]);
}
for (int i = 0; i < height.size(); i++) {
res += min(maxLeftDP[i], maxRightDP[i]) - height[i];
}
return res;
}
本题可以转化为求某个柱子左右两侧(含本柱子)的最大柱子高度,两者的最小值与本柱子高度之差,就是本柱子上能积累的雨水,如下图所示(来源:代码随想录):
- 单调栈:
int trap(vector<int>& height) {
// 单调栈法:求取横向雨水的面积
int res = 0;
if (height.size() <= 2) return res;
// 构造单调栈:栈底到栈顶元素递减,记录柱子下标
stack<int> st;
for (int i = 0; i < height.size(); i++) {
// 用新元素-栈顶元素-栈顶元素的后一个元素这三者来盛水
while (!st.empty() && height[i] > height[st.top()]) {
// 记录最低处位置
int bottle = st.top();
st.pop();
// 栈顶元素的后一个元素存在,出现低洼
if (!st.empty()) {
// 雨水横向宽度
int w = i - st.top() - 1;
// 雨水纵向高度
int h = min(height[st.top()], height[i]) - height[bottle];
res += w * h;
}
}
// 新元素等于栈顶元素时,出栈栈顶元素而入栈新元素,因为利用低洼处右边界计算雨水宽度
if (!st.empty() && height[i] == height[st.top()]) {
st.pop();
}
st.push(i);
}
return res;
}
要把该问题转化为利用单调栈求解右侧出现的第一个更大元素的问题,必须转换一下思路,求低洼处积水的横向面积,如下图所示(来源:代码随想录):
具体思路并不难,但要注意几处细节的处理:
- 出现较大元素时,先记录栈顶元素(即最低位置),然后弹出栈顶元素,判断栈是否为空,即左侧是否有更大的元素以组成低洼;
- 单调栈记录了柱子位置的下标,由于计算的是积水横向面积,就算上一个低洼处已被弹出(如图中第二个水坑处),也不影响下一次计算的结果;
- 遇到相同的元素,要弹出旧的栈顶,保留更右侧的元素,因为积水的宽度是通过栈顶元素下一个元素(若有多个重复元素)的最右侧元素决定的。
84. 柱状图中最大的矩形[*]
本题首先要明白如何构建可勾勒的最大矩形,拥有一个自己的构建方式。对每一个柱子,期望其高度可以被充分利用,从而求这个柱子左右两侧首次出现的比其高度更低的柱子的位置为right和left,从而每个位置下可以获得的最大面积为:
sum = heights[i] * (right[i] - left[i] - 1)
- 动态规划法:
int largestRectangleArea(vector<int>& heights) {
// 动态规划法:求出每个位置(含当前位置)左右两侧出现更小元素的下标
vector<int> minRightDP(heights.size());
vector<int> minLeftDP(heights.size());
// 左边第一个小于当前元素的下标
minLeftDP[0] = -1;
for (int i = 1; i < heights.size(); i++) {
int t = i - 1;
while (t >= 0 && heights[t] >= heights[i]) {
t = minLeftDP[t];
}
minLeftDP[i] = t;
}
// 右边第一个小于当前元素的下标
minRightDP[heights.size() - 1] = heights.size();
for (int i = heights.size() - 2; i >= 0; i--) {
int t = i + 1;
while (t < heights.size() && heights[t] >= heights[i]) {
t = minRightDP[t];
}
minRightDP[i] = t;
}
int res = 0;
for (int i = 0; i < heights.size(); i++) {
int sum = heights[i] * (minRightDP[i] - minLeftDP[i] - 1);
res = max(sum, res);
}
return res;
}
利用状态转移找到左右两侧第一个小于当前元素的下标位置,注意状态转移公式的迭代方向和初始化(是为了避免死循环和符合求解的物理意义)。
- 单调栈:
// 单调栈:从栈尾到栈头递增,记录元素下标
stack<int> st;
// 数组头部和尾部插入0,以保证单元素/递增序列有结果
heights.insert(heights.begin(), 0);
heights.push_back(0);
int res = 0;
for (int i = 0; i < heights.size(); i++) {
while (!st.empty() && heights[i] < heights[st.top()]) {
int top = st.top();
st.pop();
int w = i - st.top() - 1;
int h = heights[top];
res = max(res, w * h);
}
st.push(i);
}
return res;
本题采用单调栈法与上一题很类似,“接雨水”是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。栈顶和栈顶的下一个元素以及要入栈的三个元素组成了要求的最大面积的高度和宽度。
为了能够处理单个柱子/单调递增序列的结果,在数组前后都添加了0。
316. 去除重复字母[*]
string removeDuplicateLetters(string s) {
// 利用单调栈保证字母按字典序递增(若栈头元素只剩下一个,不再pop)
stack<char> st;
// 判断元素是否只剩下最后一个
int count[26] = { 0 };
for (char str : s) {
count[str - 'a']++;
}
// 满足字典条件时有可能元素已经重复,因此需要额外一个map记录是否存在
unordered_map<char, bool> in_stack;
for (char str : s) {
count[str - 'a']--;
// 出现重复有三种情况:cc/cac/cdc
// 第一种情况可以跳过;第二种情况前一个c会被pop,第三种情况必须留下第一个c
if (in_stack[str]) continue;
while (!st.empty() && st.top() > str) {
if (count[st.top() - 'a'] == 0) break;
in_stack[st.top()] = false;
st.pop();
}
st.push(str);
in_stack[str] = true;
}
// 获取结果字符串
string res;
while (!st.empty()) {
res.push_back(st.top());
st.pop();
}
reverse(res.begin(), res.end());
return res;
}
单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符 ,且栈顶字符的剩余数量并不为0,栈顶元素就可以舍弃。
另外,出现重复有三种情况:cc/cac/cdc 。第一种情况可以跳过;第二种情况前一个c会被pop,第三种情况必须留下第一个c。为了处理第三种情况,需要一个额外的hashtable记录元素是否已经在单调栈中存在了。
单调栈进阶
1124. 表现良好的最长时间段
// 记工作小时数大于 8 的为 1 分,否则为 −1 分
// 原问题可以看做求解区间分数和大于 0 的最长区间长度
// 即求解最长的一段区间 [l,r] 使得 s[r]−s[l] > 0
int longestWPI(vector<int>& hours) {
int n = hours.size();
// 分数前缀和 s
vector<int> s(n + 1);
stack<int> stk;
// 栈中元素为 s[0]∼s[r−1] 的递减项
stk.push(0);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + (hours[i - 1] > 8 ? 1 : -1);
if (s[stk.top()] > s[i]) {
stk.push(i);
}
}
int res = 0;
for (int r = n; r >= 1; r--) {
while (stk.size() && s[stk.top()] < s[r]) {
res = max(res, r - stk.top());
stk.pop();
}
}
return res;
}