单调栈问题特征
单调栈,就是栈的应用,里面的元素的大小按照他们所在栈内的位置,满足一定的单调性。
- 单调栈里的元素具有单调性
- 元素加入栈前,会在栈顶端把破坏栈单调性的元素都删除
- 单调递增:栈可以找到元素向左向右遍历第一个比他小的元素,当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小的元素,新栈顶元素是出栈元素向前找第一个比其小的元素
- 单调递减:栈可以找到元素向左享有遍历第一个比他大的元素
单调栈问题思路
- 常用于一组数组,结果与数组中元素位置及元素大小都有关系,遇到的问题和前后元素之间的大小关系有关系
- 思考思路为:固定数组中的一个元素去想该点所需信息,观察数组中每一个元素的结果与其左右的第一个比它大或小的元素是否有关系,要取左右更大的数则用递减栈,取左右更小的数则用递增栈。
- 另外,关注出栈时的意义,出栈时往往可以计算出该点元素的结果,结合业务去计算目的结果
- 需要单独思考栈空,元素相等,元素处理完后栈内剩余元素的处理,可使用哨兵优化
- 栈内结果常常储存为元素的下标,因为这类问题往往与下标关联,存储下标便于转换为其结果
单调栈问题解法
维护一个栈,遍历数组,如遍历到的元素小于栈顶则入栈,否则栈顶出栈指导满足栈顶小于元素,将元素入栈
实现关键
- 关注特殊情况:
- 都遍历完成后:栈内元素出栈时,栈内元素一定能扩散到栈的末尾
- 出栈后栈内元素为空:当前数组一定扩散到栈的最左边
- 栈中存在高度相等元素:遇到与栈顶相同时,弹出当前栈顶
- 关注出栈时的意义
- 关注栈内存的信息
- 添加前后哨兵:nums[-1]=0, nums[len] =0,可数组放入新数组中,首尾添加两个元素作为哨兵
基础模板
stack<int> s;
for (int i = 0; i < A.size(); i++) {
while (!s.empty() && A[s.top()] > A[i]) {
//出栈时转换为对应业务的计算
s.pop();
}
s.push(i);
}
单调栈系列题目
1、下一个更大元素系列
-
LeetCode 496. 下一个更大元素 I:需要找到其右侧下一个更大元素,采用单调递减栈,如大于栈顶说明栈顶元素找到了其右侧更大元素出栈,栈内存未找到右侧最大元素的数的坐标,利用坐标更便于转换其对应位置关系
-
LeetCode 739. 每日温度:需要找到其右侧下一个更大元素,同上题(496. 下一个更大元素 I)
vector<int> res(T.size(), 0); stack<int> s; // 采用单调递减栈存未处理的元素下标,便于转换为返回结果 for (int i=0; i<T.size(); i++) { while (!s.empty() && T[i] > T[s.top()]) { // 当大于栈顶时说明找到了栈顶元素的下一个更大元素,则出栈 int right = i-1; res[s.top()] = right - s.top() + 1; s.pop(); } s.push(i); }
-
LeetCode 1019. 链表中的下一个更大节点:同第一题,由数组转换为了链表,叠加了链表的处理。因为无法通过下标拿到值了,此时栈中需要存储值及其下标两个信息,当出栈写结果时考虑结果数组大小,进行resize
-
LeetCode 503. 下一个更大元素 II:在第一题(496. 下一个更大元素 I)的基础上增加了循环队列的处理,对于循环队列可以将其转换为
// 对于循环队列的处理 for (int i = 0; i < nums.size() * 2 - 1; i++) { // 边界变为两圈 while (!stk.empty() && nums[stk.top()] < nums[i % nums.size()]) { // 通过i % nums.size()取模来实现下一圈 …… } …… }
-
LeetCode 901. 股票价格跨度:与1、2题一致,区别为要找右侧小于等于当前元素的第一个元素,采用单调递增栈实现,第一个不满足单调递增栈的元素即为比栈顶小的元素。
2、柱形图面积系列
-
LeetCode 84. 柱状图中最大的矩形:思路为用每个i上面的值nums[i]作为高求该元素作为高可围成的面积,再取最大。计算每个元素作为高时,需要找到其左边和右边第一个小于它的元素,采用单调递增栈。当下一个元素小于栈顶时即为栈顶元素右边第一个比其小的值,出栈后它前一个元素即为其左边一个比它小的元素,即可求出底,再求面积即可。
int largestRectangleArea(vector<int>& heights) { int maxRes = 0; stack<int> s; // 栈中存放的是遍历到的下标 // 1、添加前后哨兵,避免特殊处理 vector<int> tmp_heights(heights); tmp_heights.insert(tmp_heights.begin(), -2); // 后面哨兵要比前面的大,不让前面出栈 tmp_heights.push_back(-1); s.push(0); // 插入左侧哨兵 // 2、遍历维护单调栈 for (int i = 1; i < tmp_heights.size(); i++) { // 注意:添加了哨兵,不用单独处理栈全出完及遍历完单独处理栈内元素,同时需注意相等时也入栈 while(tmp_heights[s.top()] > tmp_heights[i]){ int right = i - 1; int mid = s.top(); s.pop(); int left = s.top() + 1; maxRes = max(maxRes, (right - left + 1) * tmp_heights[mid]); //cout<<left<<" "<<tmp_heights[left]<<" "<<right<<" "<<tmp_heights[right]<<" "<<maxRes<<endl; } s.push(i); } return maxRes; }
-
LeetCode 85. 最大矩形:上题(84 柱状图的最大矩形)的变形,需要先预计算将矩阵转换为柱形图(转换方式:挨个遍历行,求每一行为底对应列的和为高的柱形图),然后变为求柱状图的最大矩形。矩阵问题考虑分解到每一行为底来实现。
-
LeetCode 42.接雨水:第一题的变形,所求面积方式不一样,转换为要找每个元素左右两边比它大的数。所以转换为使用单调递减栈,实际面积的计算根据业务要求转换。
3、延展用法
-
LeetCode 962. 最大宽度坡:找A[i]<=A[j]的最大距离,采用贪心的思想,较小数采用尽量靠左边的,较大数采用尽量靠右边的。所以从左往右找找小的数,采用单调递减栈来维护(因为如果后来的数比栈顶大, 那必不如栈顶符合靠左且小),栈顶即为最小的数;从右往左找比栈顶大的元素,当大于栈顶时,不断出栈,看看最左边能到哪,不断重复到栈空,取最大距离即可。本题关键在于元素大小也依然与元素位置相关,考虑单调栈,但需要进行灵活使用
int maxWidthRamp(vector<int>& A) { int left = 0; int maxRes = 0; stack<int> s; for (int i=0; i<A.size(); i++) { // 单调递减栈从左往右存较小数 if (s.empty() || A[i] < A[s.top()]) { s.push(i); } } for (int i = A.size()-1; i>=0; i--) { // 从右往左找比栈顶大的数取距离 while (!s.empty() && A[i] >= A[s.top()]) { maxRes = max(maxRes, i - s.top()); s.pop(); } } return maxRes; }
-
LeetCode 239. 滑动窗口最大值:思考当往右滑动窗口时,可能改变的是左侧元素出去,右侧元素加入,如果左侧元素为最大元素则当前最大值为其第二大的,则维护一个单调递增栈即可,这样栈顶永远为当前最大的元素,左侧出去时比较是否为栈顶如果为则需要出栈,右侧进来时比较是否比栈顶大,如果是则需要入栈。