说明:上一篇《 数据结构与算法 – 08 栈》介绍了栈的数据结构定义,特性,常见操作及其复杂度。学以致用才是硬道理,本篇在此基础上收集整理一些栈在软件工程的实际应用,以此加深对栈的理解。
1. 函数调用栈
函数调用栈是一个非常经典且普遍的应用。C,C++,Java等函数调用/方法调用的运行时内存过程的实现就是基于栈做的。线程运行时,操作系统会给其分配一块内存,这块内存被组织成“栈”的结构。在函数被调用时该函数相关的所有内容会被组织成一个“栈帧”。栈帧的结构如下:
- 函数的返回地址和参数
- 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 函数调用的上下文
- 栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部;
ebp 寄存器又被称为帧指针(Frame Pointer);
esp 寄存器又被称为栈指针(Stack Pointer);
敲重点:函数的调用和调用后的返回,其底层实现就是这些栈帧内容的入栈和出栈操作
。
思考: 为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?
用其它数据结构也可以。只不过,函数调用先进后出的特性,比如函数调用完毕后栈顶恢复到被调用函数,这都正好与栈特性匹配,所以用栈是最顺理成章的事情了。
参考:栈帧
2. 表达式求值
常用场景:编译器利用栈进行表达式求值。以 1 + 2*3 - 4 为例:
使用2个栈,一个用来保存操作数,一个用来保存运算符。详细操作如下:
从左往右遍历表达式,
(1) 遇到数值就压入操作数栈;
(2) 遇到运算符,先将该运算符与运算符栈顶的运算符比较;
(3) 若优先级高于栈顶运算符,则压入运算符栈;若优先级低于或等于栈顶运算符时,则栈顶运算符出栈,操作数栈顶2个操作数出栈,进行运算后的结果压入操作数栈。
(4) 继续 (2) 和 (3) 直到最后。
3. 检查表达式中的括号是否匹配
比如,{[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。此时我们只需要1个栈–用来保存左侧括号。我们从左往右扫描表达式,下面是判断表达式括号是否合法(匹配)的思路:
(1) 遇到左侧括号,入栈;
(2) 遇到右侧括号,则与栈顶左括号比较(先出栈):
若不匹配,说明表达式不合法,扫描结束;
若匹配则继续扫描,(扫描还未完毕)如果遇到栈为空,则不合法,扫描结束;
(3) 扫完后,若栈为空,则合法;若栈不为空,则说明表达式也不合法。