栈的典型应用
延迟缓冲
在一些应用问题中,输入可分解为多个单元并通过迭代依次扫描处理,但过程中各步计算往往滞后于扫描的进度,需要等到必要的信息已经完整到一定程度之后,才能做出判断并实施计算的这类场合,而栈则可以扮演数据缓冲区的角色。
表达式求值
中缀表达式的求取便是一个延迟缓冲的问题。当程序扫描一个表达式并对其进行求取,无法在刚扫描之后就对表达式的部分进行求取,而是要判断一下某个运算符是否在此时可以进行运算,而这个判断完成时往往是滞后于扫描速度的。这时候就可以运用栈工具,比如,现在要求取下面这个表达式:
我们自左向右扫描表达式,扫描到4时,将其压入栈中,扫描到+号,也将其压入栈中;扫描到乘号时,因为此时乘号的优先级高于加号,所以栈中的加号并不进行相应的运算,而是将乘号继续压入栈中;一直扫描到3时,此时栈中最靠近栈顶的运算符是×号,继续往后扫描,发现后面一个运算符是‘“-”号,因为乘号的优先级低于x号,所以将乘号运算符号进行相应的计算,并将计算结构继续压入栈中,继续扫描到原来的那个减号,此时栈中最靠近栈顶的运算符是加号,因为加号在表达式的前面,即使减号与加号的优先级相同,野先计算加号运算符,并将得到的结果压入栈中,以此类推,直到扫描到最终的/0终结符号,此时如果中缀表达式是正确的话,栈中会剩下一个操作数,而这个操作数就是这个中缀表达式的结果。
关于运算符的执行次序(即运算优先级),一部分决定于事先约定的惯例(比如乘除优先于加减), 另一部分则则决定于括号。也就是说,仅根据表达式的某一前缀,并不能完全确定其中各运算符可以执行以及执行的次序,只有在获得足够多的后续信息才可以。
为了更加方便的对运算次序进行判断,这里这里我们设置两个栈,其中一个栈只装操作数,另一个栈只装操作符。
我们可以将不同运算符之间的优先级次序描述为一个二维表格:
#define N_OPTR 9 //运算符总数
typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; //运算符集合
//加、减、乘、除、乘方、阶乘、左括号、右括号、起始符与终止符
const char pri[N_OPTR][N_OPTR] = { //运算符优先等级 [栈顶] [当前]
/* |-------------------- 当 前 运 算 符 --------------------| */
/* + - * / ^ ! ( ) \0 */
/* -- + */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
/* | - */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
/* 栈 * */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
/* 顶 / */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
/* 运 ^ */ '>', '>', '>', '>', '>', '<', '<', '>', '>',
/* 算 ! */ '>', '>', '>', '>', '>', '>', ' ', '>', '>',
/* 符 ( */ '<', '<', '<', '<', '<', '<', '<', '=', ' ',
/* | ) */ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
/* -- \0 */ '<', '<', '<', '<', '<', '<', '<', ' ', '='
};
有了这张优先级表,我们就可以进行算法编写:
double evaluate ( char* S, char*& RPN ) { //对(已剔除白空格的)表达式S求值,并转换为逆波兰式RPN
Stack<double> opnd; Stack<char> optr; //运算数栈、运算符栈
optr.push ( '\0' ); //尾哨兵'\0'也作为头哨兵首先入栈
while ( !optr.empty() ) { //在运算符栈非空之前,逐个处理表达式中各字符
if ( isdigit ( *S ) ) { //若当前字符为操作数,则
readNumber ( S, opnd ); append ( RPN, opnd.top() ); //读入操作数,并将其接至RPN末尾
} else //若当前字符为运算符,则
switch ( orderBetween ( optr.top(), *S ) ) { //视其与栈顶运算符之间优先级高低分别处理
case '<': //栈顶运算符优先级更低时
optr.push ( *S ); S++; //计算推迟,当前运算符进栈
break;
case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
optr.pop(); S++; //脱括号并接收下一个字符
break;
case '>': { //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
char op = optr.pop(); append ( RPN, op ); //栈顶运算符出栈并续接至RPN末尾
if ( '!' == op ) { //若属于一元运算符
double pOpnd = opnd.pop(); //只需取出一个操作数,并
opnd.push ( calcu ( op, pOpnd ) ); //实施一元计算,结果入栈
} else { //对于其它(二元)运算符
double pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); //取出后、前操作数
opnd.push ( calcu ( pOpnd1, op, pOpnd2 ) ); //实施二元计算,结果入栈
}
break;
}
default : exit ( -1 ); //逢语法错误,不做处理直接退出
}//switch
}//while
return opnd.pop(); //弹出并返回最后的计算结果
}
该算法自左向右扫描中缀表达式,那些已扫描但不能处理的操作数和操作符分别压入栈opend、optr中,==一旦判定已缓存的子表达式优先级足够,便弹出相关的操作数和操作符,随即执行运算, 并将结果压入栈中。
算法中,我们首先是在操作符栈中压入\0操作符。
而后进行扫描,如果扫描到数字,则调用readNumber函数读取这部分数字并压入opend栈中,为什么不直接读取,是因为可能这个数字一个整数的十位数或者是更高位数,也可能是一个小数。
如果扫描到操作符,则将操作符栈optr中的栈顶元素于当前扫描到的操作符的优先级调用orderBewteen函数进行比较,
1. 如果栈顶运算符的优先级低于当前运算符,则将当前操作符压入optr栈中;比如pri[+][×] = ‘<’,表示栈顶操作符加号的优先级小于当前操作符乘号,而将乘号压入栈中。
2. 若栈顶运算符优先级高于当前操作符,比如pri[×][+] = ‘>’,则表示该栈顶操作符可以进行操作符,应该将其弹出做进一步处理:如果该操作符是一元操作符阶乘号(!),则将操作数opend栈中的一个栈顶元素弹出执行阶乘运算,并将结果压入栈中;若是其中二元操作符,则弹出opend栈中的前两个操作数,并执行运算。
3. 当栈顶操作符与当前操作符优先级相等,比如pri[(][)] = ‘=’, 此时将弹出“(”操作符,并继续处理“)”之后的操作符。不难看出,栈顶的左括号与当前的右括号一定是匹配的,它们的作用就是约束介乎两者之间的子表达式的优先级关系。当扫描到右括号时,实际括号中其它的操作符已经完成了它们的运算符,所以两个括号的使命也就完成。在优先级表格中,可以看到还有一种情况是优先级相等的,就是栈顶操作符和当前操作符都是“\0”,由于我们在算法启动之初就已经压入了一个“\0”,故在达到表达式的结束标识符“\0”时,即出现优先级相等情况。操作符栈optr的“\0”弹出。
逆波兰表达式
逆波兰表达式(Reverse Polish Notation,RPN)是数学表达式的一种,其语法规则可以概括为:操作符紧邻于对应的(最后一个)操作数之后。比如“1 2 +”即通常习惯的“1 + 2”。中缀表达式“(1+2)* 3 ^ 4”对应的后缀表达式为“1 2 + 3 4 ^ *”,其操作符在操作数后面,所以逆波兰表达式也被称作后缀表达式。有了后缀表达式,我们可以更方便的求其值,不需要括号即可表示带优先级的关系。
只需要一个栈即可:上图就是一个求解逆波兰表达式的过程,这里引入一个栈,然后将0压入栈,之后遇到阶乘操作符,这是将栈中的一个操作数弹出进行计算,将结果再压入栈中,之后继续扫描,将123压入栈,遇到加号操作符,之后弹出栈中的两个操作数,进行计算,将计算结果压入栈中。不难看出,后缀表达式计算时,只需按部就班的进行扫描,而后将操作数压入栈中,遇到操作符时候,不需判断操作符的优先级情况,只需的弹出需要的操作数即可。
手动转换
那么,怎么将一个中缀表达式转换为一个后缀表达式?一种方法是进行手工转换,假定下面一个中缀表达式:
(0!+ 1)* 2 ^ (3!+ 4) - (5!- 67-(8 + 9)
首先,我们假设在之前并未就操作符之间的优先级关系做过任何约定,所以我们只能通过添加括号来约束优先级。上面这个式子通过添加括号显示的指定运算次序:
((((0)!+ 1)* (2 ^ ((3)!+ 4))) - (((5)!- 67)-(8 + 9)))
之后将各运算符后移,使之紧邻其对应的右括号的右侧:
((((0)! 1)+(2 ^((3)! 4)+)^)* (((5)!67)-(8 9)+)-)-
最后一步,去掉所有的括号:
0!1+ 2 ^3! 4+^* 5!67-8 9+ - -
由以上步骤可见,操作数之间的相对次序,在转换前后保持不变;而操作符再PRN中所处的位置,恰好就是其对应的操作数均已就绪且该运算可以执行的位置。
自动转换
double evaluate ( char* S, char*& RPN ) { //对(已剔除白空格的)表达式S求值,并转换为逆波兰式RPN
Stack<double> opnd; Stack<char> optr; //运算数栈、运算符栈
optr.push ( '\0' ); //尾哨兵'\0'也作为头哨兵首先入栈
while ( !optr.empty() ) { //在运算符栈非空之前,逐个处理表达式中各字符
if ( isdigit ( *S ) ) { //若当前字符为操作数,则
readNumber ( S, opnd ); append ( RPN, opnd.top() ); //读入操作数,并将其接至RPN末尾
} else //若当前字符为运算符,则
switch ( orderBetween ( optr.top(), *S ) ) { //视其与栈顶运算符之间优先级高低分别处理
case '<': //栈顶运算符优先级更低时
optr.push ( *S ); S++; //计算推迟,当前运算符进栈
break;
case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
optr.pop(); S++; //脱括号并接收下一个字符
break;
case '>': { //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
char op = optr.pop(); append ( RPN, op ); //栈顶运算符出栈并续接至RPN末尾
if ( '!' == op ) { //若属于一元运算符
double pOpnd = opnd.pop(); //只需取出一个操作数,并
opnd.push ( calcu ( op, pOpnd ) ); //实施一元计算,结果入栈
} else { //对于其它(二元)运算符
double pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); //取出后、前操作数
opnd.push ( calcu ( pOpnd1, op, pOpnd2 ) ); //实施二元计算,结果入栈
}
break;
}
default : exit ( -1 ); //逢语法错误,不做处理直接退出
}//switch
}//while
return opnd.pop(); //弹出并返回最后的计算结果
}
实际上我们刚才写的代码中已经完成了RPN的转换,其中append ( RPN, opnd.top() )
和append ( RPN, op )
就是分别在逆波兰表达式中添加操作数与操作符,遇到操作数只要添加即可,因为在RPN转换前后操作数次序不变;而操作符只有在可以执行的时候才添加,也就是栈顶操作符优先级高于当前操作符,其实可以看成我们遇到了一个隐式的右括号,这时将操作符放到隐式右括号后面即可。