栈(Stack)是计算机科学中最基础也最强大的数据结构之一。它遵循后进先出(LIFO)原则,与我们日常生活中的堆叠盘子、浏览器后退按钮等场景高度契合。本文将带你从栈的核心特性出发,结合三个经典算法题(计算器、字符串解码、栈序列验证),深入剖析栈在编程中的应用技巧。无论你使用C++、Python还是JavaScript,都能从中获得启发。

1. 栈的核心特性与基础操作

栈是一种受限的线性表,只允许在表的一端(栈顶)进行插入和删除操作。它的核心特性可以用四个字概括:后进先出。这意味着最后压入栈的元素会最先被弹出。

常见操作

  • push:将元素压入栈顶
  • pop:移除栈顶元素
  • top/peek:获取栈顶元素但不移除
  • empty:判断栈是否为空

C++中,标准库提供了std::stack容器适配器,底层默认使用deque实现。如果你更倾向于手动控制,也可以像下面代码那样用vector模拟栈行为。

⚠️ 注意事项:栈的空间复杂度通常为O(n),如果递归深度过大,容易导致栈溢出。在Python中,使用列表模拟栈时,append()pop()的时间复杂度均为O(1),非常高效。

class Solution {
public:
    int calculate(string s) {
        //用数组来模拟栈
        vector st;
        int i=0,n=s.size();
        char op='+';
        while(i='0'&&s[i]<='9'){
                int tmp=0;
                while(i='0'&&s[i]<='9') tmp=tmp*10+(s[i++]-'0');
                if(op=='+') st.push_back(tmp);
                else if(op=='-') st.push_back(-tmp);
                else if(op=='*') st.back()*=tmp;
                else st.back()/=tmp;
            }else{
                op=s[i];
                i++;
            }
        }
        int sum=0;
        for(auto e: st){
            sum+=e;
        }
        return sum;
    }
};

上述代码展示了如何用数组模拟栈实现一个简易计算器。它通过延迟计算的策略,将加减乘除统一处理,避免了复杂的运算符优先级判断。在实际开发中,如果你使用TypeScriptJavaScript,也可以用数组的push()pop()完成类似逻辑。

2. 栈的经典应用:字符串解码

字符串解码是栈在解析与编译领域的典型应用。题目要求将形如3[a]2[bc]的字符串解码为aaabcbc。核心思想是:遇到数字时,将其压入数字栈;遇到左括号时,将当前累积的字符串压入字符串栈;遇到右括号时,弹出栈顶的字符串和数字,进行拼接。

关键点

  • 使用两个栈分别存储数字和字符串,避免混淆
  • 遇到字母时,持续累积直到遇到数字或括号
  • 右括号处理时,注意字符串拼接顺序:先弹出栈顶字符串,再重复k次,最后追加到上一级栈顶字符串

Go语言中,由于没有内置的栈类型,通常用切片模拟栈操作,与C++的vector思路相似。而在Python中,利用列表的append()pop()可以写出更简洁的代码。

class Solution {
public:
    string decodeString(string s) {
        stack nums;
        stack st;
        st.push("");
        int i=0,n=s.size();
        while(i='0'&&s[i]<='9'){
                int tmp=0;
                while(s[i]>='0'&&s[i]<='9'){
                    tmp=tmp*10+(s[i++]-'0');
                }
                nums.push(tmp);
            }else if(s[i]=='['){
                i++;
                string tmp="";
                while(s[i]>='a'&&s[i]<='z'){
                    tmp+=s[i++];
                }
                st.push(tmp);
            }else if(s[i]==']'){
                string tmp=st.top();
                st.pop();
                int k=nums.top();
                nums.pop();
                while(k--){
                    st.top()+=tmp;
                }
                i++;
            }else{
                string tmp;
                while(i='a'&&s[i]<='z'){
                    tmp+=s[i++];
                }
                st.top()+=tmp;
            }
        }
        return st.top();
    }
};

⚠️ 常见错误:忘记处理嵌套括号的情况。例如2[3[a]b],如果只用一个栈,很容易丢失外层信息。正确做法是每次遇到左括号都新建一个栈帧,确保内层解码结果能正确拼接到外层。

[AFFILIATE_SLOT_1]

3. 栈的验证问题:压入弹出序列

栈的验证问题考察的是对栈操作过程的理解。题目给定两个序列pushedpopped,判断能否通过栈操作实现这样的弹出顺序。核心思路是模拟压栈过程:依次将pushed中的元素压入栈,同时检查栈顶是否与popped当前指针指向的元素相等,如果相等则弹出并移动指针。

算法步骤

  1. 初始化一个空栈,指针i指向popped的起始位置
  2. 遍历pushed数组,将每个元素压入栈
  3. 每次压入后,检查栈顶是否等于popped[i],若相等则弹出i++
  4. 重复步骤2-3,直到所有元素处理完毕
  5. 最后判断i是否等于popped的长度

这个算法的时间复杂度为O(n),空间复杂度为O(n)。在JavaScript中,可以用数组的push()pop()轻松实现。如果你使用TypeScript,还可以加上类型注解,让代码更健壮。

class Solution {
public:
    bool validateStackSequences(vector& pushed, vector& popped) {
        int n=popped.size();
        stack result;
        int i=0;
        for(auto e: pushed){
            result.push(e);
            while(result.size()&&result.top()==popped[i]){
                result.pop();
                i++;
            }
        }
        return i==n;
    }
};

最佳实践:在实际面试或编码中,建议先画图模拟整个过程,再动手写代码。很多开发者容易忽略while循环的条件判断(如栈是否为空),导致空栈访问错误。另外,如果序列长度很大,可以用数组模拟栈来减少函数调用开销,提升性能。

4. 栈在不同语言中的实现对比

不同编程语言对栈的支持各有特色:

  • C++:标准库std::stack,底层默认deque,也可用vector模拟
  • Python:直接用listappend()pop()时间复杂度O(1)
  • JavaScript/TypeScript:数组的push()pop(),注意shift()是O(n)
  • Go:用切片模拟,append()slice[:len-1]

性能建议:在高频场景下,C++的vector模拟栈通常比std::stack更快,因为减少了包装层的开销。而Python和JavaScript虽然在语法上更简洁,但要注意递归深度限制(通常为1000层),避免栈溢出。

[AFFILIATE_SLOT_2]

总结

栈作为一种后进先出的数据结构,在表达式求值、括号匹配、深度优先搜索等场景中扮演着核心角色。本文通过计算器、字符串解码、栈序列验证三个经典案例,展示了栈在不同语言(C++、Python、JavaScript、TypeScript、Go)中的灵活运用。掌握栈的本质,不仅能提升算法能力,还能帮助你写出更高效、更简洁的代码。

核心要点回顾

  • 栈的LIFO特性决定了它的应用场景
  • 模拟栈操作时,注意边界条件(空栈、嵌套)
  • 不同语言实现栈的性能差异需因地制宜

希望这篇文章能帮你彻底吃透栈的用法,下次遇到类似问题时,能够得心应手!