Leetcode刷题笔记Day15:单调栈

134 阅读5分钟

每日温度

  • 力扣题目链接
  • 这道题目让我们第一次了解到单调栈的用法
  • 所谓单调栈,就是栈底到栈顶(严格)递增(减)的栈
  • 如果是正常的做法,两重循环遍历就会导致超时
// 不好的解法
vector<int> dailyTemperatures(vector<int>& temperatures) {
    int n=temperatures.size();
    vector<int> ans(n, 0);
    for(int i=0; i<n-1; i++)
        for(int j=i+1; j<n; j++)
            if(temperatures[j]>temperatures[i]){
                ans[i]=j-i;
                break;
            }
    return ans;
}
  • 事实上,每一步的信息我们都可以通过这个单调栈解决,按照温度递减的次序入栈,如果接下来要入栈的温度大于栈顶的温度,就可以把栈顶弹出,记录下栈顶元素右边第一个比它高的温度,以此类推
vector<int> dailyTemperatures(vector<int>& T) {
    int n=T.size();
    vector<int> ans(n, 0);
    // 栈底到栈顶递减栈
    stack<int> st; st.push(0); // 存放下标
    // 这里需要走到最后一步把栈里能更新的更新
    for(int i=1; i<n; i++)
        if(T[i]<=T[st.top()]) st.push(i);
        else{
            while(!st.empty() && T[i]>T[st.top()]){
                ans[st.top()]=i-st.top();
                st.pop();
            }
            st.push(i);
        }
    return ans;
}

下一个更大元素Ⅰ

  • 力扣题目链接
  • 稍微变了一下,在全集中找子集中的数的下一个更大元素
  • 其实也不难,我们只需要记录下nums1元素和索引的映射关系就好
  • 不难想到利用unordered_map,在nums2中利用单调栈就好
vector<int> nextGreaterElement(vector<int>& nums1,
                               vector<int>& nums2) {
    stack<int> st;
    vector<int> result(nums1.size(), -1);
    if(nums1.size()==0) return result;
    unordered_map<int, int> umap; // key:下标元素,value:下标
    for(int i=0; i<nums1.size(); i++) umap[nums1[i]]=i;
    st.push(0); //这里0是num2的下标
    // 在nums2构造递减栈
    for(int i=1; i<nums2.size(); i++){
        if(nums2[i]<=nums2[st.top()]) st.push(i);
        else{
            while(!st.empty() && nums2[i]>nums2[st.top()]){
                // nums1有top元素
                if(umap.count(nums2[st.top()]))
                    // 更新result
                    result[umap[nums2[st.top()]]]=nums2[i];
                st.pop();
            }
            st.push(i);
        }
    }
    return result;
}

下一个更大元素Ⅱ

  • 力扣题目链接
  • 这道题是在一个数组中循环地找下一个更大的数
  • 很直接的做法就是把两个数组拼在一起,但其实我们可以通过模运算“假装”把它们拼到一起
vector<int> nextGreaterElements(vector<int>& nums) {
    vector<int> result(nums.size(), -1);
    stack<int> st; st.push(0);
    // 这里循环2倍
    for(int i=1; i<nums.size()*2; i++)
        if(nums[i%nums.size()]<=nums[st.top()]) st.push(i%nums.size());
    else{
        while(!st.empty() && nums[i%nums.size()]>nums[st.top()]){
            result[st.top()]=nums[i%nums.size()];
            st.pop();
        }
        st.push(i%nums.size());
    }
    return result;
}

接雨水

  • 力扣题目链接
  • 又是两道hard的题目,怕了吗?
  • 如何计算凹槽的体积,有两种思路,一种是按列来算,一种按行来算
  • 按列来算就得找到当前凹槽左右两边柱子的最大高度
int trap(vector<int>& height) {
    if(height.size()<=2) return 0;
    int n=height.size();
    vector<int> maxLeft(n, 0);
    vector<int> maxRight(n, 0);
    // 记录每个柱子左边柱子最大高度
    maxLeft[0] = height[0];
    for(int i=1; i<n; i++)
        maxLeft[i]=max(maxLeft[i-1], height[i]);
    // 记录每个柱子右边柱子最大高度
    maxRight[n-1] = height[n-1];
    for(int i=n-2; i>=0; i--)
        maxRight[i]=max(maxRight[i+1], height[i]);
    // 求和
    int sum=0;
    for(int i=1; i<n-1; i++){
        // 木桶效应:取决于短板
        int count=min(maxLeft[i], maxRight[i])-height[i];
        if(count>0) sum+=count;
    }
    return sum;
}
  • 按行来算就是单调栈的思路了,找到凹槽两边第一个比它高的元素
  • 构造递减栈是合适的(方向:栈底->栈顶)
int trap(vector<int>& height) {
    if(height.size()<=2) return 0;
    int sum=0; stack<int> st; st.push(0);
    for(int i=1; i<height.size(); i++)
        if(height[i]<height[st.top()]) st.push(i);
        else if(height[i]==height[st.top()]){
            // 因为遇到相同高度的柱子,需要使用最右边的柱子来计算宽度
            st.pop();   // 保证严格递减栈
            st.push(i);
        }
        else{
            while(!st.empty() && height[i]>height[st.top()]){
                int mid=st.top(); st.pop();
                // 这时候还得判断一下
                if(!st.empty()){
                    int h=min(height[st.top()], height[i])
                        -height[mid];
                    int w=i-st.top()-1;
                    sum+=h*w;
                }
            }
            st.push(i); // 为避免忘记,写完while马上写
        }
    return sum;
}

柱状图中最大的矩形

  • 力扣题目链接
  • 这个题目恰好和接雨水反过来,它需要构造的是递增栈
  • 本题要记录记录每个柱子左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度,同理,右边也是
int largestRectangleArea(vector<int>& heights) {
    int n=heights.size();
    vector<int> minLeftIdx(n);
    vector<int> minRightIdx(n);
    // 记录每个柱子 左边第一个小于该柱子的下标
    minLeftIdx[0]=-1;
    for(int i=1; i<n; i++){
        int t=i-1;
        // 这里不是用if,而是不断向左寻找的过程
        // 有kmp那味了
        while(t>=0 && heights[t]>=heights[i]) t=minLeftIdx[t];
        minLeftIdx[i]=t;
    }
    // 记录每个柱子 右边第一个小于该柱子的下标
    minRightIdx[n-1]=n;
    for(int i=n-2; i>=0; i--){
        int t=i+1;
        while(t<n && heights[t]>=heights[i]) t=minRightIdx[t];
        minRightIdx[i]=t;
    }
    // 求和
    int sum=0;
    for(int i=0; i<n; i++){
        int area=heights[i]*(minRightIdx[i]-minLeftIdx[i]-1);
        sum=max(sum, area);
    }
    return sum;
}
  • 单调栈是怎么处理的呢?
  • 只有栈里从小到大的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子
  • 本质上说,就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
  • 这个确实是要难理解不少,掌握上面双指针优化的方法也行!
int largestRectangleArea(vector<int>& heights) {
    stack<int> st; st.push(0);
    int maxArea=0;
    // 首尾加入0获得计算结果
    heights.insert(heights.begin(), 0);
    heights.push_back(0);
    
    for(int i=1; i<heights.size(); i++)
        // 构造递增栈
        if(heights[i]>heights[st.top()]) st.push(i);
        else if(heights[i]==heights[st.top()]){
            st.pop(); st.push(i);
        }else{
            // 这里妙就妙在首尾都是0了
            // 找最大面积过程中不断的削平山峰,多个山头最后变成一个山头
            while(!st.empty() && heights[i]<heights[st.top()]){
                int mid=st.top();
                st.pop();
                if(!st.empty()){
                    int left=st.top(), right=i;
                    maxArea=max(maxArea, 
                                (right-left-1)*heights[mid]);
                }
            }
            st.push(i);
        }
    return maxArea;
}

参考资料

[1] 代码随想录

[2] Leetcode题解