栈的应用(括号匹配、表达式求值、汉诺塔)
栈的应用(括号匹配、表达式求值、汉诺塔)
要熟练掌握栈就必须学习栈的应用,把握其中的规律从而举一反三。本文将介绍栈在括号匹配、表达式求值和汉诺塔问题中的应用。
一、栈在括号匹配中的应用
1. 问题描述
对一个字符串的左右括号进行匹配,例如,字符串①(a*②(b+c③)+d④)在位置 ① 和 ② 有左括号,在位置 ③ 和 ④ 有右括号,位置 ① 的左括号与位置 ④ 的右括号匹配,位置 ② 的左括号与位置 ③ 的右括号匹配。而在字符串①(a+b②)③)④(中,位置 ③ 的右括号没有与之匹配的左括号,位置 ④ 的左括号没有与之匹配的右括号。
2. 求解的问题和策略
- 问题:编写一个 C++ 程序,输入的是一个字符串,输出的是匹配的括号的序号对以及不匹配的括号的序号。
- 策略:从左到右的扫描字符串,由于每一个右括号都与最近扫描的那个未匹配的左括号匹配,因此我们将扫描到的左括号保存到栈中,每当扫描到一个右括号,就将它与栈顶的左括号(如果存在)相匹配,并将匹配的左括号从栈顶删除。
3. C++实现
void printMatchedPairs(string key){//括号匹配
arrayStack<int> s;
int length = (int) key.size();
//扫描表达式key,寻找左括号和右括号
for(int i = 0; i < length; i++){
if(key.at(i) == '('){s.push(i);}//.at(i)传回索引i所指的数据
else{
if(key.at(i) == ')'){
try{//若栈不为空
cout << s.top() << ' ' << i <<endl;
s.pop();//删除栈中匹配的左括号
}
catch(stackEmpty){//捕捉栈空异常,没有与之匹配的左括号
cout << "No match for right parenthesis at" << i <<endl;
}
}
}
}
//栈不为空,剩余栈中的左括号没有与之匹配的右括号
while(!s.empty()){
cout << "No match for left parenthesis at" << s.top() <<endl;
s.pop();
}
}
//代码的时间复杂度是O(n),其中n为输入表达式的长度
二、栈在表达式求值中的应用
1. 将中缀表达式转换为后缀表达式
中缀表达式(如 a+b )是一种符合人们思维习惯的算术表达式,而后缀表达式(如 ab+ )更容易被计算机解析和运算。同时,与前缀表达式(波兰式)或后缀表达式(逆波兰式)不同的是,中缀表达式需要利用括号来限制运算的先后次序,而前缀、后缀表达式中考虑了运算符的优先级,因此不需要括号。下面介绍三种将中缀表达式转换为后缀表达式的方法。
中缀表达式示例:A+B*(C-D)-E/F
1)表达式树法
中缀表达式 A+B*(C-D)-E/F 对应的后缀表达式是 ABCD-*+EF/- 。下图是中缀表达式对应的表达式树,可以看出后缀表达式与中缀表达式对应的表达式树的后序遍历序列是一致的。
因此我们可以通过画出中缀表达式对应的表达式树并写出它的后序遍历序列来计算出某中缀表达式的后缀表达式。
2)加括号法
- 将中缀表达式 A+B*(C-D)-E/F 按照运算符的运算顺序添上括号:((A+(B*(C-D)))-(E/F)) ;
- 依次将括号内的运算符移至对应括号的后面:((A(B(CD)-)*)+(EF)/)- ;
- 将左右括号都去除:ABCD-*+EF/- ,这就是后缀表达式。
3)借助栈法
- 从左到右遍历中缀表达式,遇到操作数时直接加入后缀表达式中;
- 遇到界限符时,若为 “(” 则直接入栈,若为 “)” 则依次弹出栈中的运算符并加入后缀表达式中,直到弹出 “(” 为止;
【注意:“(” 直接删除,不用加入后缀表达式中】 - 遇到运算符时,若其优先级高于除 “(” 外的栈顶运算符,则直接入栈,否则,从栈顶开始,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式中,直到遇到一个优先级低于它的运算符或遇到 “(” 时为止,之后将当前运算符入栈;
- 遍历完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式中。
2. 用栈实现后缀表达式求值
方法:从左往右依次扫描后缀表达式的每一项,若该项是操作数,则将其压入栈中;若该项是操作符 < op > ,则依次从栈中退出两个操作数 Y 和 X ,形成运算指令 X < op > Y ,并将计算结果压入栈中。当所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。
例如:用栈实现后缀表达式:ABCD-*+EF/- 求值的过程如下。
3. 例题
① 【2012 统考真题】已知操作符包括 ‘’+‘’ 、‘’-‘’ 、‘‘×’’ 、“÷” 、“(” 和 “)” 。将中缀表达式 a+b-a×((c+d)÷e-f)+g 转换为等价的后缀表达式 ab+acd+e÷f-×-g+ 时,用栈来存放暂时还不能确定运算次序的操作符。栈初始时为空时,转换过程中同时保存在栈中的操作符的最大个数是(A)。
A. 5
B. 7
C. 8
D. 11
② 【2014 统考真题】假设栈初始为空,将中缀表达式 a÷b+(c×d-e×f)÷g 转换为等价的后缀表达式的过程中,当扫描到 f 时,栈中的元素依次是(B)。
A. + ( × -
B. + ( - ×
C. ÷ + ( × - ×
D. ÷ + - ×
三、栈在递归中的应用(汉诺塔问题求解)
1. 问题描述
假设有 n 个碟子和 3 座塔,初始时所有碟子都从大到小的堆在塔 A 上,我们需要把碟子都移动到塔 B 上,可以借助塔 C ,每次移动一个碟子,而且任何时候都不能把大碟子压在小碟子上面。
2. 求解策略
- 使用递归求解汉诺塔问题: 首先需要将最大的碟子移到塔 B 的底部,因此需要把其余 n-1 个碟子移到塔 C ,然后把最大的碟子移到塔 B ;接下来是把塔 C 上的 n-1 个碟子移到塔 B 上,为此可以利用塔 B 和塔 A 。
【可以完全忽略塔 B 上已有的一个碟子,因为这个碟子比塔 C 上将要移过来的所有碟子都大,在它的顶上可以堆放任何一个碟子】 - 使用递归和栈结合求解汉诺塔问题:从每个塔上移走碟子是按照 LIFO(后进先出)的方式进行的,因此可以把每个塔表示成一个栈。三座塔在任何时候都总共有 n 个碟子,因此,如果使用链表形式的栈,只需申请 n 个元素空间;如果使用数组形式的栈,由于塔 A 和塔 B 的容量都必须是 n ,塔 C 的容量必须是 n-1 ,因此所需的总空间为 3n-1 。
【n 值较小时(如 n ≤ 30 ),数组形式的栈和链表形式的栈在空间需求上差别很小,但是数组形式的栈比链表形式的栈运行速度要快,因此选择使用数组形式的栈】
3. C++实现
- 使用递归的方法:
初始调用的语句是 towersOfHanoi(n, A, B, C) ,输出的是把碟子从塔 A 移到塔 B 的移动次序
//把塔A顶部的n个碟子移到塔B,用塔C作为中转站
void towersOfHanoi(int n, char A, char B, char C){
if(n > 0){
towersOfHanoi(n-1, A, C, B);//把塔A顶部的n-1个碟子移到塔C
//把最大的碟子从塔A移到塔B
cout << "Move top disk from tower" << A << "to top of tower" << B <<endl;
towersOfHanoi(n-1, C, B, A);//把塔C顶部的n-1个碟子移到塔B
}
}
- 使用递归和数组形式的栈结合的方法:
预处理程序创建三个堆栈 tower[1: 3] 用来储存三座塔的布局,所有碟子从 1(最小的碟子)到 n(最大的碟子)编号。初始布局是所有碟子都在 tower[1](即塔 A)中,其余两座塔为空,当每移动一个碟子后,三座塔的布局会被修改并显示在输出设备上
//全局变量,tower[1:3]分别表示塔 A、B、C
arrayStack<int> tower[4];
void MoveAndShow(int, int, int, int);
//预处理程序
void towerOfHanoi(int n){
for(int d = n; d > 0; d--){tower[1].push(d);}//初始化,将碟子d都加到塔A上
MoveAndShow(n, 1, 2, 3);
}
//把塔A顶部的n个碟子移到塔B,用塔C作为中转站
MoveAndShow(int n, int A, int B, int C){
if(n > 0){
MoveAndShow(n-1, A, C, B);//把塔A顶部的n-1个碟子移到塔C
int t = tower[A].top();
tower[A].pop();
tower[B].push(t);//把一个碟子从塔A的顶部移到塔B的顶部
showState();//显示三座塔的布局
MoveAndShow(n-1, C, B, A);//把塔C顶部的n-1个碟子移到塔B
}
}