深入浅出了解栈——从基础到单调栈的应用

94 阅读6分钟

基本概念

定义:栈是一种抽象的数据类型,它遵循先进后出的原则。这意味着最后被添加到栈中的元素将首先被移除。

栈可以类比为一叠盘子,你只能从最上面添加或移除盘子。

栈的基本操作

栈支持以下基本操作:

  1. Push(入栈) :向栈中添加一个新元素。该操作将元素添加到栈顶。
  2. Pop(出栈) :从栈中移除栈顶的元素,并返回该元素。如果栈为空,则无法执行此操作。

所以每次需要pop的时候,要检查一下是否为空栈。

  1. Peek(查看栈顶元素) :查看栈顶元素但不移除它。如果栈为空,则没有元素可查看。
  2. Empty(检查是否为空) :判断栈是否为空。当栈中没有任何元素时返回真(true),否则返回假(false)。
  3. Size(获取栈大小) :返回栈中元素的数量。

栈在实际问题中的应用

  1. 括号匹配:用于验证代码或数学公式中的括号是否正确配对。
  2. 单调栈:用于寻找数组中每个元素右边或左边第一个比它大或小的元素。
  3. 最小栈:设计支持常数时间内获取最小值的栈。
  4. 回溯算法:解决迷宫问题、八皇后问题等,利用栈保存路径信息以便回溯。
  5. 函数调用堆栈:编程语言使用栈来管理函数调用,存储返回地址、局部变量等信息。
  6. 表达式求值与转换:如将中缀表达式转化为后缀表达式(逆波兰表示法),或者直接计算后缀表达式的值。

1. 括号匹配

题目:20.有效的括号

链接: leetcode.cn/problems/va…

理解题目,左括号必须用相同类型的右括号闭合,则说明字符串的长度是偶数,首先排除奇数的可能。

左括号必须以正确的顺序闭合,并且每个右括号都有一个对应的相同类型的左括号。 我们可以利用栈“先进后出”的特点来解决。

解决方案

具体步骤如下:

  1. 初始化一个空栈。

  2. 遍历字符串中的每一个字符:

    • 如果是左括号,则将其压入栈中。
    • 如果是右括号,检查栈顶元素是否为对应的左括号。如果是,则弹出栈顶元素;如果不是,则说明括号不匹配,返回false
  3. 最终如果栈为空,说明所有括号都匹配;否则,返回false

class Solution {
public:
    bool isValid(string s) {
        if(s.length() % 2 != 0) return false;
        stack<char> myStack;
        for(char ch : s){
            switch(ch){
                case '(': 
                    myStack.push(')');
                    break;
                case '[':
                    myStack.push(']');
                    break;
                case '{':
                    myStack.push('}');
                    break;
                case ')':
                    if(!myStack.empty()){
                        if(')' != myStack.top()) return false;
                        myStack.pop();
                        break;
                    }else{
                        return false;
                    }
                case ']':
                    if(!myStack.empty()){
                        if(']' != myStack.top()) return false;
                        myStack.pop();
                        break;
                    }else{
                        return false;
                    }
                case '}':
                    if(!myStack.empty()){
                        if('}' != myStack.top()) return false;
                        myStack.pop();
                        break;
                    }else{
                        return false;
                    }
                default: break;
            }
        }
        if(!myStack.empty()) return false; // 排除只有左边的括号情况
        return true;
    }
};

2. 单调栈

什么是单调栈?

单调栈是指栈内元素保持某种顺序(递增或递减)的栈。通过维护这种单调性,可以高效地解决问题,比如查找数组中每个元素右侧或左侧第一个更大或更小的元素

应用案例
  • 下一个更大元素 I 和 II:通过单调递减栈找到每个元素右侧的第一个更大元素。

下一个更大元素I: leetcode.cn/submissions…

下一个更大元素II: leetcode.cn/submissions…

  • 每日温度:对于给定的每天温度,找出至少需要等待多少天才能遇到更高温度。

leetcode.cn/submissions…

  • 商品折扣后的最终价格:基于相邻更低的价格为当前商品提供折扣。

leetcode.cn/submissions…

我们来讲解每日温度这题:

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

示例 2:

输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

示例 3:

输入: temperatures = [30,60,90]
输出: [1,1,0]
为什么使用单调栈?

解决这个问题时,我们可以利用单调递减栈从栈底到栈顶元素逐渐减少)来高效地找到每个温度对应的“下一个更高温度”的天数。通过这种方法,我们可以避免对每个元素进行两两比较,从而将时间复杂度优化至O(n),其中n是输入数组的长度。

单调栈的工作原理
  1. 遍历温度数组:从左向右遍历数组中的每一个温度。
  2. 维护单调性:使用栈来存储尚未找到下一个更高温度的那些天的索引,并保持栈内温度的单调递减特性。
  3. 更新结果:当遇到一个新温度时,如果它比栈顶元素(即之前某一天的温度)高,则说明找到了栈顶元素对应那天的“下一个更高温度”,计算等待天数并更新结果数组;同时,弹出栈顶元素,继续检查新的栈顶直到栈为空或当前温度不大于栈顶元素为止。
  4. 入栈操作:无论是否进行了弹出操作,最后都要将当前天的索引压入栈中,以维持栈的单调性
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        // 单调栈算法
        int n = temperatures.size();
        stack<int> indexStack; //存储索引
        vector<int> days(n,0); //初始化长度为n, 值都为0
        for(int i = 0; i < n; i++){
           while(!index.empty() && temperatures[i] > temperatures[indexStack.top()]){
                int preIndex = indexStack.top();
                indexStack.pop();
                days[preIndex] = i - preIndex;
           }
           index.push(i);
        }
        return days;
    }
};
代码解析
  • 初始化:首先初始化一个与temperatures等长的结果数组days,默认值设为0,因为如果没有找到比当天更高的温度,默认答案就是0。然后创建一个空栈indexStack用于存放尚未找到答案的那些天的索引。

  • 遍历与更新:遍历temperatures数组,对于每一天的温度:

    • 如果栈非空并且当前温度大于栈顶索引对应的温度,那么就找到了栈顶元素对应的那一天的答案,即i - prevIndex天后出现了更高的温度。
    • 弹出栈顶索引,继续检查栈顶直至条件不再满足。
    • 最后,无论是否有更新操作,都将当前索引压入栈中,以保持栈的单调性。

这种方法保证了每个元素最多只会被压入和弹出栈一次,因此整体的时间复杂度为O(n),非常适合处理大规模数据。