单调栈

    技术2025-03-14  24

    定义

    栈内元素按照递增或者递减的顺序排列的栈

    适用问题

    单调栈分为单调递增与单调递减栈,可以用于获取下一个比当前元素大(小)的元素。当需要通过比较前后元素的大小关系时,可以使用单调栈解决问题。

    单调递减栈

    在队列中针对每个元素从右边寻找第一个比它大的元素在队列中针对每个元素从左边寻找第一个比它大的元素(从后往前)

    这两种解决问题过程一样,但是角度不同。 使用下面这个例子来解释: 有一本武功秘籍,根据先来后到的顺序教授人武功。但是在排队过程中,如果有武功更加高强的人发现前面的人武功不如自己,就会打发他离开,并把自己的武功传授给他(这一过程就相当于为前面的人找到右侧第一个大于它的元素)。然后如果发现前面不如自己武功的人都被打发走了,此时站在面前的是武功高于自己的人(相当于找到为自己找到左侧第一个大于自己的元素), 就停下脚步。

    伪代码如下:

    对于第i个到来的人: 每当栈中有人并且打不过自己的时候: 让这个人离开,并且将这个人右侧的第一个能打过他的记录为第i个人 // 也可以记录栈顶的人为离开的人的左侧第一个不能打过的人 记录第i个人左侧第一个不能打过的人为栈顶的人 自己入队

    下面看几道使用单调栈解决的问题: 901 股票价格跨度

    编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。

    今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。

    例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。

    问题分析: 这道题目需要求解的是小于或等于今天价格的最大连续日数,其实就是求解左侧大于今天价格的那一天。符合单调递减栈的特性。

    代码:

    class StockSpanner { public: int index; stack<pair<int,int>> s; StockSpanner() { index = 0; } int next(int price) { index++; while(!s.empty() && s.top().second<=price) { s.pop(); } int ans; if(s.empty()) // 当栈为空的时候表示前面的元素都小于当前元素,故ans = index ans = index; else { ans = index-s.top().first; } s.push({index,price}); return ans; } };

    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}) 。

    注意:在下面的示例中,诸如 [2,1,5] 这样的输入(不是输出)是链表的序列化表示,其头节点的值为 2,第二个节点值为 1,第三个节点值为 5 。

    问题分析: 本质上也是求右侧第一个大于当前元素的元素。 代码:

    vector<int> nextLargerNodes(ListNode* head) { stack<pair<int,int>> s; vector<int> res; int index = 0; while(head) { res.push_back(0);// 这里是为了保证链表每个元素都有对应结果 方便之后按照下标进行修改 而且最后栈中剩下的元素是按照从大到小排序的,无右侧更大值 res = 0 while(!s.empty() && s.top().second < head->val) { res[s.top().first] = head->val; s.pop(); } s.push({index, head->val}); index++; head = head->next; } return res; }

    单调递增栈:

    在一个队列中针对每个元素寻找右侧第一个比它小的元素在一个队列中针对每个元素寻找左侧第一个比它小的元素

    896 单调数列

    题目描述: 如果数组是单调递增或单调递减的,那么它是单调的。

    如果对于所有 i <= j,A[i] <= A[j],那么数组 A 是单调递增的。 如果对于所有 i <= j,A[i]> = A[j],那么数组 A 是单调递减的。

    当给定的数组 A 是单调数组时返回 true,否则返回 false。

    题目分析: 这道题目有比较简单的思路,遍历两次判断是否满足递增或者递减即可。 使用单调栈的话,也很简单,只需要判断最后单调栈的大小是否等于数组大小。如果有一个栈满足size相等,那么就证明是单调的。

    代码:

    class Solution { public: bool isMonotonic(vector<int>& A) { stack<int> more; stack<int> less; for(int i=0;i<A.size();i++) { while(!more.empty() && more.top() < A[i]) { more.pop(); } more.push(A[i]); while(!less.empty() && less.top() > A[i]) { less.pop(); } less.push(A[i]); } return more.size()==A.size() || less.size()== A.size(); } };

    84. 柱状图中最大的矩形

    题目描述 题目分析: 对于每个柱形来说,找到左边第一个低于它的高度,找到右边第一个低于它的高度,那么就能确定这个柱形所确定的矩形的最大面积。 可以看出这是典型的单调递增栈的问题。

    代码: 第一次写的代码很low,元素出栈时确定右边第一个小于它的元素,入栈时确定左边第一个大于它的元素。 但是这样就会出现一个问题:怎么存储入栈时所确定的元素呢?在这个实现中入栈与出栈不在同时发生。而且在遍历完所有元素之后,还需要对单调栈内的元素进行一次处理。

    int largestRectangleArea(vector<int>& heights) { stack<int> s;// 存放下标 int maxS = 0; int l = heights.size(); if(l==0) return 0; int record[l];// 记录每个元素左边第一个小于它的下标。 并初始化为0 for(int i=0;i<heights.size();i++) { while(!s.empty() && heights[s.top()] > heights[i]) { int index = s.top(); // 出栈确定右边第一个小于 由于先入栈 此时数组中已经记录左边第一个小于 maxS = max(maxS,heights[index] *(i-record[index]-1)); s.pop(); } // 入栈确定左边第一个小于 if(!s.empty()) record[i] = s.top(); else record[i] = -1; s.push(i); } //栈里面还有一个递增序列 对于这些元素 右边第一个小于它的是数组末尾 while(!s.empty()) { maxS = max(maxS, heights[s.top()] * (l-record[s.top()]-1)); s.pop(); } return maxS; }

    优化版本代码:最精彩的地方在于在数组首尾的位置添加了两个0元素。

    //使用单调递增栈 但是前面以及最后添加一个0元素 这种算法实现更加简单 int largestRectangleArea(vector<int>& heights) { stack<int> s; int maxS = 0; heights.insert(heights.begin(),0);// 这里为了防止s.top()由于空栈出错,前面需要栈底添加0元素 heights.push_back(0);// 为了保证最后栈内所有元素都能出栈 避免了后续对一个递增序列的处理 for(int i=0;i<heights.size();i++) { while(!s.empty() && heights[s.top()] > heights[i]) { int temp = s.top(); s.pop(); int value = heights[temp] * (i-s.top()-1); // 这种实现方法有个好处就是可以同时获取到出栈元素的左边第一个以及右边第一个小于它的元素 maxS = max(maxS, value); } s.push(i); } return maxS; }

    42.接雨水 题目描述: 常见的两种思路: 第一种: 按照每一列来进行求解 发现每一列接的雨水由左右两端最大高度决定的。 可以有两种解法。

    解法一:使用两个数组分别保存每个元素左右最大高度

    代码:

    int trap(vector<int>& height) { int l = height.size(); if(l<=1) return 0; int left[l]; int right[l]; left[0] = right[l-1] = 0; for(int i=1;i<l;i++) { left[i] = max(left[i-1], height[i-1]); } for(int i=l-2;i>=0;i--) { right[i] = max(right[i+1], height[i+1]); } int res = 0; for(int i=0;i<l;i++) { int temp = min(left[i], right[i]); if(temp>height[i]) res+=(temp-height[i]); } return res; }

    解法二:使用两个变量保存左右最大高度 然后每次对其中的较小者进行判断 如果当前元素大于较小者,更新较小者对应的最大高度,否则将结果+当期元素接的雨水高度

    对于一个元素而言,其雨水高度是由左右最大高度的较小者决定的,因此我们只需考虑哪个最大高度比较小,不用管另外一个高度。

    代码:

    int trap(vector<int>& height) { int l = height.size(); if(l<=1) return 0; // i左边的最大 j右边的最大 int leftmost = 0,rightmost = 0; int i = 0, j = l-1; int res = 0; while(i<=j) { if(leftmost < rightmost){ leftmost<=height[i]? leftmost = height[i++]: res +=(leftmost-height[i++]); } else { rightmost<=height[j]? rightmost = height[j--]: res +=(rightmost-height[j--]); } } return res; }

    思路二:使用单调递减栈,每次求解元素的左右两端第一个大于它的高度,如果两个高度较小者大于当前高度,那么更新结果,否则跳过

    这种算法关注的是左右第一个大于当前元素的高度所确定的面积 考虑的不再是单个元素接的雨水高度 代码:

    int trap(vector<int>& height) { stack<int> s;// 存储下标 int l = height.size(); if(l<=1) return 0; int res = 0; for(int i=0;i<l;i++) { while(!s.empty() && height[i] > height[s.top()]) { int temp = s.top(); s.pop(); // 为空表示左边没有比heights[temp]更大的元素 if(s.empty()) res+=0; else{ // 加上左右第一个大于其的高度所确定的面积 考虑的不再是单个元素接的雨水高度 res +=(min(height[s.top()], height[i])-height[temp]) *(i-s.top()-1); } } s.push(i); } return res; }

    总结一下

    单调栈适合那些求解左右第一个大于或者小于元素的问题, 对于左第一个的求解,可以与右一个一样都放在while(!empty() &&)里面,不过要注意为空的情况。也可以放在while循环外面,在每个循环入栈的时候,确定其左边第一个。

    Processed: 0.014, SQL: 9