数据结构 | 第4章 栈与队列(中)

46 阅读4分钟

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 }

image-20220701095840775.png

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 天,点击查看活动详情