数据结构学习(栈与队列:四(栈的典型应用(延迟缓冲)))

栈的典型应用

延迟缓冲

在一些应用问题中,输入可分解为多个单元并通过迭代依次扫描处理,但过程中各步计算往往滞后于扫描的进度,需要等到必要的信息已经完整到一定程度之后,才能做出判断并实施计算的这类场合,而栈则可以扮演数据缓冲区的角色。

表达式求值

中缀表达式的求取便是一个延迟缓冲的问题。当程序扫描一个表达式并对其进行求取,无法在刚扫描之后就对表达式的部分进行求取,而是要判断一下某个运算符是否在此时可以进行运算,而这个判断完成时往往是滞后于扫描速度的。这时候就可以运用栈工具,比如,现在要求取下面这个表达式:
求取中缀表达式
我们自左向右扫描表达式,扫描到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转换前后操作数次序不变;而操作符只有在可以执行的时候才添加,也就是栈顶操作符优先级高于当前操作符,其实可以看成我们遇到了一个隐式的右括号,这时将操作符放到隐式右括号后面即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值