单调栈

134 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

单调栈

  • 定义:单调栈,栈底到栈顶,按照单调递增或者单调递减的顺序。

  • 应用场景

    面对一个序列,经常可以被用来应用求:

    • 左边距离它最近且比它大/小的数字和右边距离它最近的且比他大/小的数字

    • 当前元素的左边最大/最小的元素。

      比如单调减增栈。对于某个元素,它下面的数就是他左边距离最近的比他大的数字,让它弹出就是它右边距离最近的比大的数字。且在每次弹出时开始计算有关题目要求的

    • 末尾注意 在遍历所有元素结束的时候,如果栈中还有元素,要依次弹出结算。

      比如,存在数组 [3,5,4,7,1],保持栈是单独递减。

       1)首先将3入栈,此时栈是空的,可以直接入栈
       1) 当将5入栈时,53大,此时就可以计算3的右侧距离最近的比他大的元素,这个元素就是5.因为3左侧没有那么元素,那么就是NULL。将3弹出后,再将5入栈
       2) 45小,可以直接入栈,
       47大于4,因此此时要计算4的左右两侧距离最近比他大的元素。左侧是5,右侧是让他弹出的元素7
       1) 同理,也要弹出5,左侧是null,右侧是7
       6)入栈7
       7)入栈1
       8)此时序列已经遍历结束,要弹出1,让1弹出的因为遍历结束,右侧是null.左侧7
       9)让7弹出的是null,左侧也是null
       ​
       顺便也知道了,左右两侧都是null的元素就是最大值。
      

      如果题目有限定条件,比如不会出现INT_MAX,那么也是可以在原数组后面 nums.push_back(INT_MAX) ,这样就不用再单独讨论序列遍及结束时候的问题了。

Leetcode

最大二叉树

   假设一个输入序列:[3, 2, 1, 6, 0, 5]
 ​
   设置一个辅助栈,从大到小存储,保持单调递减。
 ​
   1) 首先入栈3
   223 小,入栈
   312 小,入栈
   46 大于1,因此要弹出1126之间选择二者之间较小的元素作为父节点,因此选择212的右侧,使得1作为2的右子节点
   5)弹出1后,6仍然比2大,同理2要在36之间选择一个作为父节点。36小,因此选择323的右侧,因此2作为3的右子节点
   6)同理弹出3,让3作为6的左子节点
   7)入栈6
   8)入栈0
   9)入栈5的时候比0大,要弹出0,选择5作为父节点,并且05的左孩子
   10)弹出5,左侧是6,作为5的父节点
   116最后弹出,就是根节点
 class Solution {
 public:
     TreeNode* constructMaximumBinaryTree(std::vector<int>& nums) {
      
         std::stack<TreeNode*> nodes;
 ​
         TreeNode* curNode = nullptr;
         for(size_t i=0; i < nums.size(); ++i) { 
             curNode = new TreeNode(nums[i]);
 ​
             while(!nodes.empty() && nodes.top()->val < curNode->val) { 
                 TreeNode* top = nodes.top(); 
                 nodes.pop();
                 
                 if(!nodes.empty() && nodes.top()->val < curNode->val) 
                 {
                     nodes.top()->right = top;
                 }
                 else  
                 {
                     curNode->left = top;  
                 }
             }
 ​
             nodes.push(curNode);
         }
 ​
         // 遍历结束,此时栈中可能还是会有一些元素
         while(!nodes.empty()) { 
             curNode = nodes.top(); 
             nodes.pop();
 ​
             if(!nodes.empty()) 
                 nodes.top()->right = curNode;
             
         }
 ​
         return curNode;
     }
 };

直方图

求的是最大的矩形面积。其实就是求一个矩阵可以向左向左移动的获得的面积最大值 max {L_i*area_i}。最大值受限于一个柱子左右两边距离最近的比他小的柱子。 符合单调栈的应用场景。

示例[2,1,5,6,2,3]

 栈的单调性。单调递增栈,遇到小的就弹出:
     1)空栈,2的下标直接入栈
     1) 1比2小,此时要弹出2,并计算2能移动的面积:(1- (-1)+1) * 2 = 2
     2) 1入栈
     4)5 比 1大,5下标入栈
     1) 6 比5大,6下标直接入栈
     2) 2比6小,此时可以计算6的移动面积:(4-2+1) * 6 = 6
     3) 2还比5小,此时计算5的移动面积:(4-1+1)*5 = 10
     4) 2 比1大,直接入栈
     9)3比2大,直接入栈
 ​
     此时遍历完毕,但是栈中还是有3个元素。
     10)先是弹出3,并计算3的移动面积:3右侧序列的最大长度:(6-4-1)*3 = 3
     11)同理,弹出2,计算面积 (6-3-1)*2 = 4
     12) 同理弹出1,计算面积:(6- (-1) -1)*1 = 6
 ​
     此时最大是10.

因此,有代码实现:

 class Solution {
 public:
     int largestRectangleArea(const std::vector<int>& heights) {
         
         std::stack<int> path;
         int maxArea =0;
         int N = heights.size();
 ​
         for(int i=0; i < N;  ++i) { 
           while(!path.empty() && heights[path.top()] > heights[i]) { 
             int curr = path.top(); path.pop();
             int left = path.empty() ? -1 : path.top();
 ​
             maxArea= std::max(maxArea, (i - left -1) * heights[curr]);
           }
 ​
           path.push(i);
         }
 ​
         while(!path.empty()) { 
           int curr = path.top(); path.pop();
           int left = path.empty() ? -1 : path.top();
 ​
           maxArea= std::max(maxArea, (N - left -1) * heights[curr]);
         }
         return maxArea;
     }
 };

最大子矩阵

这道题目是84的题目的一个升级版,将每一行都看作是一个直方图,直接遍历即可。

 class Solution {
 public:
     int maximalRectangle(std::vector<std::vector<char>>& matrix) {
       if(matrix.empty()) return 0;
 ​
       int cols = matrix[0].size();
       std::vector<int> line(cols);
       int maxArea =0;
       
       for(const auto& base : matrix) { 
           for(int col=0; col < cols; ++col) { 
 ​
             line[col] = base[col] !='0' ? line[col]+1 : 0;
           }
 ​
           maxArea = std::max(maxArea, largestRectangleArea(line)); // 上一题的函数
       }
 ​
       return maxArea;
     }
 };

接雨水

对于输入序列为 [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] ,如下如图所示。这个题目与直方图所求的最大面积不同,求的是 “面积和”。根据木桶理论,水位限制于低的木板,因此维护一个单调递增的栈,当出现当前木板高度小于栈顶的木板高度,那么就可以计算栈顶木板能存储的水面积了

   class Solution {
   public:
     int trap(vector<int>& height) {
       if(height.empty()) return 0;
       
       std::stack<int> path;
       int sum  = 0;
       for(int curr = 0; curr < height.size(); ++curr){
         // 栈顶木板高度 小于 当前木板高度
         while(!path.empty() && height[path.top()] < height[curr]) {
           int h = height[path.top()]; path.pop();   // 栈顶高度
           if(path.empty()) break;
 ​
           int distance = curr - path.top() -1;
           
           int min = std::min(height[path.top()], height[curr]);
           sum += distance * (min - h); // 这里还有个减法,因为要从当前水位开始计算
         } 
 ​
         path.push(curr);
       }
 ​
       return sum;
     }
   };

买卖股票的最佳时机 I

这道题,直观上求的就是对于每个元素其比他大的右边的最大值是多少。单调栈的解法,巧妙的转换了思路:求的是对于当前元素其左边最小的元素。保持栈的单调递减属性,那么每次对于当前元素而言,其左边最小的元素就是栈底。

因此,对于当前数:curr - base

 class Solution {
 public:
     int maxProfit(std::vector<int>& prices) {
         std::stack<int> path; 
 ​
         int base =0;       // 栈底
         int maxProfit = 0; 
         for(int i=0; i < prices.size(); ++i) {   
             // 栈顶的值 > 当前值,那么可以计算最大利润了
             while(!path.empty() && prices[path.top()] > prices[i]) { 
 ​
                 int top = path.top(); path.pop();  // 栈顶
                 // 栈中的元素肯定是保持单调递增的
                 int profit = prices[top] - prices[base];
                 maxProfit = std::max(profit, maxProfit);
             }
             // 当前元素 prices[i] 比栈中任何一个元素都小时,就会使得栈空
             if(path.empty()) { base =i; }
 ​
             path.push(i);
         }
 ​
         while(!path.empty()) { 
             int top = path.top(); 
             path.pop(); 
                 
             int profit = prices[top] - prices[base];
             maxProfit = std::max(profit, maxProfit);
         }
 ​
         return maxProfit;
     }
 };