栈与单调栈专题:有效的括号、最小栈、字符串解码、每日温度、柱状图中最大的矩形)
前面已经把数组、哈希、双指针、滑动窗口链表专题这些比较常见的基础块过了一遍。这一篇开始进入栈。很多人一开始学栈的时候感觉它很简单不就是“先进后出”吗。但真正刷 Hot100 的时候你会发现栈简单归简单但特别能出题。尤其是下面这几类括号匹配表达式 / 字符串展开维护“最近更大 / 更小”元素单调栈优化区间问题1. 先把栈最核心的感觉记住栈的特点就一句话后进先出LIFO就像一摞盘子最后放上去的最先拿出来C 里最常用的是stackint st;几个最常用操作st.push(x); // 入栈 st.pop(); // 出栈 st.top(); // 看栈顶 st.empty(); // 判空你现在先别把它看得太复杂前期就把它理解成一个只能从顶部操作的容器。2. 什么题该想到栈你先记几个特别强的信号。2.1 括号匹配看到()[]{}这种题基本第一反应就是栈。因为右括号要和最近的、还没匹配的左括号对应。这个“最近”两个字就很有栈味。2.2 要处理“嵌套结构”比如3[a2[c]]这种题里面有一层套一层遇到右括号时回头处理前面的内容这也非常适合栈。2.3 要找“右边第一个更大元素”比如每日温度下一个更大元素这类题通常不是普通栈而是单调栈2.4 要维护“当前最小值 / 最大值”比如最小栈这种设计题本质是普通栈存元素再额外想办法快速拿到最值3. 第一题有效的括号题意很直接给定一个只包含括号的字符串s判断它是否有效。要求左括号必须按正确顺序闭合而且每个右括号都要有对应类型的左括号。(力扣 LeetCode)3.1 这题第一眼该想到什么这题就是栈的招牌题。因为当你遇到一个右括号时你要去找最近那个还没被匹配掉的左括号。“最近、未匹配”这两个关键词非常典型地对应栈顶。3.2 最基本思路遍历字符串如果是左括号就入栈如果是右括号就看栈顶能不能和它匹配不匹配直接false最后栈空说明全部匹配成功3.3 代码class Solution { public: bool isValid(string s) { stackchar st; for (char c : s) { if (c ( || c [ || c {) { st.push(c); } else { if (st.empty()) return false; char top st.top(); st.pop(); if ((c ) top ! () || (c ] top ! [) || (c } top ! {)) { return false; } } } return st.empty(); } };3.4 代码怎么理解比如字符串([{}])过程大概是(入栈[入栈{入栈遇到}弹出{匹配成功遇到]弹出[匹配成功遇到)弹出(匹配成功最后栈空所以有效。3.5 这题最容易犯的错错误 1遇到右括号时没先判空比如)这种一上来就是右括号如果你不先判空直接top()就会出问题。错误 2最后忘记判断栈是否为空比如(((遍历过程中没出错但最后还有没匹配完的左括号所以也应该返回false。3.6 这题你要记住的括号匹配固定模型左括号入栈右括号匹配栈顶最后栈必须为空这个模型后面还会反复见到。4. 第二题最小栈这题要求你设计一个支持push、pop、top并且能在常数时间内返回最小元素的栈。(力扣 LeetCode)4.1 这题第一眼该想到什么普通栈只能做到入栈出栈看栈顶但现在题目还要求getMin()也是 O(1)那最直接的问题就是如果当前最小值被弹出了新的最小值是谁如果你每次都重新遍历栈那就不是O(1)了。所以这题的关键是再维护一个“最小值栈”4.2 思路我们准备两个栈一个正常栈st专门存所有元素。一个最小栈minSt栈顶始终保存“当前阶段的最小值”。比如每次压入一个新值val时st.push(val)minSt.push(min(val, minSt.top()))这样minSt的每一层都对应st当前状态下的最小值。4.3 代码class MinStack { public: stackint st; stackint minSt; MinStack() { } void push(int val) { st.push(val); if (minSt.empty()) { minSt.push(val); } else { minSt.push(min(val, minSt.top())); } } void pop() { st.pop(); minSt.pop(); } int top() { return st.top(); } int getMin() { return minSt.top(); } };4.4 代码怎么理解假设依次压入3, 5, 2, 4那两个栈会变成普通栈3 5 2 4最小栈3 3 2 2你会发现普通栈顶是当前最后一个元素最小栈顶是当前整个栈里的最小值这样getMin()直接返回minSt.top()就行。4.5 这题你要记住的设计类栈题常见思路普通栈不够就再加一个辅助栈来维护额外信息。这里维护的是最小值。以后也可能维护最大值或者别的状态。5. 第三题字符串解码这题给你一个经过编码的字符串规则是k[encoded_string]表示括号里的字符串重复k次要求你返回解码后的结果。(力扣 LeetCode)5.1 这题第一眼该想到什么看到这种3[a2[c]]你就要有感觉有数字有括号有嵌套碰到]才能知道这一层该怎么展开这种结构非常适合用栈。因为每进一层括号就相当于保存一层现场每遇到]就把这一层收回来。5.2 这题怎么想最顺我们遍历字符串同时维护当前重复次数num当前构造出来的字符串cur一旦遇到[说明准备进入新的一层把当前的num和cur压栈保存然后重置开始处理括号里的内容一旦遇到]说明这一层结束了把前一层的字符串和次数取出来把当前字符串重复若干次接回去5.3 代码class Solution { public: string decodeString(string s) { stackint numSt; stackstring strSt; int num 0; string cur ; for (char c : s) { if (isdigit(c)) { num num * 10 (c - 0); } else if (c [) { numSt.push(num); strSt.push(cur); num 0; cur ; } else if (c ]) { int repeat numSt.top(); numSt.pop(); string prev strSt.top(); strSt.pop(); string tmp ; for (int i 0; i repeat; i) { tmp cur; } cur prev tmp; } else { cur c; } } return cur; } };5.4 代码怎么理解比如3[a2[c]]遍历过程大概是读到3记下num 3读到[把3和当前空串压栈读到a当前cur a读到2记下num 2读到[把2和a压栈读到c当前cur c读到]弹出2和a得到a cc acc再读到]弹出3和空串得到accaccacc5.5 这题最容易犯的错错误 1多位数字不会处理比如12[a]数字不是一位所以要写num num * 10 (c - 0);不能只记当前这一位。错误 2遇到[时没把之前状态存起来这样嵌套一深就乱了。5.6 这题你要记住的核心处理嵌套结构的题经常是入栈保存现场出栈恢复现场。这题就是非常典型的一道。6. 第四题每日温度这题给你一个数组temperatures要求返回一个数组answer其中answer[i]表示第i天之后要等几天才会出现更高温度如果之后都不会升高就填 0。(力扣 LeetCode)6.1 这题第一眼该想到什么这题的核心不是“比较大小”本身而是对每个位置找右边第一个比它大的数你一看到这种“右边第一个更大元素”就要往单调栈上想。6.2 什么叫单调栈单调栈本质上还是栈只不过栈里的元素保持某种单调性。这一题里我们维护的是单调递减栈存下标为什么存下标因为最后答案要的是更高温度那天的下标 - 当前下标所以光存温度值不够要存位置。6.3 思路从左到右遍历每一天。设当前天数是i温度是temperatures[i]。如果当前温度比栈顶对应的温度高说明栈顶那一天终于找到了“右边第一个更高温度”那就可以算答案了。然后不断弹栈直到栈空或者栈顶温度不小于当前温度。最后把当前下标压入栈。6.4 代码class Solution { public: vectorint dailyTemperatures(vectorint temperatures) { int n temperatures.size(); vectorint ans(n, 0); stackint st; // 存下标 for (int i 0; i n; i) { while (!st.empty() temperatures[i] temperatures[st.top()]) { int idx st.top(); st.pop(); ans[idx] i - idx; } st.push(i); } return ans; } };6.5 代码怎么理解假设[73,74,75,71,69,72,76,73]遍历到74时发现比栈顶73大所以73那一天的答案就是1遍历到75时比74大所以74那一天答案也是1遍历到72时会把前面比它小的、还没找到更高温度的那些天依次弹出来结算这就是单调栈的典型过程。6.6 这题你要记住的核心单调栈最经典用途找右边第一个更大找右边第一个更小找左边第一个更大 / 更小而“每日温度”就是最标准的入门题之一。6.7 这题最容易犯的错错误 1栈里存值不存下标这样你算不出距离。错误 2不知道为什么要弹栈因为一旦当前温度更高就说明栈顶那一天的答案已经确定了。7. 第五题柱状图中最大的矩形这题给你一个数组heights表示柱状图每根柱子的高度每根宽度都是 1要求求出能勾勒出的最大矩形面积。(力扣 LeetCode)7.1 这题第一眼该想到什么这题是单调栈里的经典难题。先别被“困难”两个字吓到它本质上还是在做一件事以每一根柱子作为高看看它最多能向左右扩多宽如果某根柱子高度是h那只要左右两边的柱子都不低于它它就能继续扩。所以问题就变成找每根柱子左边第一个比它小的位置找每根柱子右边第一个比它小的位置这就是单调栈特别擅长的事。7.2 先理解面积公式假设当前看的是第i根柱子高度为heights[i]如果它左边第一个更小的位置是left右边第一个更小的位置是 right那它能扩展的宽度就是right - left - 1所以面积是heights[i] * (right - left - 1)核心就变成怎么快速求每根柱子的左右边界。7.3 单调栈怎么做这里我们维护一个单调递增栈存下标意思是栈里对应的柱子高度从栈底到栈顶递增。一旦当前柱子高度比栈顶小说明栈顶那根柱子的“右边第一个更小”找到了当前下标就是它的右边界然后它弹出后新的栈顶就是它左边第一个更小的位置。这样就能直接算面积。7.4 为什么常常在数组两边补 0这是这题特别常见的技巧。我们在原数组左右各补一个 0左边补 0右边补 0这样有两个好处好处 1保证所有柱子最后都能被弹出来结算。好处 2边界处理简单很多不容易漏掉最左或最右的情况。7.5 代码class Solution { public: int largestRectangleArea(vectorint heights) { vectorint h; h.push_back(0); for (int x : heights) h.push_back(x); h.push_back(0); stackint st; int ans 0; for (int i 0; i h.size(); i) { while (!st.empty() h[i] h[st.top()]) { int mid st.top(); st.pop(); int left st.top(); int right i; int width right - left - 1; int area h[mid] * width; ans max(ans, area); } st.push(i); } return ans; } };7.6 代码怎么理解当我们遍历到当前柱子h[i]时如果h[i] h[st.top()]说明栈顶那根柱子的右边第一个更小元素就是i。把栈顶弹出来设它下标是mid。此时right ileft st.top()弹出后的新栈顶于是width right - left - 1 area h[mid] * width这样就能更新答案。7.7 这题最难的地方到底是什么很多人不是不会写栈而是不知道为什么“弹出时”就能结算面积。你要这样理解一旦某根柱子右边遇到了更矮的柱子那它继续往右扩就不可能了。所以就在这一刻它的最大宽度确定了。而它左边第一个更小的位置正好就是弹出后新的栈顶。也就是说弹栈这一刻就是这根柱子结算的时候这句话很关键。7.8 这题你要记住的核心柱状图最大矩形固定想法枚举“高”单调栈找左右第一个更小弹栈时结算面积这题是真正理解单调栈的代表题。8. 这五道题放在一起你要看出什么这一篇最重要的不是你又记了五道题而是你要看出来栈题其实也有固定分类8.1 有效的括号核心普通栈最近未匹配左括号8.2 最小栈核心设计题辅助栈维护最小值8.3 字符串解码核心栈保存嵌套层的现场出栈恢复上一层状态8.4 每日温度核心单调栈右边第一个更大元素8.5 柱状图中最大的矩形核心单调栈左右第一个更小元素弹栈时结算9. 模板9.1 括号匹配模板for (char c : s) { if (是左括号) { st.push(c); } else { if (st.empty()) return false; if (不匹配) return false; st.pop(); } } return st.empty();9.2 辅助栈模板stackint st; stackint help;当普通栈操作时辅助栈同步维护额外信息。9.3 单调栈找右边第一个更大for (int i 0; i n; i) { while (!st.empty() nums[i] nums[st.top()]) { int idx st.top(); st.pop(); // 结算 idx } st.push(i); }9.4 单调栈找左右边界while (!st.empty() h[i] h[st.top()]) { int mid st.top(); st.pop(); int left st.top(); int right i; // 结算 mid }10. 本篇总结这篇里最关键的两块是第一块普通栈有效的括号最小栈字符串解码第二块单调栈每日温度柱状图中最大的矩形