代码随想录算法训练营day44

12 阅读12分钟

739.每日温度 单调栈的设计思路:通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。

理解结果数据不一定要一定按照顺序录入之后,很多问题就迎刃而解了

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        //使用单调栈在O(n)时间复杂度内解决问题
        //设计单调栈
        stack<int> st;
        //设计存储结果的返回数组
        vector<int> result(temperatures.size(),0);
        //初始化,先将数组下标0入栈
        st.push(0);
        //执行迭代递推逻辑
        for(int i=1;i<temperatures.size();i++){
            if(temperatures[i]<temperatures[st.top()]){//不满足条件,遍历温度比栈顶温度低
                //执行入栈
                st.push(i);
            }
            else if(temperatures[i]==temperatures[st.top()]){//不满足条件,遍历温度和栈顶温度相等
                //执行入栈
                st.push(i);
            }
            else {//满足条件开始录入结果
                while(!st.empty() && temperatures[i] > temperatures[st.top()]){//注意这里要防止空栈问题
                    //录入结果
                    result[st.top()]=i-st.top();
                    st.pop();
                }
               //将遍历的新结果入栈
                st.push(i);
            }
        }
        return result;
        
    }
};

496.下一个更大元素 难点解析:这道题远比上一道单调栈绕,需要综合运用hash表,单调栈等知识点 1.使用hash表快速查找nums1的数据内容和下标, 2.明确单调栈的入栈对象是nums2的下标 3.result初始化的对象是nums1的结果,应当全部初始化为-1; 4.明确栈顶元素比较的对象!!! 5.正确设计匹配情况应该怎么存入结果,这部分类似上题。

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        //使用单调栈解答问题
        //首先设计单调栈
        stack<int> st;
        //使用result存储结果
        vector<int> result(nums1.size(),-1);
        //剪支排除意外情况
        if(nums1.size()>nums2.size())
        return result;
        //对nums1进行预处理,方便快速查找下标或结果
        unordered_map<int,int> countmap;
        for(int i=0;i<nums1.size();i++){
            countmap[nums1[i]]=i;
        }
        //初始化单调栈,将下标0入栈
        st.push(0);
        //开始执行迭代关系,递推方程
        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()]){//注意防止空栈情况
                  if(countmap.count(nums2[st.top()])>0){//确定是否存在这个元素在hash表中
                      int index=countmap[nums2[st.top()]];
                      result[index]=nums2[i];//把满足条件的元素存入结果中
                  }
                  st.pop();
               }
               st.push(i);
            }
        }
        return result;
    }
};

503.下一个更大元素 变成循环数组之后,要学会使用取余来进行运算

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        //使用单调栈
        vector<int> result(nums.size(),-1);//在同等规模情况下全部初始化为-1;
        //剪支处理
        if(nums.size()==0) return result;
        //设计单调栈
        stack<int> st;
        //初始化
        st.push(0);
        //考虑到为循环数组,可以使用衔接,也可以使用除余达成双倍循环目的
        for(int i=1;i<2*nums.size();i++){//注意,这里是从1开始遍历,因为0已经入栈了
            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; 
    }
};

42.接雨水问题 1.采用双指针方式是从列的方式进行计算 2.采用单调栈的方式是从行的方式进行计算,如何设计核心单调出栈逻辑还是需要画图来处理

class Solution {
public:
    int trap(vector<int>& height) {
        //类似求下一个较大元素之差
        //采用单调栈的做法
        //使用result存储所有结果
        int result=0;
        //设计单调栈
        stack<int> st;
        //入栈初始化
        st.push(0);
        //执行单调迭代,并且需要注意,必须满足height[i]<height[j]且雨水量为j-i-1;设置mid,通过height[i],st.top()三个元素来计算雨水体积
        for(int i=1;i<height.size();i++){//注意边界从1开始
            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()]){
                    //设置mid作为中间凹点
                    int mid=st.top();
                    st.pop();
                    // 关键修正2:弹出mid后先判断栈是否为空(避免空栈访问)
                    if (st.empty()) break;
                    int left=st.top();
                    int right=i;
                    //计算雨水体积
                    int h=min(height[left],height[right])-height[mid];//求高
                    int l=right-left-1;//求宽
                    int m=h*l;
                    result=m+result;
                }
                st.push(i);//存储下标
            }
        }
        return result;
    }
};

84柱状图最大面积 弄明白矩形怎么算,不仅要考虑入栈的满足条件的情况,还要考虑不满足条件时,也就是所有元素都入栈却没有相应条件的情况!!! 这些怎么算具体看代码!!!

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //类似雨水问题,雨水问题是求当前柱左右两次大于其的第一个的柱子高度,这里最大矩形是求当前柱左右两侧第一个小于其的柱子高度
        //剪支排除意外情况
        if(heights.size()==0) return 0;
        //设计单调栈,栈内存储的是heights的下标
        stack<int> st;
        //初始化st,将下标0入栈
        st.push(0);
        int result=0;//初始化result=0;
        //遍历heights并分情况进行对比
        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{
                while(!st.empty()&&heights[i]<heights[st.top()]){
                    int mid=st.top();
                    st.pop();
                // 关键修正:栈空时宽度为i,而非直接break
                    int h = heights[mid];
                    int w = 0;
                    if (st.empty()) {
                        w = i; // 左边界不存在,宽度为当前i(左侧所有位置都可作为矩形宽度)
                    } else {
                        int left = st.top();
                        w = i - left - 1; // 左边界存在,宽度=右-左-1
                    }
                    result = max(h * w, result);
                }
                st.push(i);
            }
            
        }
        // 关键修正2:处理栈中剩余元素(右侧无更小值,右边界为heights.size())
        while (!st.empty()) {
            int mid = st.top();
            st.pop();
            int h = heights[mid];
            // 右边界为数组长度,左边界逻辑同上
            int w = st.empty() ? heights.size() : (heights.size() - st.top() - 1);
            result = max(h * w, result);
        }
        return result;
    }
};

柱状图中最大的矩形,它的高度一定等于某一根具体柱子的高度

为什么?你可以想象一下:如果有一个矩形的高度,比它覆盖范围内的所有柱子都高,那它肯定 “悬空” 了,不成立;如果它的高度比覆盖范围内的某些柱子矮,那我们完全可以把高度再 “拔高” 一点,直到碰到某根柱子的顶部 —— 这时候的面积才是最大的。

所以,我们的算法本质是:遍历每一根柱子,强制以「这根柱子的高度」作为矩形的高,找到此时能达到的最大宽度,计算面积,最后在所有面积里取最大值。


二、mid 是什么?它就是「当前被选中作为高的那根柱子」

在单调栈的逻辑里,我们弹出栈顶元素 mid 的那一刻,就是在专门计算「以 mid 这根柱子为高」的最大矩形面积

我给你画个具体的场景,结合代码里的经典例子 heights = [2, 1, 5, 6, 2, 3] 来说:

场景还原(关键步骤):

假设我们遍历到了下标 i = 4(对应高度 2),此时栈里存的下标是 [1, 2, 3](对应高度 [1, 5, 6])。

现在看代码逻辑:

  1. 触发弹出条件heights[i] = 2 < heights[st.top()] = 6(栈顶是下标 3),所以我们要弹出 mid = 3

  2. 确定左右边界

    • 弹出 mid 后,新的栈顶是 2(对应高度 5)—— 这就是 mid左边界(左边第一个比 6 矮的柱子);
    • 当前的 i = 4(对应高度 2)—— 这就是 mid右边界(右边第一个比 6 矮的柱子)。
  3. 计算面积

    • 此时,我们要算的是「以 mid = 3 这根柱子(高度 6)为高」的矩形;
    • 因为左右边界都比它矮,所以这个矩形的高度最高只能到 6,否则就会被左右边界 “挡住”;
    • 所以必须用 heights[mid] = 6 作为高,宽度是 i - left - 1 = 4 - 2 - 1 = 1,面积是 6 * 1 = 6

三、如果不用 heights[mid],会发生什么?

我给你反证一下,假设你刚才不用 heights[mid],而是用了别的高度:

  1. 如果你用左边界的高度 5:那你算的就是「以 5 为高」的矩形,这应该是在弹出 mid = 2 的时候才算的,不是现在;
  2. 如果你用右边界的高度 2:那高度就太小了,完全浪费了 mid = 3 这根 6 的高度,算出来的面积肯定不是最大的;
  3. 如果你随便选一个中间高度:比如 4,那这个矩形既没有充分利用柱子的高度,逻辑上也不成立(因为没有对应的柱子支撑这个高度)。

所以,只有用 heights[mid],才是精准地在计算「当前这根柱子能贡献的最大面积」


四、再看后续的弹出,巩固理解

刚才弹出 mid = 3 后,栈顶变成了 2(高度 5),此时 heights[i] = 2 还是小于 5,所以会继续弹出:

  1. 弹出 mid = 2(高度 5);
  2. 左边界是新的栈顶 1(高度 1),右边界还是 i = 4
  3. 此时计算的是「以 mid = 2 这根柱子(高度 5)为高」的矩形;
  4. 宽度是 4 - 1 - 1 = 2,面积是 5 * 2 = 10(这比刚才的 6 更大,会更新 result)。

你看,每弹出一个 mid,就是在专门为这根柱子 “量身定制” 计算面积,所以高度必须是它自己的

铁律一:最大矩形的高度,一定等于某一根柱子的高度

这是整个算法的根本前提,我再帮你强化一遍(用反证法最直观):

  • 假设存在一个 “最大矩形”,它的高度不等于任何一根柱子的高度。
  • 情况 1:它的高度比覆盖范围内的所有柱子都高 → 那这个矩形是 “悬空” 的,根本不成立。
  • 情况 2:它的高度比覆盖范围内的某些柱子矮 → 那我们完全可以把这个矩形的高度往上拔,直到碰到某根柱子的顶部 —— 此时高度增加了,宽度没变,面积肯定更大,说明原来的那个根本不是 “最大” 的。

所以结论是:

所有可能成为 “最大矩形” 的候选,它们的高度必然是某一根具体柱子的高度。

这意味着:我们不需要去考虑 “任意高度的矩形”,只需要把每一根柱子都当成 “矩形的高”,算出它能达到的最大面积,最后在这些面积里挑最大的—— 就一定能找到全局最优解。


铁律二:对于每一根柱子作为高,它的最大宽度是固定的,且可以被精准找到

既然高已经固定为 heights[mid],那要让面积最大,就必须让宽度最大

那 “以 mid 为高的矩形” 的最大宽度是多少?答案是:从「左边第一个比 mid 矮的柱子」到「右边第一个比 mid 矮的柱子」之间的距离。

我给你画个图你就秒懂了(还是用经典例子 heights = [2, 1, 5, 6, 2, 3],看中间那根高度为 5 的柱子,下标 2):

  • 左边界:左边第一个比 5 矮的柱子是下标 1(高度 1);
  • 右边界:右边第一个比 5 矮的柱子是下标 4(高度 2);
  • 最大宽度右边界 - 左边界 - 1 = 4 - 1 - 1 = 2
  • 最大面积5 * 2 = 10

为什么这就是 “最大宽度”?因为:

  • 如果宽度再往左扩一点,就会碰到左边界的 1—— 此时矩形的高度就不能再保持 5 了,会被 1 压成 1
  • 如果宽度再往右扩一点,就会碰到右边界的 2—— 同理,高度会被压成 2

所以,只要找到了这两个 “左右边界”,我们就算出了这根柱子能贡献的「理论最大面积」—— 不可能更大了。


铁律三:单调栈能在一次遍历中,为每一根柱子精准找到左右边界

这就是这个算法最 “厉害” 的地方 —— 它不是暴力地为每根柱子单独去左右找边界(那样是 O(n2)),而是通过维护一个单调递增栈,在遍历的过程中 “顺便” 就把所有边界都找好了。

我再给你拆解一下单调栈的 “工作流”,你就能看到它是如何 “一箭双雕” 的:

  1. 栈的规则:只存柱子的下标,且下标对应的高度严格单调递增

  2. 遇到更高的柱子:直接入栈 —— 因为它还没找到右边界,继续往后看。

  3. 遇到更矮的柱子(关键!)

    • 此时,栈顶元素的右边界找到了(就是当前这个更矮的柱子);
    • 把栈顶元素 mid 弹出来,此时新的栈顶就是 mid 的左边界(因为栈是递增的,新栈顶是左边第一个比 mid 矮的);
    • 好了,mid 的左右边界都齐了,算面积!
  4. 处理栈里剩下的元素

    • 遍历完数组后,栈里可能还有一些元素 —— 它们的右边界是数组的末尾(因为右边没有比它们更矮的了);
    • 依次弹出,左边界还是栈顶,右边界设为 heights.size(),继续算面积。

你看,每一根柱子都会入栈一次、出栈一次,出栈的时候就是它 “算总账” 的时候 —— 此时它的左右边界都已确定,算出来的就是它的最大面积。


总结:为什么这个算法能 “保证找到最大矩形”?

把三个铁律串起来,就是一个完美的逻辑闭环:

  1. 前提:最大矩形的高,一定是某根柱子的高 → 我们只需要算每根柱子的最大面积;
  2. 核心:对于每根柱子,只要找到左右第一个更矮的边界,就能算出它的最大面积;
  3. 手段:单调栈能在 O(n) 时间内,为每根柱子精准找到这两个边界,并算出最大面积;
  4. 结果:我们算出了所有 “候选矩形” 的最大面积,最后取最大值 → 这就是全局最大的矩形。

一句话总结:我们没有漏掉任何一个可能的 “最大候选”,并且把每个候选都算到了它的极限,所以最终结果一定是最大的。