1. 简介
单调栈是一种特殊的栈,其中的元素按照某种方式有序排列。单调栈主要用于维护给定序列中的最值,该最值可以是最终结果,也可以是中间结果。单调栈主要适用于解决比当前元素大/小的前/后一个元素,如对于数组[1, 3, 2, 5, 7, 4, 9]
,构建一个以当前位置元素结尾的单调递增栈:
有了上面这个单调栈,我们借助一个变量求原序列中的最长递增子序列长度(维护栈的大小),或求比当前元素小的前一个元素(当前元素入栈前的栈顶元素),或求比当前元素小的后一个元素(使栈顶元素出栈的元素)。下面介绍使用几个单调栈解决的经典问题。
2. 每日温度
题目来源 739.每日温度
题目描述 给定一个数组表示气温,即temperatures[i]
表示第i
天的气温。设计算法求想要观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,则使用0
代替。
如输入数组为temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
,则返回结果应为[1, 1, 4, 2, 1, 1, 0, 0]
。由上面的介绍,这道题是求比当前元素大的后一个元素,对应于最大栈(或非递增栈)。其次,由于这里求的是所隔天数,所以我们将元素对应的索引入栈。
vector<int> dailyTemperatures(vector<int>& T) {
// 存放结果
int size = T.size();
vector<int> ans(size, 0);
// 定义栈并先将第一个元素(索引)入栈,避免第一个非空判断
stack<int> stk;
stk.push(0);
// 遍历
for (int i = 1; i < size; ++i) {
// 维护非递增栈
if (T[i] <= T[stk.top()]) {
stk.push(i);
}
else {
// T[i]为使栈顶元素出栈的元素,即比栈顶元素大(不小于)的第一个元素
while (!stk.empty() && T[i] > T[stk.top()]) {
// 当前位置减去栈顶索引,即为所需记录结果
ans[stk.top()] = i - stk.top();
stk.pop();
}
stk.push(i);
}
}
// 返回结果
return ans;
}
其他题解 官方题解
3. 去除重复字母
题目来源 316.去除重复字母
题目描述 给定一个字符串s
,去除字符串中的重复字母,使得每个字母只出现一次,并使返回的结果字典序最小。
如输入是s = "bcabc"
,去除重复字母后得到的结果有abc
、bac
、cab
,由于需要满足返回的结果字典序最小,所以返回abc
。直观上,我们需要返回一个字典序最小的序列,即一个元素为字符的单调递增栈。同时,我们仅能删除重复元素,所以使用哈希表记录每个元素出现的次数,在维护单调栈时,当且仅当栈顶元素出现次数大于零时才将其出栈。另外,由于栈中元素已保持单调性,如果当前元素已经存在于栈中,则跳过该元素,并将其出现次数减一。为了避免额外的存储空间,这里直接将字符串作为单调栈。
string removeDuplicateLetters(string s) {
// 存放结果,直接使用字符串充当单调栈
string ans;
// 由于元素仅含小写字母,使用长度为26的数组代替哈希表
vector<int> vec(26, 0);
for (char ch : s) ++vec[ch - 'a'];
// 记录当前元素是否已经存在于栈中
vector<int> isExist(26, 0);
// 遍历字符串
for (int i = 0; i < s.length(); ++i) {
// 如果当前元素不存在栈中,加入当前元素并维护一个单调栈
if (!isExist[s[i] - 'a']) {
while (!ans.empty() && s[i] < ans.back()) {
// 栈顶元素出现次数大于零时才将其出栈
if (vec[ans.back() - 'a'] > 0) {
// 出栈后更改存在标志并将其出栈
isExist[ans.back() - 'a'] = 0;
ans.pop_back();
}
else {
break;
}
}
// 更改当前元素的存在标志并将其入栈
isExist[s[i] - 'a'] = 1;
ans.push_back(s[i]);
}
// 当前元素出现次数减一
--vec[s[i] - 'a'];
}
// 返回结果
return ans;
}
其他题解 官方题解
4. 移掉K位数字
题目来源 402.移掉K位数字
题目描述 给定一个以字符串表示的非负整数num
,移除这个数中的K
位数字,使得剩下的数字最小。
如输入为num = "1432219"
和K = 3
,由于移除数字4
,3
和2
后得到的数字1219
最小,所以返回结果为"1219"
。首先,这道题使用单调栈解决的思路是当栈内弹出元素少于K
个时,我们需要维护一个单调递增栈。当栈内弹出元素个数等于K
个时,我们只需将栈内元素与还没有遍历的剩余元素拼接即可得到最后的答案。然后,依据题目要求,去掉结果中的前导零。
class Solution {
public:
string removeKdigits(string num, int k) {
int len = num.length();
// 特殊判断
if (len == k) {
return "0";
}
// 存放结果
string ans = "";
// 定义单调递增栈,并压入首元素
stack<char> stk;
stk.push(num[0]);
// 遍历
int i = 1;
for (; i < len; ++i) {
// 维护一个单调非递减栈,并保持出栈元素数不大于K
while (!stk.empty() && num[i] < stk.top() && k) {
stk.pop();
--k;
}
// 当前元素入栈
stk.push(num[i]);
// 如果K等于零,则拼接各部分并返回结果
if (k == 0) {
ans = ans + num.substr(i + 1, len - 1);
// 栈内元素
while (!stk.empty()) {
ans = stk.top() + ans;
stk.pop();
}
break;
}
}
// 如果遍历完原序列后,出栈元素少于K个,则取单调栈的前size - K个元素
if (k != 0) {
while (!stk.empty()) {
ans = stk.top() + ans;
stk.pop();
}
ans = ans.substr(0, ans.size() - k);
}
// 去除前导零
int j = 0, size = ans.size();
while (j < size && ans[j] == '0') {
++j;
}
// 全部为零
if (j == size) {
return "0";
}
else {
ans = ans.substr(j, size - j + 1);
}
return ans;
}
};
其他题解 官方题解
5. 拼接最大数
题目来源 321.拼接最大数
题目描述 给定长度分别为m
和n
的两个数组,其元素由0-9
构成,表示两个自然数各位上的数字。现在从这两个数组中挑出k
(有效)个数字拼接成一个新的数,并且同一数组中取出的数字保持其在原数组的相对顺序。求满足该条件的最大数。
如输入为num1 = [3, 4, 6, 5]
、num2 = [9, 1, 2, 5, 8, 3]
和k = 5
,则返回结果为[9, 8, 6, 5, 3]
。该问题等价于,从num1
中选出长度为x
的序列,从num2
中选出长度为y
的序列,满足x + y = k
,最后再将二者拼接得到最终结果。而只有在两个子序列均为最大时,拼接出的结果才可能最大。 显然,在寻找子序列时,我们可以使用单调栈维护一个递减的序列。最后,不断遍历各种选择可能性,并维护一个最大序列。
在上述过程中,存在拼接两个子序列的操作,首先自定义拼接的merge
函数,但合并时可能存在如下情况:
[1, 2, 3]
[1, 2, 5]
基于合并有序链表的思路,我们使用两个指针分别遍历两个序列。指向较大元素的指针向后移动,但如果出现上述情况,我们需要继续比较后续的内容。即自定义比较函数,判断哪个序列更大:
// 比较函数
int compare(vector<int>& subsequence1, int index1, vector<int>& subsequence2, int index2) {
int size1 = subsequence1.size(), size2 = subsequence2.size();
// 遍历
while (index1 < size1 && index2 < size2) {
// 判断当前位置的元素是否相同
int diff = subsequence1[index1] - subsequence2[index2];
// 如果前者大,则返回正数,否则返回负数,下同
if (diff != 0) {
return diff;
}
++index1;
++index2;
}
// 如果遍历完第一个序列,则返回负数,否则返回正数
return (x - index1) - (y - index2);
}
然后定义合并函数:
// 合并函数
vector<int> merge(vector<int>& subsequence1, vector<int>& subsequence2) {
int size1 = subsequence1.size(), size2 = subsequence2.size();
// 特殊判断
if (size1 == 0) {
return subsequence2;
}
if (size2 == 0) {
return subsequence1;
}
// 存放结果
int size = size1 + size2;
vector<int> merged(size);
// 定义双指针分别遍历两个子序列
int index1 = 0, index2 = 0;
// 填充结果
for (int i = 0; i < size; ++i) {
// 前者大
if (compare(subsequence1, index1, subsequence2, index2) > 0) {
merged[i] = subsequence1[index1++];
}
// 后者大
else {
merged[i] = subsequence2[index2++];
}
}
return merged;
}
接着实现在序列中维护一个长度为k
的单调栈,即从原序列中选择一个最大的子序列:
vector<int> maxSubsequence(vector<int>& nums, int k) {
int size = nums.size();
// 使用数组实现单调栈,其大小为k
vector<int> stk(k, 0);
// 定义指针指向栈顶用以非空判断
int top = -1;
// 剩余可出栈的元素数量,初始为size - k,选择k个即移除size - k个
int remain = size - k;
// 遍历
for (int i = 0; i < size; ++i) {
// 维护单调递增栈,并保持剩余可出栈元素数量大于零
while (top >= 0 && stk[top] < nums[i] && remain > 0) {
--top;
--remain;
}
// 如果单调栈内元素不足k个,则当前元素入栈
if (top < k - 1) {
stk[++top] = nums[i];
}
// 如果不满足上述if条件,即当前元素不入栈,接着遍历
else {
--remain;
}
}
return stk;
}
整体代码:
class Solution {
public:
vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
int size1 = nums1.size(), size2 = nums2.size();
// 存放结果
vector<int> ans(k, 0);
// 定义开始索引和结束索引,保证后续的的i和k - i有效
int start = max(0, k - size2), end = min(k, size1);
// 遍历所有选择可能,得到最终结果
for (int i = start; i <= end; ++i) {
// 得到子序列
vector<int> sub1(maxSubsequence(nums1, i));
vector<int> sub2(maxSubsequence(nums2, k - i));
// 合并子序列
vector<int> curSub(merge(sub1, sub2));
// 如果当前序列大于最大序列
if (compare(curSub, 0, ans, 0) > 0) {
ans.swap(curSub);
}
}
return ans;
}
vector<int> maxSubsequence(vector<int>& nums, int k) {
// ...
}
// 合并函数
vector<int> merge(vector<int>& subsequence1, vector<int>& subsequence2) {
// ...
}
// 比较函数
int compare(vector<int>& subsequence1, int index1, vector<int>& subsequence2, int index2) {
// ...
}
};
其他题解 官方题解
6. 总结
单调栈是一种普通栈的变形,其按某种顺序存放各元素,用于解决原序列中的最长递增子序列长度,比当前元素小的前一个元素,比当前元素小的后一个元素等问题。
参考
- https://leetcode-cn.com/problemset/all/.