4.3.2 递归嵌套
4.3.2.1 栈混洗(倒来倒去)
4.3.2.2 括号匹配:
对源程序的语法检查是代码编译过程中重要而基本的一个步骤,而对表达式括号匹配的检查则又是语法检查中必需的一个环节。
算法:递归实现(recursion) 空间消耗:O(n2)(最坏情况下divide()需要线性时间,且递归深度为O(n))
0001 void trim ( const char exp[], int& lo, int& hi ) { //删除exp[lo, hi]不含括号的最长前缀、后缀
0002 while ( ( lo <= hi ) && ( exp[lo] != '(' ) && ( exp[lo] != ')' ) ) lo++; //查找第一个和
0003 while ( ( lo <= hi ) && ( exp[hi] != '(' ) && ( exp[hi] != ')' ) ) hi--; //最后一个括号
0004 }
0005
0006 int divide ( const char exp[], int lo, int hi ) { //切分exp[lo, hi],使exp匹配仅当子表达式匹配
0007 int crc = 1; //crc为[lo, mi]范围内左、右括号数目之差
0008 while ( ( 0 < crc ) && ( ++lo < hi ) ) //逐个检查各字符,直到左、右括号数目相等,或者越界
0009 if ( exp[lo] == '(' ) crc ++;
0010 else if ( exp[lo] == ')' ) crc --;
0011 return lo;
0012 }
0013
0014 bool paren ( const char exp[], int lo, int hi ) { //检查表达式exp[lo, hi]是否括号匹配(递归版)
0015 trim ( exp, lo, hi ); if ( lo > hi ) return true; //清除不含括号的前缀、后缀
0016 if ( ( exp[lo] != '(' ) || ( exp[hi] != ')' ) ) return false; //首、末字符非左、右括号,则必不匹配
0017 int mi = divide ( exp, lo, hi ); //确定适当的切分点
0018 return paren ( exp, lo + 1, mi - 1 ) && paren ( exp, mi + 1, hi ); //分别检查左、右子表达式
0019 }
算法:迭代实现(iteration) 空间消耗:O(n)
0001 bool paren ( const char exp[], int lo, int hi ) { //表达式括号匹配检查,可兼顾三种括号
0002 Stack<char> S; //使用栈记录已发现但尚未匹配的左括号
0003 for ( int i = lo; i <= hi; i++ ) /* 逐一检查当前字符 */
0004 switch ( exp[i] ) { //左括号直接进栈;右括号若与栈顶失配,则表达式必不匹配
0005 case '(': case '[': case '{': S.push ( exp[i] ); break;
0006 case ')': if ( ( S.empty() ) || ( '(' != S.pop() ) ) return false; break;
0007 case ']': if ( ( S.empty() ) || ( '[' != S.pop() ) ) return false; break;
0008 case '}': if ( ( S.empty() ) || ( '{' != S.pop() ) ) return false; break;
0009 default: break; //非括号字符一律忽略
0010 }
0011 return S.empty(); //最终栈空,当且仅当匹配
0012 }
4.3.3 延迟缓冲
在一些应用问题中,输入可分解为多个单元并通过迭代依次扫描处理,但过程中的各步计算往往滞后于扫描的进度,需要待到必要的信息已完整到一定程度之后,才能作出判断并实施计算。在这类场合,栈结构则可以扮演数据缓冲区的角色。
优先级表:
0001 #define N_OPTR 9 //运算符总数
0002 typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; //运算符集合
0003 //加、减、乘、除、乘方、阶乘、左括号、右括号、起始符与终止符
0004
0005 const char pri[N_OPTR][N_OPTR] = { //运算符优先等级 [栈顶] [当前]
0006 /* |-------------------- 当 前 运 算 符 --------------------| */
0007 /* + - * / ^ ! ( ) \0 */
0008 /* -- + */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
0009 /* | - */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
0010 /* 栈 * */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
0011 /* 顶 / */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
0012 /* 运 ^ */ '>', '>', '>', '>', '>', '<', '<', '>', '>',
0013 /* 算 ! */ '>', '>', '>', '>', '>', '>', ' ', '>', '>',
0014 /* 符 ( */ '<', '<', '<', '<', '<', '<', '<', '=', ' ',
0015 /* | ) */ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
0016 /* -- \0 */ '<', '<', '<', '<', '<', '<', '<', ' ', '='
0017 };
表达式的求值及RPN转换(代码4.7):
0001 double evaluate ( char* S, char* RPN ) { //对(已剔除白空格的)表达式S求值,并转换为逆波兰式RPN
0002 Stack<double> opnd; Stack<char> optr; //运算数栈、运算符栈
0003 optr.push ( '\0' ); //尾哨兵'\0'也作为头哨兵首先入栈
0004 while ( !optr.empty() ) { //在运算符栈非空之前,逐个处理表达式中各字符
0005 if ( isdigit ( *S ) ) { //若当前字符为操作数,则
0006 readNumber ( S, opnd ); append ( RPN, opnd.top() ); //读入操作数,并将其接至RPN末尾
0007 } else //若当前字符为运算符,则
0008 switch ( priority ( optr.top(), *S ) ) { //视其与栈顶运算符之间优先级高低分别处理
0009 case '<': //栈顶运算符优先级更低时
0010 optr.push ( *S ); S++; //计算推迟,当前运算符进栈
0011 break;
0012 case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
0013 optr.pop(); S++; //脱括号并接收下一个字符
0014 break;
0015 case '>': { //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
0016 char op = optr.pop(); append ( RPN, op ); //栈顶运算符出栈并续接至RPN末尾
0017 if ( '!' == op ) //若属于一元运算符
0018 opnd.push ( calcu ( op, opnd.pop() ) ); //则取一个操作数,计算结果入栈
0019 else { //对于其它(二元)运算符
0020 double pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); //取出后、前操作数
0021 opnd.push ( calcu ( pOpnd1, op, pOpnd2 ) ); //实施二元计算,结果入栈
0022 }
0023 break;
0024 }
0025 default : exit ( -1 ); //逢语法错误,不做处理直接退出
0026 }//switch
0027 }//while
0028 return opnd.pop(); //弹出并返回最后的计算结果
0029 }
不同优先级的处置(见代码):
- 若当前运算符的优先级更高,则optr中的栈顶运算符尚不能执行
- 反之,一旦栈顶运算符的优先级更高,则可以立即弹出并执行对应的运算
- 当前运算符与栈顶运算符的优先级“相等”:脱括号并接受下一字符。
4.3.4 逆波兰表达式
逆波兰表达式(reverse Polish notation, RPN)是数学表达式的一种,其语法规则可概括为:操作符紧临于对应(最后一个)操作数之后。
RPN表达式亦称作后缀表达式(postfix),原表达式则称作中缀表达式(infix)。尽管RPN表达式不够直观易读,但其对运算符优先级的表述能力,却毫不逊色于常规的中缀表达式;而其在计算效率方面的优势,更是常规表达式无法比拟的:RPN表达式中运算符的执行次序,可更为简捷地确定,既不必在事先做任何约定,更无需借助括号强制改变优先级。具体而言,各运算符被执行的次序,与其在RPN表达式中出现的次序完全吻合。以上面的" 1 2 + 3 4 ^ * "为例,三次运算的次序{ +, ^, * },与三个运算符的出现次序完全一致。
实际上,代码4.7中evaluate()算法在对表达式求值的同时,也顺便完成了从常规表达式到RPN表达式的转换。在求值过程中,该算法借助append()函数,将各操作数和运算符适时地追加至串rpn的末尾,直至得到完整的RPN表达式。这里采用的规则十分简明:凡遇到操作数,即追加至rpn;而运算符只有在从栈中弹出并执行时,才被追加。
0001 void append ( char* rpn, double opnd ) { //将操作数接至RPN末尾
0002 char buf[64];
0003 if ( ( int ) opnd < opnd ) sprintf ( buf, "%6.2f \0", opnd ); //浮点格式,或
0004 else sprintf ( buf, "%d \0", ( int ) opnd ); //整数格式
0005 strcat ( rpn, buf ); //RPN加长
0006 }
0007
0008 void append ( char* rpn, char optr ) { //将运算符接至RPN末尾
0009 int n = strlen ( rpn ); //RPN当前长度(以'\0'结尾,长度n + 1)
0010 sprintf ( rpn + n, "%c \0", optr ); //接入指定的运算符
0011 }
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情”