3.17-栈

20. 有效的括号 - 力扣(LeetCode)

s

class Solution {
  public:
      bool isValid(string s) {
          stack<char> st;
          for (int i = 0; i < s.length(); i++) {
             if(st.empty() || s[i] == '(' || s[i] == '[' || s[i] == '{') {
              st.push(s[i]);
              continue;
             }
             char t = st.top();
             st.pop();
             if(t == '(' && s[i] == ')') continue;
             else if(t == '[' && s[i] == ']') continue;
             else if(t == '{' && s[i] == '}') continue;
             else return false;
          }
          return st.empty();
      }
  };

灵神题解

什么情况下是无效字符串?

  1. 左括号没有对应的右括号。例如 ((),缺失了一个右括号。
  2. 右括号没有对应的左括号。例如 ()),缺失了一个左括号。
  3. 括号类型不匹配。例如 [()},其中 [ 要和 } 组成一对括号,但是括号类型不同。

思路

本题是「邻项消除」问题(见文末的题单),这类问题都可以用栈解决。

s={[()]} 为例说明:

  1. 创建一个空栈。

  2. 从左到右遍历 s。

  3. s[0]={,这是一个左括号,入栈。

  4. s[1]=[,这是一个左括号,入栈。

  5. s[2]=(,这是一个左括号,入栈。

  6. s[3]=),这是一个右括号,它必须和栈顶的 ( 组成一对(消除),弹出栈顶。

  7. s[4]=],这是一个右括号,它必须和栈顶的 [ 组成一对(消除),弹出栈顶。

  8. s[5]=},这是一个右括号,它必须和栈顶的 { 组成一对(消除),弹出栈顶。

  9. 遍历结束,由于栈为空,说明所有括号均已匹配完毕,返回 true。反之,如果在遍历的过程中,发现栈为空,或者括号类型不匹配的情况,返回 false。此外,如果遍历结束栈不为空,说明还有没匹配的左括号,返回 false。

细节

由于括号两两一对,所以 s 的长度必须是偶数。如果 s 的长度是奇数,可以直接返回 false。
我们可以创建一个哈希表(或者数组),保存每个右括号对应的左括号,这样可以直接判断栈顶的左括号是否与右括号为同一类型,从而省去大量 if-else 判断。

写法一

class Solution {
    unordered_map<char, char> mp = {{')', '('}, {']', '['}, {'}', '{'}};
public:
    bool isValid(string s) {
        if (s.length() % 2) { // s 长度必须是偶数
            return false;
        }
        stack<char> st;
        for (char c : s) {
            // mp.contains(c) 用来判断 c 是不是 mp 的一个 key
            if (!mp.contains(c)) { // c 是左括号
                st.push(c); // 入栈
            } else { // c 是右括号
                if (st.empty() || st.top() != mp[c]) {
                    return false; // 没有左括号,或者左括号类型不对
                }
                st.pop(); // 出栈
            }
        }
        return st.empty(); // 所有左括号必须匹配完毕
    }
};

写法二

也可以在哈希表/数组中保存每个左括号对应的右括号。在遍历到左括号时,把对应的右括号入栈。这样遍历到右括号时,只需看栈顶括号是否一样即可。

class Solution {
    unordered_map<char, char> mp = {{'(', ')'}, {'[', ']'}, {'{', '}'}};
public:
    bool isValid(string s) {
        if (s.length() % 2) { // s 长度必须是偶数
            return false;
        }
        stack<char> st;
        for (char c : s) {
            if (mp.contains(c)) { // c 是左括号
                st.push(mp[c]); // 入栈对应的右括号
            } else { // c 是右括号
                if (st.empty() || st.top() != c) {
                    return false; // 没有左括号,或者左括号类型不对
                }
                st.pop(); // 出栈
            }
        }
        return st.empty(); // 所有左括号必须匹配完毕
    }
};

写法三

用 if-else 代替 mp。写法二三的核心思想就是:遍历到左括号就入栈相应的右括号,之后只需比较栈顶元素与s[i]是否相等即可。

class Solution {
public:
    bool isValid(string s) {
        if (s.length() % 2) { // s 长度必须是偶数
            return false;
        }
        stack<char> st;
        for (char c : s) {
            if (c == '(') {
                st.push(')'); // 入栈对应的右括号
            } else if (c == '[') {
                st.push(']');
            } else if (c == '{') {
                st.push('}');
            } else { // c 是右括号
                if (st.empty() || st.top() != c) {
                    return false; // 没有左括号,或者左括号类型不对
                }
                st.pop(); // 出栈
            }
        }
        return st.empty(); // 所有左括号必须匹配完毕
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 n 是 s 的长度。
  • 空间复杂度:O(n) 或 O(1)。如果能修改 s,那么直接把 s 当作栈,可以做到 O(1) 额外空间。

155. 最小栈 - 力扣(LeetCode)

本质是维护前缀最小值,简洁写法!

引入

给你一个数组 nums,如何计算每个前缀的最小值?

定义preMin[i]表示 nums[0] 到 nums[i] 的最小值。

这可以从左到右计算:

  • preMin[0]=nums[0]。
  • preMin[1]=min(nums[0],nums[1])。
  • preMin[2]=min(nums[0],nums[1],nums[2])=min(preMin[1],nums[2])
  • preMin[3]=min(nums[0],nums[1],nums[2],nums[3])=min(preMin[2],nums[3])
  • ……

一般地,我们有

preMin[i]=min(preMin[i−1],nums[i])

回到本题

nums 视作栈,本题相当于在 nums 的末尾动态地添加/删除元素。

  • 栈中除了保存添加的元素,还保存前缀最小值。(栈中保存的是 pair)
  • 添加元素:设当前栈的大小是 n。添加元素 val 后,额外维护 preMin[n]=min(preMin[n−1],val),其中 preMin[n−1] 是添加 val 之前,栈顶保存的前缀最小值。
  • 删除元素:弹出栈顶即可。

细节

一开始栈为空(n=0),添加 val 时,我们没有对应的 preMin[n−1]。需要特判栈为空的情况吗?

不需要。初始化的时候,在栈底加一个 ∞ 哨兵,作为 preMin[−1]

注意题目保证 pop,top,getMin 都是在非空栈上操作的。

class MinStack {
    stack<pair<int, int>> st;

public:
    MinStack() {
        // 添加栈底哨兵 INT_MAX
        // 这里的 0 写成任意数都可以,反正用不到
        st.emplace(0, INT_MAX);
    }

    void push(int val) {
        st.emplace(val, min(getMin(), val)); 
    }

    void pop() {
        st.pop();
    }

    int top() {
        return st.top().first;
    }

    int getMin() {
        return st.top().second;
    }
};
//复杂度分析
//时间复杂度:所有操作均为 O(1)。
//空间复杂度:O(q)。其中 q 是 push 调用的次数。最坏情况下,只有 push 操作,需要 O(q) 的空间保存元素。

394. 字符串解码 - 力扣(LeetCode)

这题看到括号的匹配,首先应该想到的就是用栈来解决问题。

其次,读完题目,要我们类似于制作一个能使用分配律的计算器。想象:如3[a2[c]b] 使用一次分配律-> 3[accb] 再使用一次分配律->accbaccbaccb

注释:

#include <ctype.h>*//isalpha、isalnum、isdigit、islower、isupper*

  • isalpha()用来判断一个字符是否为字母
  • isalnum()用来判断一个字符是否为数字或者字母,也就是说判断一个字符是否属于a~ z||A~ Z||0~9。
  • isdigit() 用来检测一个字符是否是十进制数字0-9
  • islower()用来判断一个字符是否为小写字母,也就是是否属于a~z。
  • isupper()和islower相反,用来判断一个字符是否为大写字母。

定义于头文件 <cctype> 以上如果满足相应条件则返回非零,否则返回零。

class Solution {
public:
    string decodeString(string s) {
        string res = ""; // 存储当前正在处理的字符串
        stack<int> nums;  // 存储数字的栈,用于处理嵌套的重复次数
        stack<string> strs; // 存储字符串的栈,用于处理嵌套的字符串拼接
        int num = 0; // 当前累积的数字
        
        for (char c : s) { // 遍历输入字符串的每个字符
            if (isdigit(c)) { // 处理数字字符
                num = num * 10 + (c - '0'); // 累积数字,如"23"变为23
            } else if (isalpha(c)) { // 处理字母字符
                res += c; // 直接拼接到当前结果中
            } else if (c == '[') { // 遇到左括号,保存当前状态到栈中
                nums.push(num); // 当前数字压栈,用于之后的重复次数
                num = 0; // 重置数字
                strs.push(res); // 当前字符串压栈,用于之后的拼接
                res = ""; // 重置当前字符串,处理括号内的新内容
            } else if (c == ']') { // 遇到右括号,处理重复和拼接
                int times = nums.top(); // 取出栈顶的重复次数
                nums.pop();
                string temp = res; 
                res = strs.top(); // 取出栈顶的父级字符串
                strs.pop();
                for (int i = 0; i < times; i++) { // 将当前字符串重复times次
                    res += temp; // 拼接到父级字符串后
                }
            }
        }
        return res; // 返回最终解码结果
    }
};

样例解析:以输入 "3[a2[c]]" 为例

  1. 遍历到 '3'num 累积为3。
  2. 遍历到 '[':将 num=3 压入 nums 栈,res="" 压入 strs 栈。重置 num=0res=""
  3. 遍历到 'a'res 变为 "a"。
  4. 遍历到 '2'num 累积为2。
  5. 遍历到 '[':将 num=2 压入 nums 栈,res="a" 压入 strs 栈。重置 num=0res=""
  6. 遍历到 'c'res 变为 "c"。
  7. 遍历到 ']'
    • 取出 nums 栈顶的2,重复 "c" 2次得到 "cc"。
    • 取出 strs 栈顶的 "a",拼接后 res 变为 "a" + "cc" = "acc"。
  8. 遍历到 ']'
    • 取出 nums 栈顶的3,重复 "acc" 3次得到 "accaccacc"。
    • strs 栈顶为空字符串,最终 res 变为 "accaccacc"。

最终结果为 "accaccacc"

算法思路

  • 栈的应用:使用两个栈分别存储数字和字符串,处理嵌套结构。
  • 数字累积:遇到连续数字字符时,累积计算成整数。
  • 括号处理:左括号触发状态保存,右括号触发字符串重复和拼接。
  • 时间复杂度:O(N),每个字符处理一次,嵌套层数不影响总操作次数。
  • 空间复杂度:O(N),栈的空间与输入字符串的嵌套深度相关。

739. 每日温度 - 力扣(LeetCode)

从左到右, 更新为主, 去除为辅, 元素可重复。

从右到左, 去除为主, 更新为辅, 元素无重复。

image-20250317161214828

image-20250317161233861

写法一:从右到左

栈中记录下一个更大元素的「候选项」的下标。

每次循环,我们可以在「候选项」中找到答案。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        vector<int> ans(n);
        stack<int> st;
        for (int i = n - 1; i >= 0; i--) {
            int t = temperatures[i];
            while (!st.empty() && t >= temperatures[st.top()]) {
                st.pop();
            }
            if (!st.empty()) {
                ans[i] = st.top() - i;
            }
            st.push(i);
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
  • 空间复杂度:O(min(n,U)),其中 U=max(temperatures)−min(temperatures)+1。返回值不计入,仅考虑栈的最大空间消耗。

写法二:从左到右

栈中记录还没算出下一个更大元素的那些数的下标。

相当于栈是一个 todolist,在循环的过程中,现在还不知道答案是多少,在后面的循环中会算出答案。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        vector<int> ans(n);
        stack<int> st; // todolist
        for (int i = 0; i < n; i++) {
            int t = temperatures[i];
            while (!st.empty() && t > temperatures[st.top()]) {
                int j = st.top();
                st.pop();
                ans[j] = i - j;
            }
            st.push(i);
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
  • 空间复杂度:O(n)。注意这种写法栈中可以有重复元素。

84. 柱状图中最大的矩形 - 力扣(LeetCode)

单调栈【基础算法精讲 26】

首先,面积最大矩形的高度一定是 heights 中的元素。这可以用反证法证明:假如高度不在 heights 中,比如 4,那我们可以增加高度直到触及某根柱子的顶部,比如增加到 5,由于矩形底边长不变,高度增加,我们得到了面积更大的矩形,矛盾,所以面积最大矩形的高度一定是 heights 中的元素。

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

  • 在 i 左侧的小于 h 的最近元素的下标 left,如果不存在则为 −1。求出了 left,那么 left+1 就是矩形最左边那根柱子。

  • 在 i 右侧的小于 h 的最近元素的下标 right,如果不存在则为 n。求出了 right,那么 right−1 就是矩形最右边那根柱子。

比如示例 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。

枚举 i,计算对应的矩形面积,更新答案的最大值。

如何快速计算 leftright?这可以用单调栈求出。

class Solution {
public:
    int largestRectangleArea(vector<int> &heights) {
        int n = heights.size();
        vector<int> left(n, -1);   // 存储每个柱子左边第一个比它矮的柱子的索引
        vector<int> right(n, n);   // 存储每个柱子右边第一个比它矮的柱子的索引
        stack<int> st;             // 单调递增栈,用于维护最近的较小元素索引
        
        // 从左向右遍历,确定每个柱子的左边界
        for (int i = 0; i < n; i++) {
            // 弹出栈顶比当前柱子高的元素,保持栈的单调性
            while (!st.empty() && heights[i] <= heights[st.top()]) {
                st.pop();
            }
            // 栈顶元素即为当前柱子左侧第一个更矮的柱子
            left[i] = st.empty() ? -1 : st.top(); 
            st.push(i); // 当前柱子索引入栈
        }

        st = stack<int>(); // 清空栈,准备反向遍历
        
        // 从右向左遍历,确定每个柱子的右边界
        for (int i = n - 1; i >= 0; i--) {
            // 同样维护单调递增栈
            while (!st.empty() && heights[i] <= heights[st.top()]) {
                st.pop();
            }
            // 栈顶元素即为当前柱子右侧第一个更矮的柱子
            right[i] = st.empty() ? n : st.top(); 
            st.push(i);
        }

        // 计算最大矩形面积
        int max_area = 0;
        for (int i = 0; i < n; i++) {
            // 当前柱子能形成的最大宽度为 (右边界 - 左边界 - 1)
            int width = right[i] - left[i] - 1;
            max_area = max(max_area, heights[i] * width);
        }
        return max_area;
    }
};

样例解析:以输入 heights = [2,1,5,6,2,3] 为例

步骤一:确定左边界(从左到右遍历)

  • i=0(高度2):
    • 栈空 → left[0] = -1,压入0。
  • i=1(高度1):
    • 弹出栈顶0(2≥1)→ 栈空 → left[1] = -1,压入1。
  • i=2(高度5):
    • 栈顶1(1≤5)→ 不弹出 → left[2] = 1,压入2。
  • i=3(高度6):
    • 栈顶2(5≤6)→ 不弹出 → left[3] = 2,压入3。
  • i=4(高度2):
    • 弹出栈顶3(6≥2)→ 弹出栈顶2(5≥2)→ 弹出栈顶1(1≤2)→ 停止。
    • left[4] = 1,压入4。
  • i=5(高度3):
    • 栈顶4(2≤3)→ 不弹出 → left[5] = 4,压入5。

左边界结果left = [-1, -1, 1, 2, 1, 4]


步骤二:确定右边界(从右到左遍历)

  • i=5(高度3):
    • 栈空 → right[5] = 6,压入5。
  • i=4(高度2):
    • 弹出栈顶5(3≥2)→ 栈空 → right[4] = 6,压入4。
  • i=3(高度6):
    • 弹出栈顶4(2≤6)→ 栈空 → right[3] = 6,压入3。
  • i=2(高度5):
    • 弹出栈顶3(6≥5)→ 栈空 → right[2] = 6,压入2。
  • i=1(高度1):
    • 弹出栈顶2(5≥1)→ 弹出栈顶4(2≥1)→ 弹出栈顶5(3≥1)→ 栈空。
    • right[1] = 6,压入1。
  • i=0(高度2):
    • 弹出栈顶1(1≤2)→ 不弹出 → right[0] = 1,压入0。

右边界结果right = [1, 6, 6, 6, 6, 6]


步骤三:计算每个柱子的面积

  • i=0:宽度 1 - (-1) - 1 = 1 → 面积 2*1 = 2
  • i=1:宽度 6 - (-1) - 1 = 6 → 面积 1*6 = 6
  • i=2:宽度 6 - 1 - 1 = 4 → 面积 5*4 = 20
  • i=3:宽度 6 - 2 - 1 = 3 → 面积 6*3 = 18
  • i=4:宽度 6 - 1 - 1 = 4 → 面积 2*4 = 8
  • i=5:宽度 6 - 4 - 1 = 1 → 面积 3*1 = 3

最大面积:20(来自柱子高度5,覆盖索引2到5的区域)


算法关键点

  • 单调栈:维护一个单调递增栈,快速找到左右第一个更小的元素。
  • 左右边界left[i]right[i]分别表示柱子i的左右边界,矩形宽度为right[i] - left[i] - 1
  • 时间复杂度:O(n),每个元素入栈、出栈各一次。
  • 空间复杂度:O(n),存储左右边界和栈的空间。

法二:

有一种优化的单调栈方法,可以在一次遍历中处理,不需要显式地存储left和right数组

。这种方法的核心思想是在栈中保存一个递增的柱子序列,当遇到一个比栈顶小的柱子时,就计算以栈顶柱子为高的矩形面积。这时候,右边界就是当前遍历到的位置,左边界则是栈顶的下一个元素(因为栈是递增的,所以栈顶下一个元素就是左边第一个更小的)。这样可以在一次遍历中处理所有可能的矩形,而无需提前计算左右边界。

那么因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!

其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度

主要就是分析清楚如下三种情况:当前遍历的元素heights[i]与栈顶元素heights[st.top()]的大小情况

  • 情况一:大于,入栈
  • 情况二:等于,栈顶出栈,新元素入栈(更不更换元素都行)
  • 情况三:小于,开始求面积

细节:

在 height数组上后,都加了一个元素0, 为什么这么做呢?

避免数组本身就是升序/降序的情况。

class Solution {
public:
 int largestRectangleArea(vector<int>& heights) {
     stack<int> st;
     heights.insert(heights.begin(), 0); // 数组头部加入元素0
     heights.push_back(0); // 数组尾部加入元素0
     st.push(0);
     int result = 0;
     for (int i = 1; i < heights.size(); i++) {
         while (heights[i] < heights[st.top()]) {//注意这里是while循环,等到比heights[i]大的全出栈后才入栈st.push(i)
             int mid = st.top();
             st.pop();
             int w = i - st.top() - 1;//(右边-左边-1)
             int h = heights[mid];
             result = max(result, w * h);
         }
         st.push(i);
     }
     return result;
 }
};
posted @ 2025-03-17 21:08  七龙猪  阅读(2)  评论(0)    收藏  举报
-->