数据结构与算法总结4(个人原创,带详细注释代码)

32. 最长有效括号

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

示例 3:

输入:s = ""
输出:0
动态规划

结合题目,有 最长 这个字眼,可以考虑尝试使用 动态规划 进行分析。这是一个 最值型 动态规划的题目。

定义一个 dp 数组,其中第 i 个位置表示强制以下标为 i 的字符「结尾」的最长有效子字符串的长度。

看第 i 个位置,这个位置的元素 s[i] 可能有如下两种情况:

  • s [ i ] = = ′ ( ′ s[i]==′(′ s[i]==(

    这时,s[i] 无法和其之前的元素组成有效的括号对,所以,dp[i]=0

  • s [ i ] = = ′ ) ′ s[i]==′)′ s[i]==)

    这时,需要看其前面对元素来判断是否有有效括号对。

    情况1:

    s [ i − 1 ] = = ′ ( ′ s[i−1]==′(' s[i1]==(

    即 s[i] 和 s[i−1] 组成一对有效括号,有效括号长度新增长度2,i位置的最长有效括号长度为 其之前2个位置的最长括号长度加上当前位置新增的2,我们无需知道i−2位置对字符是否可以组成有效括号对。

    那么有:

    d p [ i ] = d p [ i − 2 ] + 2 dp[i]=dp[i−2]+2 dp[i]=dp[i2]+2

    情况2:

    s [ i − 1 ] = = ′ ) ′ s[i−1]==′)′ s[i1]==)

    这种情况下,如果前面有和s[i]组成有效括号对的字符,即形如 ((…)),这样的话,就要求s[i−1]位置必然是有效的括号对,否则s[i]无法和前面对字符组成有效括号对。

    这时,我们只需要找到和s[i]配对对位置,并判断其是否是 ( 即可。和其配对对位置为:i-1-dp[i−1]。

    • i-1为其上一个位置的下标,dp[i-1]为以上一个元素结尾的最长括号对长度,二者相减即为该括号对串开头的前一个元素下标

    如果:

    s [ i − 1 − d p [ i − 1 ] ] = = ′ ( ′ s[i−1-dp[i−1]]==′(′ s[i1dp[i1]]==(

    有效括号长度新增长度 2,i 位置对最长有效括号长度为 i-1位置的最长括号长度加上当前位置新增的 2,那么有:

    d p [ i ] = d p [ i − 1 ] + 2 dp[i]=dp[i−1]+2 dp[i]=dp[i1]+2

    值得注意的是,i−1-dp[i−1] 和 i 组成了有效括号对,这将是一段独立的有效括号序列,如果之前的子序列是形如 (…) 这种序列,那么当前位置的最长有效括号长度还需要加上这一段。所以:

    d p [ i ] = d p [ i − 1 ] + d p [ i − 1 − d p [ i − 1 ] − 1 ] + 2 dp[i]=dp[i−1]+dp[i−1-dp[i−1]−1]+2 dp[i]=dp[i1]+dp[i1dp[i1]1]+2

class Solution {
public:
    int longestValidParentheses(string s) {
        int ans = 0;
        s.insert(0,")");//初始加了哨兵,接下来s[i-1]就不用判断i>0了
        vector<int> dp(s.length(),0);
        for(int i=1;i<s.length();i++){
            if(s[i]=='(') continue;//'('作为结尾的括号对必定不合法
            else{
                if(s[i-1]=='('){//和上一个'('配对
                    dp[i] = dp[i-2]+2;
                }
                else{
                    if(s[i-1-dp[i-1]]=='('){//和以上一个位置的')'结尾的括号对开头前一个元素配对
                        dp[i]=dp[i-1]+2+dp[i-2-dp[i-1]];//前面')'结尾括号对+2+再前面括号对长度
                    }
                }
                ans = max(ans,dp[i]);
            }
        }
        return ans;
    }
};

始终保持栈底元素为当前已经遍历过的元素中**「最后一个没有被匹配的「右括号」的下标」**,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:

  • 对于遇到的每个 ‘(’ ,我们将其入栈
  • 对于遇到的每个 ‘)’,我们先弹出栈顶元素:
    • 如果栈为空,说明弹出了上一个不能匹配的右括号,也就是当前的 ‘)’ 没有对应的左括号,也就是有效子串的判定到此结束,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
    • 如果栈不为空,当前右括号的下标减去弹出后的栈顶元素即为「以该右括号为结尾的最长有效括号的长度」,这里栈顶元素为「该有效串开头的上一个位置」,因此计算长度时候不用加一了

我们从前往后遍历字符串并更新答案即可。

需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的栈底为「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 −1 的元素用来计算长度。

  • 比如’(‘’)‘,下标为1的’)‘使得下标为0的’('出栈了,此时有效子串长度为 1-(-1) = 2

1.为何初始栈顶元素要放-1?因为要保证当有左括号出栈时,有栈顶元素可以配合计算长度(也就是可以相减),选择-1是因为下标是从0开始的。

2.左括号入栈是因为以左括号结尾的任意子串都为无效括号串,不需要计算直接入栈即可。

也就是左括号不能主动形成有效连续串,只能被右括号征召而被动弹出形成串,而右括号是可以主动形成连续串的

3.为何要出栈后减去栈顶元素而不是弹出的元素?如果采用:遇到")"就将栈顶元素和它配对,当前匹配的字符的长度就是当前下标减去栈顶下标再加一。这样只是计算以当前元素作为边界的括号有效的字符长度,但是需要计算的是所有连续的括号有效字符的总长度。比如“()()”这个字符串。错误的做法返回2,而实际上长度为4

也即核心定义为:以当前’)‘为「子串结尾」的最大长度 而非 以当前’)'为「括号对结尾」的最大长度

4.遇到无法匹配的右括号则入栈,遇到更大下标的右括号则更新,因为一旦有无法匹配的右括号说明其前面遍历过的部分要废掉,从下一个括号开始计算新的子串长度

总结为

  1. 左 -> 入栈,右 -> 弹出栈中左
  2. 弹栈后的栈顶元素是连续串的上一个位置
  3. 所有无法配对的右括号都是子串的连续断处,从其后面重新开始计算连续长度
  4. 栈中起始“哨兵”设置为-1,表示第一个「最后一个没有被匹配的右括号的下标」,以计算连续串长度
class Solution {
public:
    int longestValidParentheses(string s) {
        int ans = 0;
        stack<int> st;
        st.push(-1);//一开始的有效子串的上一个位置为-1
        for(int i=0;i<s.length();i++){//遇到左括号不可能形成有效子串,只有右括号有可能形成
            if(s[i]=='(') st.push(i);
            else{
                st.pop();
                if(st.empty()) st.push(i);//把上个无法匹配的右括号弹出了,当前右括号无效,入栈更新
                else ans = max(ans,i-st.top());//否则形成了连续有效子串,更新长度
            }
        }
        return ans;
    }
};

根据

总结为

  1. 左 -> 入栈,右 -> 弹出栈中左
  2. 右括号匹配弹栈后,连续串长度需要更新时,有两种可能:
    • 弹栈后的栈顶元素是连续串的「上一个位置」,比如"((()"
    • 弹栈后没有“上一个位置”的信息了,那么要用一个额外变量记录一整个连续串的起始位置,如"()()"
  3. 所有无法配对的右括号都是子串的连续断处,从其后面重新开始计算连续长度

也可写作如下,也就是遇到右括号弹栈并计算串长度时,手动分辨是从上一个位置计算还是从头开始处计算

该做法的本质:栈里只存放待匹配的 ( 符号,虽然这些 ( 在原字符串中的下标不一定连续,但 ( 之间一定为有效括号,因此可以使用栈顶元素作为有效括号的左边界计算长度。

举个 🌰,对于 s = ((())(( 的情况,当整个字符串处理完,栈内剩下的不是所有的 ‘(’ 符号(不是 5 个),而是剩余待匹配的 ‘(’ 符号( 3 个,即从左往右第 1、4 和 5 个 ‘(’),虽然第 1 和第 4 个 ‘(’ 符号在 s 中不连续,但其之间必然是有效括号。

class Solution {
public:
    int longestValidParentheses(string s) {
        int ans = 0;
        stack<int> st;
        int start = 0;//若整个子串都结束了,栈空,没有连续串的上一个位置,用一个位置记录整个子串的起点
        for(int i=0;i<s.length();i++){
            if(s[i]=='(') st.push(i);//左 -> 入栈
            else{//右 -> 弹出栈中左
                if(st.empty()) start = i+1;//所有无法配对的右括号都是子串的连续断处,重设起点
                else{
                    st.pop();
                    //以下均为计算连续串长度
                    if(st.empty()) ans = max(ans,i-start+1);//当前连续串没有上一个位置
                    else ans = max(ans,i-st.top());//当前连续串有上一个位置
                }
            }
        }
        return ans;
    }
};

总而言之,第一个写法用“维护栈底元素为**「最后一个没有被匹配的「右括号」的下标」**”的设计规避了对弹出左括号后,栈是否为空的讨论,将更新长度的「上一个位置」统一化为弹栈后的栈顶元素

不需要额外空间

在此方法中,我们利用两个计数器 left 和 right 。首先,我们从左到右遍历字符串,对于遇到的每个 ‘(’,我们增加 left 计数器,对于遇到的每个 ‘)’ ,我们增加 right 计数器。每当 left 计数器与 right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当 right 计数器比 left 计数器大时,我们将 left 和 right 计数器同时变回 0。(right>left时,必不可能成合法括号串)

这样的做法贪心地考虑了「以当前字符下标结尾的有效括号长度」,每次当右括号数量多于左括号数量的时候之前的字符我们都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 (() ,这种时候最长有效括号是求不出来的。

解决的方法也很简单,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来:

当 left 计数器比 right 计数器大时,我们将 left 和 right 计数器同时变回 0

当 left 计数器与 right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串
这样我们就能涵盖所有情况从而求解出答案。

class Solution {
public:
    int longestValidParentheses(string s) {
        int left = 0,right = 0;
        int ans = 0;
        for(char c:s){
            c=='('?left++:right++;
            if(left==right) ans = max(ans,left+right);
            else if(right>left) left = right = 0;
        }
        left = right = 0;
        reverse(s.begin(),s.end());//记住reverse的写法,传入头尾迭代器
        for(char c:s){
            c==')'?right++:left++;
            if(left==right) ans = max(ans,left+right);
            else if(left>right) left = right = 0;
        }
        return ans;
    }
};
42.接雨水

题目:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

另一个题解

灵茶山艾府

思路:

  • 每个柱子顶部可以储水的高度为:该柱子的左右两侧最大高度的较小者减去此柱子的高度。
  • 对于height数组每个位置,想象有一个宽度为1存水的桶,那么该桶左右两边的高度即为 该位置左右两侧的最大高度,那么能储水的容量即为桶的短边高度减去柱子高度,因为高于桶左右短边的水是会流出去的,而低于这个高度的不会流出去
  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
动态规划

暴力法中,我们对每个柱子的位置,都向左向右遍历,获得该位置的左右最大高度,然后取较小值减去该点高度获得该点的储水量

在上述的暴力法中,对于每个柱子,我们都需要从两头重新遍历一遍求出左右两侧的最大高度,这里是有很多重复计算的,很明显最大高度是可以记忆化的,具体在这里可以用数组边递推边存储,也就是常说的动态规划,DP。

因此用一个pair数组存储每个位置的左右最大高度,先正反向**(从左往右递推左最大高度,从右往左递推右最大高度)**遍历两遍,填完数组,然后再遍历一遍height数组,对每个点累加储水量得答案

注意这里重点是,每个位置左边最大高度只能由height[0]递推到该点获得,右边最大高度只能由height[n-1]递推到该点,因此需要正反递推两遍

同时由于已递推过并记录上一个点的左右最大高度,这样递推的时候只需要将当前点高度与上一个点记忆化的高度比较 取最大值,不用对每个点从头开始递推,退化为暴力法

//这里体现了dp的记忆化,边递推边存储
for(int i=1;i<n;i++)
    presuffix[i].first = max(presuffix[i-1].first,height[i]);
for(int i=n-2;i>=0;i--)
    presuffix[i].second = max(presuffix[i+1].second,height[i]);
class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        int ret = 0;
        vector<pair<int,int>> presuffix(n);
        presuffix[0].first = height[0];//初始化第一个点和最后一个点的左右最大高度
        presuffix[n-1].second = height[n-1];
        //int prefix = height[0],suffix = height[n-1];
        for(int i=1;i<n;i++)
            presuffix[i].first = max(presuffix[i-1].first,height[i]);
        for(int i=n-2;i>=0;i--)
            presuffix[i].second = max(presuffix[i+1].second,height[i]);
        for(int i=0;i<n;i++) ret+=min(presuffix[i].first,presuffix[i].second)-height[i];
        return ret;
    }
};
双指针(相向)

https://www.bilibili.com/video/BV1Qg411q7ia/?vd_source=9127d0ec8fa2d871e0c484a298e38bde

每个柱子顶部可以储水的高度为:该柱子的左右两侧最大高度的「较小者」减去此柱子的高度。

当前位置的储水量 「只」 取决于**「当前位置」**左右两侧最大值的较小者

假如我们从左往右递推,那么当前点左侧的最大值可以根据上面的dp思想而始终确定,但是右边的最大值是不确定的

如果我们再对当前点求右边的最大值,退化为暴力法, O ( N 2 ) O(N^2) O(N2)

那我们不求具体的右侧最大值,而是确定左侧最大值为「较小者」即可

如何确定?这里便是双指针思想:头尾两个指针朝中间相向移动,二者一边比较一边递推进行的确定

其中一个指针维护「当前位置」左边最大值,一个维护「当前位置」右边最大值(这句话中两个「当前位置」不同,这就是双指针思想,有两个「当前位置」,两个指针轮流当主角,互相利用对方进行比较,然后互换主角身份)

注意到在当前位置起,往左递推可以更新右侧最大值,往右递推更新左侧最大值

  • 也就是说数组记忆化中,不用记录所有已经递推过的位置的左右最大值,只需要更新两个变量leftmost rightmost即可

设置两个指针往中间相向递推,因为左指针从头出发,右指针从尾部出发,左指针位置的左边最大值是确定的,右指针位置的右边最大值是确定的,左指针的右边最大值不确定(因为当前位置和右指针中间部分没有访问过),右同理

即左指针的左边信息确定,右指针右边信息确定

但是当 不确定的右边最大值 已经 大于 确定的左边最大值 时,我们计算储水量时只会考虑较小的左边最大值,那么这个时候右边的不确定性对该点储水计算没有任何意义,不确定性被抹去,可以完备计算,只需要将确定的左边最大值减去该点高度即可得储水量

左右相等的时候,两边最大高度都**「至少」**是这个值,计算哪个指针都可以

也就如图中所示,left指针指向的点,右侧最大值**「至少」是3,无论中间递推到什么结果,都会大于左边最大值,因为递推过程中最大值不会变小,只会不断和height[i]比较取较大者 **

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        int ret = 0;
        int left = 0,right = n-1;//左右遍历指针
        int leftmost = 0,rightmost = 0;//已递推左/右区域的左/右最大值
        while(left<=right){//指针交汇那点也要计算
            leftmost = max(leftmost,height[left]);//先更新 已递推左/右区域的左/右最大值
            rightmost = max(rightmost,height[right]);
            if(leftmost<=rightmost){//然后看对哪个点计算储水量,计算的点移动
                ret +=leftmost-height[left];
                left++;
            }
            else{
                ret+=rightmost-height[right];
                right--;
            }
        }
        return ret;
    }
};

11.盛水最多的容器也是一样的思路,相向双指针,前后双指针不断进行高度比较,因为盛水的面积只取决于高度较小者,那么每次只需移动较小的指针,看后面有没有高度更高的柱子,有无可能更新全局最大面积res即可

高柱子指针不用动,低柱子指针动,这样才「有可能」找到更大的全局面积

如果动了高柱子,面积只会越来越小,因为宽在缩短(指针相向移动)

class Solution {
public:
    int maxArea(vector<int>& height) {
        int ans = 0;
        int left = 0,right = height.size()-1;
        while(left<right){
            int h = height[left]>height[right]?height[right--]:height[left++];
            ans = max(ans,(right-left+1)*h);//因为上面为了简洁已经移动了指针了,这里计算宽的时候要+1 ;)
        }
        return ans;
    }
};

//以前写的
class Solution {
public:
    int maxArea(vector<int>& height) {
        int res=0;
        for(int i=0,j=height.size()-1;i!=j;)
        {
            res = max(res,min(height[i],height[j])*(j-i));
            height[i]>height[j]?--j:++i;
        }
        return res;
    }
};
单调栈

找 上一个/下一个 更大/更小 元素 ——————> 单调栈

栈中找上一个更大元素,在找的过程中填坑

  • 填坑即计算right柱子和left柱子在mid柱子情况下 中间有多少雨水计算完后相当于「填了水泥」,不用管已经计算雨水的坑口了

计算一个坑的雨水需要有left,mid,right三根柱子

right柱子和left柱子中间雨水面积由以下决定

  1. right柱子与left柱子下标之差 -1 (两个柱子之间距离,宽)
  2. right柱子高度和left柱子高度较小者与mid柱子高度之差(高)
    • 注意right和left柱子与mid柱子「均」有高度差,才能形成凹槽接雨水,因此当前柱子与栈顶柱子相同高度时无条件入栈,但是出栈时,如果相邻柱子高度相同,高度差为0,形成不了凹槽,也就没有接雨水

因此转化为单调递减栈,当遍历到的height元素大于当前栈顶元素时,弹出所有(while循环)小于当前元素height的栈中元素,并对每个弹出的元素计算雨水量

单调栈都要用while一次性弹出所有不满足的栈中元素

及时去掉无用数据,保证栈中元素有序。

遍历到高度为4后,栈中弹出高度为0,1,2的柱子,并将每个柱子作为mid,上一个柱子作为left,while循环计算雨水量

注意由于要计算柱子之间的距离(宽),栈中存的是下标而非高度

此时遍历到的元素作为right,栈顶元素作为mid,栈顶后面一个元素作为left

看这个视频

sweetiee

class Solution {
public:
    int trap(vector<int>& height) {
        stack<int> stk;
        int ans = 0;
        for(int i=0;i<height.size();i++){//找到了大于栈顶元素的柱子后,对所有低于该柱子的栈中元素均计算雨水,所以是while
            while(!stk.empty()&&height[i]>height[stk.top()]){//这里为>=可以去除栈中高度重复的柱子
                int right = i;
                int mid = stk.top();
                stk.pop();
                //或者在这里判断一下,将栈中高度相同的元素弹出
                /*while (!stk.empty() && height[stk.top()] == height[mid]) {
                    stk.pop();
                }*/
                if(stk.empty()) break;
                int left = stk.top();
                //注意right和left柱子与mid柱子均有高度差,才能形成凹槽接雨水,因此当前柱子与栈顶柱子相同高度时无条件入栈,但是出栈时,如果相邻柱子高度相同,高度差为0,形成不了凹槽,也就没有接雨水
                ans+=(min(height[right],height[left])-height[mid])*(right-left-1);
            }
            stk.push(i);//栈中存下标
        }
        return ans;
    }
};
84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

示例 2:

输入: heights = [2,4]
输出: 4

提示:

  • 1 <= heights.length <=105
  • 0 <= heights[i] <= 104
暴力

枚举「高」,我们可以使用一重循环枚举某一根柱子,将其固定为矩形的高度 h。随后我们从这跟柱子开始向两侧延伸,直到遇到高度小于 h 的柱子,就确定了矩形的左右边界。如果左右边界之间的宽度为 w,那么对应的面积为 w×h

具体来说是:依次遍历柱形的高度,对于每一个高度分别向两边扩散,求出以当前高度为矩形的最大宽度多少。

为此,我们需要对每个位置:

  • 左边第一个小于它的元素为左边界
  • 右边第一个小于它的元素为右边界

对于每一个位置,我们都这样操作,得到一个矩形面积,求出它们的最大值。

public class Solution {

    public int largestRectangleArea(int[] heights) {
        int len = heights.length;
        // 特判
        if (len == 0) {
            return 0;
        }

        int res = 0;
        for (int i = 0; i < len; i++) {

            // 找左边第一个 1 个小于 heights[i] 的下标
            int left = i;
            int curHeight = heights[i];
            while (left > 0 && heights[left - 1] >= curHeight) {
                left--;
            }

            // 找右边第一个 1 个小于 heights[i] 的索引
            int right = i;
            while (right < len - 1 && heights[right + 1] >= curHeight) {
                right++;
            }

            int width = right - left + 1;
            res = Math.max(res, width * curHeight);
        }
        return res;
    }
}

看到时间复杂度为 O ( N 2 ) O(N^2) O(N2) 和空间复杂度为 O ( 1 ) O(1) O(1) 的组合,那么我们是不是可以一次遍历,不需要中心扩散就能够计算出每一个高度所对应的那个最大面积矩形的面积呢?

很容易想到的优化的思路就是「以空间换时间」。我们需要在遍历的过程中记录一些信息。

单调栈

我们需要对每个位置找到:

  • 左边第一个小于它的元素为左边界
  • 右边第一个小于它的元素为右边界

因此想到单调栈

从左到右遍历,维护单调递增栈,

当遍历到当前元素比栈顶元素小的时候,栈顶元素的右边界就是当前元素,左边界就是栈顶的上一个元素

也就是栈顶元素的左右边界 均能在 O ( 1 ) O(1) O(1) 时间获得

相同元素怎么办?

相同元素添加时不用处理,只处理严格小于的情况,这个时候栈顶元素的 右边界 是确定的,而左边界可能出现连续的相同元素

  • 左边界与当前高度相同时不用管,相同元素只需要算 最左边那个元素(因为最左边的元素的左边就是这一串相同元素的左边界)的面积就可以了,相同元素的面积也相同
  • 也就是无论找右边界还是左边界,都要找严格小于的

也就是

  • 「入栈」 为 找栈顶元素「右边界」,「出栈」 为 找栈顶元素「左边界」
哨兵

为什么要首尾两端添加哨兵?

不加哨兵需要考虑两种特殊的情况:

  1. 弹栈的时候,栈为空也就是找不到当前栈顶元素的左边界
  2. 遍历完成以后,栈中还有元素,也就是入栈的时候找不到当前栈顶元素的右边界
  • 为什么要在heights最前面加哨兵?

    因为没有在heights前加哨兵,不能保证stack不为空

  • 为什么要在heights最后面加哨兵?

    在最后加哨兵可以保证矩形高度都是递增的特殊情况下ans也能进行计算

  • 比如[1,2],如果不加后哨兵,该序列没有输出,如果不加前哨兵,2输出面积后,栈中[1],这时1没有左边界,无法计算其面积

  • 换一种思路理解,我们要找每个柱子的左右边界,那么

    • 「首个柱子缺乏左边界,末尾柱子缺乏右边界」
  • 因为0 <= heights[i] <= 10^4,因此左右添加一个不可能的值-1,面积为负数也不会影响ans

//无哨兵特判版本
class Solution {
public:
    int largestRectangleArea(vector<int> &heights) {
        unsigned long size = heights.size();
        if (size == 1) {
            return heights[0];
        }
        int res = 0;
        stack<int> stk;
        for (int i = 0; i < size; ++i) {
            while (!stk.empty() && heights[stk.top()] > heights[i]) {
                int length = heights[stk.top()];
                stk.pop();
                int weight = i;
                if (!stk.empty()) {//没有哨兵,需要处理栈空
                    weight = i - stk.top() - 1;
                }
                res = max(res, length * weight);
            }
            stk.push(i);
        }
        while (!stk.empty()) {//因为没有哨兵,全部元素入栈一遍后,还要把栈中剩余元素都处理
            int length = heights[stk.top()];
            stk.pop();
            int width = size;//这里初始化就很别扭,想象一下如果栈不为空,那么width会正常被栈中前一个元素计算,如果为空,说明当前元素就是所有元素中最小的(如果前面有更大元素,入栈过程已被弹出了),因此这里width就是size,因为所有矩形大小都包含该矩形
            if (!stk.empty()) {
                width = size - stk.top() - 1;
            }
            res = max(res, length * wdith);
        }
        return res;
    }
};

哨兵

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //头尾给一个不可能的初值,作为左右边界元素的左右边界
        heights.emplace_back(-1);
        heights.emplace(heights.begin(),-1);
        
        stack<int> incstk;
        int ans = 0;
        
        for(int i=0;i<heights.size();i++){
            while(!incstk.empty()&&heights[i]<heights[incstk.top()]){//添加时候不用管相同元素,当有严格递减的元素出现作为栈顶右边界,会一次性弹出栈中所有相同元素,这时候再处理
                
                int base = incstk.top();
                incstk.pop();
                int left = incstk.top();
                
                //左边界与当前高度相同时不用管,相同元素只需要算最左边那个元素(因为最左边的元素左边就是这一串相同元素的左边界)的面积就可以了,相同元素的面积也相同
                //或者思考为:找栈顶元素左边界,也要找严格小于的,如果没有,就不计算了
                if(heights[base] == heights[left]) continue;
                ans = max(ans,heights[base]*(i-left-1));
            }
            incstk.emplace(i);
        }
        return ans;
    }
};

或者这样更清晰

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //头尾给一个不可能的初值,作为左右边界元素的左右边界
        heights.emplace_back(-1);
        heights.emplace(heights.begin(),-1);
        
        stack<int> incstk;
        int ans = 0;
        
        for(int i=0;i<heights.size();i++){
            //外层for和while找栈顶元素右边界,内层while找栈顶元素左边界
            while(!incstk.empty()&&heights[i]<heights[incstk.top()]){
                int base = incstk.top();
                incstk.pop();
                
                //找栈顶元素左边界
                while(heights[base] == heights[incstk.top()]){
					base = incstk.top();
                    incstk.pop();
                }
                
                int left = incstk.top();
                ans = max(ans,heights[base]*(i-left-1));
            }
            incstk.emplace(i);
        }
        return ans;
    }
};
思路更新

比如示例 1(上图),选择 i=2 这个柱子作为矩形高,那么左边小于 heights[2]=5 的最近元素的下标为 left=1,右边小于 heights[2]=5 的最近元素的下标为 right=4。

那么矩形的宽度就是 right−left−1=4−1−1=2,矩形面积为 h⋅(right−left−1)=5⋅2=10

假设 h=heights[i] 是矩形的高度,那么矩形的宽度最大是多少?我们需要知道:

  • 在 i 左侧的**「严格」小于 h 的最近元素**的下标 left。
  • 在 i 右侧的**「严格」小于 h 的最近元素**的下标 right。

如何快速计算 left 和 right?就是每个 i 的左右两侧「上一个更小元素」

朴素单调栈想法即:正逆序两遍遍历维护单调栈,用两个数组,正序存起来每个元素的 left ,逆序存 right

以下几点注意:

  • left 中所有下标初始值为 -1,也即当heights为单调递时,所有元素的边界都是 下标 -1(heights界外)
  • right 中所有下标初始值为 n,也即当heights为单调递时,所有元素的边界都是 下标 n(heights界外)
    • n = heights.size()
  • 注意正序遍历完后,栈需要清空 stk = stack<int>();
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int ans = 0;
        stack<int> stk;
        vector<int> left(heights.size(),-1);//所有元素的**左**边界都是 下标 -1(heights界外)
        for(int i=0;i<heights.size();i++){
            while(!stk.empty()&&heights[stk.top()]>=heights[i]) stk.pop();
            if(!stk.empty()) left[i] = stk.top();
            stk.push(i);
        }
        stk = stack<int>();//栈需要清空
        vector<int> right(heights.size(),heights.size());//所有元素的**右**边界都是 下标 n(heights界外)
        for(int i=heights.size()-1;i>=0;i--){
            while(!stk.empty()&&heights[stk.top()]>=heights[i]) stk.pop();
            if(!stk.empty()) right[i] = stk.top();
            ans = max(ans,(right[i]-left[i]-1)*heights[i]);
            stk.push(i);
        }
        return ans;
    }
};
85. 最大矩形

给定一个仅包含 01 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例 1:

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。
复用84题单调栈

对每行遍历一次,统计柱子高度,将每行的柱子高度数据作为heights传入84题代码

对每行的柱子高度都求一遍最大矩阵面积,每行都更新一遍全局最大值ans

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int ans = 0;
        vector<int> heights(matrix[0].size());
        for(int i=0;i<matrix.size();i++){
            //遍历每一行,更新当前行下的列高度,然后计算当前行的最大矩阵面积
            for(int j=0;j<matrix[0].size();j++){
                if(matrix[i][j]=='1') heights[j]++;//注意是char矩阵,'1'
                else heights[j] = 0;
            }
            ans = max(ans,largestRectangleArea(heights));
        }
        return ans;
    }

    //84题的代码原封复制过来
    int largestRectangleArea(vector<int>& heights) {
        int ans = 0;
        stack<int> stk;
        vector<int> left(heights.size(),-1);
        for(int i=0;i<heights.size();i++){
            while(!stk.empty()&&heights[stk.top()]>=heights[i]) stk.pop();
            if(!stk.empty()) left[i] = stk.top();
            stk.push(i);
        }
        stk = stack<int>();
        vector<int> right(heights.size(),heights.size());
        for(int i=heights.size()-1;i>=0;i--){
            while(!stk.empty()&&heights[stk.top()]>=heights[i]) stk.pop();
            if(!stk.empty()) right[i] = stk.top();
            ans = max(ans,(right[i]-left[i]-1)*heights[i]);
            stk.push(i);
        }
        return ans;
    }
};
901.股票价格跨度

设计一个算法收集某些股票的每日报价,并返回该股票当日价格的 跨度

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

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

实现 StockSpanner 类:

  • StockSpanner() 初始化类对象。
  • int next(int price) 给出今天的股价 price ,返回该股票当日价格的 跨度

示例:

输入:
["StockSpanner", "next", "next", "next", "next", "next", "next", "next"]
[[], [100], [80], [60], [70], [60], [75], [85]]
输出:
[null, 1, 1, 1, 2, 1, 4, 6]

解释:
StockSpanner stockSpanner = new StockSpanner();
stockSpanner.next(100); // 返回 1
stockSpanner.next(80);  // 返回 1
stockSpanner.next(60);  // 返回 1
stockSpanner.next(70);  // 返回 2
stockSpanner.next(60);  // 返回 1
stockSpanner.next(75);  // 返回 4 ,因为截至今天的最后 4 个股价 (包括今天的股价 75) 都小于或等于今天的股价。
stockSpanner.next(85);  // 返回 6
单调栈

这道题可以转化成:对于每一个输入的数,找上一个更大元素(NGE) 与其的距离 ——> 单调栈

  • 因为要求距离,因此栈中需要记录下标下标之差即距离

记住单调栈十六字真言:

及时去掉无用数据,保证栈中元素有序。

暴力做法是从当前位置往回找,直到找到一个大于 price 的数为止,即 price 的上一个更大元素

但实际上,对于已经访问过,小于等于 price 的数 x,不可能作为后续 next 输入的数的上一个更大元素因为 x≤price 且更远)。所以一旦发现小于等于 price 的数,就直接移除

每次使用单调栈考虑正确性时,想到下面这张图:

后来者的上一个更大元素只能是当前输入的元素所有栈中的更小元素已经被当前元素挡住了(想象成一座更高的山把后面的矮个子挡住了,也就是下图的5将2,3挡住了,5前面的元素(1)只能看到5,看不到2,3),因此栈中小于当前元素的弹出不影响正确性,只需维护栈的单调性即可

从后往前看比自己矮的山峰⛰️一样

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class StockSpanner {
private:
    int num = 1;//可以在类的定义中直接初始化成员变量
    stack<pair<int,int>> stk;
public:
    StockSpanner() {

    }
    
    int next(int price) {
        while(!stk.empty()&&price>=stk.top().first){
            stk.pop();
        }
        int ans = stk.empty()?num:num-stk.top().second;
        stk.push(make_pair(price,num++));
        //可以替换成stk.emplace(price,num++);emplace自动调用pair构造函数了
        return ans;
    }
};
1019. 链表中的下一个更大节点

给定一个长度为 n 的链表 head

对于列表中的每个节点,查找下一个 更大节点 的值。也就是说,对于每个节点,找到它旁边的第一个节点的值,这个节点的值 严格大于 它的值。

返回一个整数数组 answer ,其中 answer[i] 是第 i 个节点( 从1开始 )的下一个更大的节点的值。如果第 i 个节点没有下一个更大的节点,设置 answer[i] = 0

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:head = [2,1,5]
输出:[5,5,0]

示例 2:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:head = [2,7,4,3,5]
输出:[7,0,5,5,0]
单调栈 {#stack-1}

从右往左/逆序遍历的思考:因为是从右逆序开始,因此每个元素右侧所有元素都是已知,逻辑如下,

对每个元素检查栈,弹出比其小的元素,则栈顶元素为其NGE,若栈空,则其没有NGE

  • 因为给一个序列,其中左---->前,右---->后,因此从右往左/从后往前,就可以得知每个元素 所有后面的元素
  • 如果找下一个更大/小元素,就考虑逆序遍历,找上一个更大/小元素,正序遍历,因为 每个枚举元素 左边/前面 元素是已处理的,自然得到 “上一个” 的信息
  • 类似的逆序思想用在 670.最大交换

对每个数,寻找它下一个更大元素

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于链表如何逆序访问?我们可以从头节点开始递归,在「归」的过程中,就相当于是从右到左遍历链表了。

先递再归,便实现了链表的逆序访问

class Solution {
    vector<int> ans;
    stack<int> st; // 单调栈(节点值)

    void f(ListNode *node, int i) {
        if (node == nullptr) {
            ans.resize(i); // i 就是链表长度
            return;
        }
        
        f(node->next, i + 1);//往下递,同时用i探查链表长度
        
        //以下为「归」的过程,相当于是从右到左遍历链表,也是递归三部曲中,该层递归内要做的事情,没有返回值
        while (!st.empty() && st.top() <= node->val)
            st.pop(); 
        if (!st.empty())
            ans[i] = st.top(); // 栈顶就是第 i 个节点的下一个更大元素
        st.push(node->val);
    }

public:
    vector<int> nextLargerNodes(ListNode *head) {
        f(head, 0);
        return ans;
    }
};

用每个数,更新其它数的下一个更大元素

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于记录答案需要元素的下标,所以栈中除了保存元素值以外,还需要保存元素的下标。

class Solution {
public:
    vector<int> nextLargerNodes(ListNode* head) {
        int num = 0;
        stack<pair<int,int>> stk;
        vector<int> ans(10000);//由于不知道链表多长,先开这么大的数组,再resize
        while(head){
            while(!stk.empty()&&head->val>stk.top().first){
                ans[stk.top().second] = head->val;
                stk.pop();
            }
            stk.emplace(head->val,num);
            head = head->next;
            num++;
        }
        ans.resize(num);
        return ans;
    }
};

优化

不需要事先开长答案数组,因为是从左到右遍历,是严格按照「右边才可能有下一个更大元素」顺序的

因此每次遍历到一个元素,答案数组推入一个占位器即可,不用担心下标会超当前数组长度

也就是:访问一个元素,答案数组变长1位,并且当前访问元素作为「已递推元素」的NGE,而这些「已递推元素」的下标已经在ans中,因为是从左到右递推

并且 当前 ans 的长度就是当前节点的下标

class Solution {
public:
    vector<int> nextLargerNodes(ListNode *head) {
        vector<int> ans;
        stack<pair<int, int>> st; // 单调栈(节点值,节点下标)
        for (auto cur = head; cur; cur = cur->next) {
            while (!st.empty() && st.top().first < cur->val) {
                ans[st.top().second] = cur->val; // 用当前节点值更新答案
                st.pop();
            }
            // 当前 ans 的长度就是当前节点的下标
            st.emplace(cur->val, ans.size());
            ans.push_back(0); // 占位
        }
        return ans;
    }
};

再优化

把第 i 个节点的值记录到 ans[i],这样栈中就可以只保存下标了。

在循环结束后,栈中下标对应元素是没有下一个更大元素的,所以要把栈中的下标对应的 ans 置为 0。

class Solution {
public:
    vector<int> nextLargerNodes(ListNode *head) {
        vector<int> ans;
        stack<int> st; // 单调栈(只存下标)
        for (auto cur = head; cur; cur = cur->next) {
            while (!st.empty() && ans[st.top()] < cur->val) {
                ans[st.top()] = cur->val; // ans[st.top()] 后面不会再访问到了
                st.pop();
            }
            st.push(ans.size()); // 当前 ans 的长度就是当前节点的下标
            ans.push_back(cur->val);
        }
        while (!st.empty()) {
            ans[st.top()] = 0; // 没有下一个更大元素
            st.pop();
        }
        return ans;
    }
};
  • 30
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值