
。
- 后弹出的是**左操作数** 。
。
OPND 栈。栈在这里的作用是临时存储和调度操作数的执行顺序,完美体现了 LIFO 在调度中的作用。
求值上面转换得到的后缀表达式:
步骤 | 输入元素 | 操作数栈 | 运算 / 结果 | 解释 |
|---|---|---|---|---|
1 | 9 | 9 | - | 9 入栈。 |
2 | 3 | 9, 3 | - | 3 入栈。 |
3 | 2 | 9, 3, 2 | - | 2 入栈。 |
4 | * | 9, 6 | 3 ∗ 2 = 6 3 * 2 = 6 3∗2=6 | 弹出 2, 3。计算 3 ∗ 2 3 * 2 3∗2,结果 6 入栈。 |
5 | 5 | 9, 6, 5 | - | 5 入栈。 |
6 | - | 9, 1 | 6 − 5 = 1 6 - 5 = 1 6−5=1 | 弹出 5, 6。计算 6 − 5 6 - 5 6−5,结果 1 入栈。 |
7 | 1 | 9, 1, 1 | - | 1 入栈。 |
8 | / | 9, 1 | 1 / 1 = 1 1 / 1 = 1 1/1=1 | 弹出 1, 1。计算 1 / 1 1 / 1 1/1,结果 1 入栈。 |
9 | + | 10 | 9 + 1 = 10 9 + 1 = 10 9+1=10 | 弹出 1, 9。计算 9 + 1 9 + 1 9+1,结果 10 入栈。 |
10 | 结束 | 10 | 最终结果:10 | 栈中只剩 10。 |
弹出 2, 3。计算
,结果 6 入栈。55[9, 6, 5]-5 入栈。6-[9, 1]
弹出 5, 6。计算
,结果 1 入栈。71[9, 1, 1]-1 入栈。8/[9, 1]
弹出 1, 1。计算
,结果 1 入栈。9+[10]
弹出 1, 9。计算
,结果 10 入栈。10结束[10]最终结果:10栈中只剩 10。
通过栈的 LIFO 特性,后缀表达式的求值流程变得线性且高效,无需回溯和优先级判断,这是栈在编译器和计算器设计中至关重要的应用。
括号匹配(如 {}, [], () 的配对)是一个典型的 LIFO 问题:最近遇到的左括号,必须是第一个被匹配的右括号。
(, {, [):将其压入栈。这代表我们“记下”了一个等待匹配的起始点。), }, ]): 这个简单的算法完美模拟了编译器或解释器在语法分析(Parsing)阶段对代码结构的检查。这种 LIFO 机制确保了结构的嵌套正确性。
这是栈在计算机系统中最重要、最隐蔽的应用。
每一次函数调用(包括递归调用)的背后,都是由**程序运行时栈(Call Stack)**在默默支撑。这里可以参考我之前的函数栈帧博客
当一个函数
调用另一个函数
时:
在调用栈上分配一块内存,称为**栈帧(Stack Frame)**或活动记录(Activation Record)。
运行所需的所有上下文信息,包括:
- **返回地址**:函数 执行完毕后,程序应该回到函数
的哪一行继续执行。
- **参数**:调用 时传递给它的值。
- **局部变量**:函数 内部声明的变量。
- **保存的寄存器状态**:确保 和
不会相互干扰。
当函数
返回时,它的栈帧会被弹出,程序根据栈帧中保存的返回地址回到函数
的调用点继续执行。
递归(Recursion)是函数调用栈最直观的体现。一个函数在自己的执行过程中调用自己。
系统风险:如果递归没有正确的终止条件,或终止条件永远无法满足,调用栈会不断增长,最终耗尽系统分配的栈空间,导致著名的 栈溢出(Stack Overflow) 错误。
在后续学到数据结构中的图论和树结构中,**深度优先搜索(DFS)**是一种重要的遍历策略。
当访问到一个节点
时:
入栈(记录该节点)。
的所有邻接节点/子节点。
,并递归/迭代地从
开始搜索。
的所有邻接节点都访问完毕后,将
出栈(表示以
为根的子树/子图已搜索完毕),然后回溯到上一个节点。
手动栈确保了算法可以正确地回溯到最近访问过的、且还有未探索分支的节点,完全体现了栈的 LIFO 逻辑。
这里就是一些平时常常使用的操作,没想到与栈也息息相关吧。
应用场景 | 栈的角色与作用 | LIFO体现 |
|---|---|---|
浏览器历史 | 维护前进/后退的页面链接栈。 | 最近访问的页面,是点击“后退”时第一个返回的。 |
文本编辑器 | 维护撤销操作序列。 | 最近的操作Last-In,是第一个被撤销的 First-Out。 |
内存管理 | 程序的运行时栈 Call Stack。 | 函数调用是层层嵌套的,最后被调用的函数必须最先返回。 |
栈作为一种数据结构,它所蕴含的设计思想远比其 LIFO 规则本身更为深刻,它体现了计算领域中“约束即力量”的哲学。
栈的核心特性在于其对操作的强制约束:元素只能在栈顶进行存取。
栈(Stack) | 线性表(List) |
|---|---|
仅支持栈顶操作(Push, Pop, Top)。 | 支持在任意位置的插入、删除、访问。 |
接口最小化,行为可预测且单一。 | 接口复杂,功能强大但容易出错。 |
正是这种极简的、不可妥协的约束,使得栈具有以下独特的优势:
),将所有核心操作的时间复杂度限制在了
(均摊)。
栈的美,在于它放弃了线性表在非特定场景下的所有灵活性,只为特定问题(LIFO问题)提供了最优解。
栈中的数据天然具有临时性(Temporality)。栈底元素比栈顶元素“老”,但它们被访问的机会却更少。这种结构使得栈非常适合处理那些生命周期短暂的数据和操作。
例如在函数调用栈中:局部变量和参数只在函数执行的临时时间窗口内有效。一旦函数返回,它们所属的栈帧就被销毁。
这种临时性/局部性的特性,与计算机体系结构中的缓存(Cache)设计理念高度契合。数组实现的顺序栈,由于其连续的内存分配,天然地提高了空间局部性,使得栈帧的加载和存取效率极高。栈可以说是为现代硬件执行模型量身定制的内存管理方式之一。
除了经典的算法应用,栈在现代计算机系统、操作系统和高级编程语言的运行时环境中,扮演着不可替代的角色。
在多线程或多进程的并发环境中,每个独立的执行流(如一个线程)都会拥有自己私有的、独立的调用栈(即线程栈)。
这进一步强化了栈的局部性和临时性设计思想。只有需要共享的数据(如堆上的对象或全局变量)才需要同步机制,而栈上的数据由于其生命周期短且私有,使得线程并发得以高效实现。
我们前面讨论了中缀转后缀。在更复杂的编译器或解释器中,栈的应用远超于此:
iadd**)会从操作数栈中取出操作数,计算结果后再压回栈中。这是一种零地址指令集**架构,使得字节码指令集更紧凑、易于传输和解释。在支持闭包(Closure)的高级语言(如 JavaScript, Python, Swift)中,栈的概念被延伸到**环境(Environment)**的捕获。
栈,作为计算机科学中最古老、最基础的数据结构之一,其魅力在于其极简的 LIFO 约束。正是这种约束,将其功能聚焦于解决那些具有严格时间逆序依赖和嵌套结构的问题。
栈不仅仅是一种数据结构,它更是计算机科学中**“简洁、优雅、高效”**的设计哲学的完美体现。它教会我们:对自由施加适当的限制,往往能换来结构上的清晰、逻辑上的严谨,以及性能上的巨大提升。
在未来,无论是面对更复杂的并发模型、更深层的编译器优化,还是更抽象的函数式编程范式,栈的 LIFO 原则将始终是计算模型中不可动摇的基石。掌握栈,就是掌握了计算思维的核心脉络之一。