js实现 - 逆波兰式

没有任何方法,除非你才华横溢。
——艾略特


js实现 - 逆波兰式

2019-05-26 by 文科生

最近编译原理实验有涉及到逆波兰式,而且听闻有人在前端面试过程中被问到逆波兰式算法的实现,之前的离散数学课程中也有涉及到逆波兰式,作为一名前端人员,终于按耐不住想用js去实现求逆波兰式的算法。我查阅了大量的资料,发现有的算法虽然基本实现了对应的功能,但在细节处理方面略显不妥;而有的算法写的过于庞杂,想要完全读懂则代价昂贵,而且代码的臃肿不是因为算法本身复杂,而是加入了一些算符,这对理解算法本质是不利的。于是我干脆自己用js写一遍该算法,目的是为了整理自己的思想,将该算法的本质呈现给位前端爱好者,希望大家面试顺利!本文将实现:

  • 普通算术表达式转换为逆波兰式
  • 求逆波兰式的值

上图demo可以去本人github下载

一、逆波兰式定义

将运算对象写在前面,而把运算符号写在后面。用这种表示法表示的表达式也称做后缀式。逆波兰式的特点在于运算对象顺序不变,运算符号位置反映运算顺序。

二、算法描述

根据普通算术表达式求逆波兰式:

  1. 首先构造一个运算符栈,此运算符在栈内遵循越往栈顶优先级越高的原则。
  2. 读入一个用中缀表示的简单算术表达式,为方便起见,设该简单算术表达式的右端多加上了优先级最低的特殊符号“#”。
  3. 从左至右扫描该算术表达式,从第一个字符开始判断,如果该字符是数字,则分析到该数字串的结束并将该数字串直接输出。
  4. 如果不是数字,该字符则是运算符,此时需比较优先关系。
    做法如下:将该字符与运算符栈顶的运算符的优先关系相比较。如果,该字符优先关系高于此运算符栈顶的运算符,则将该运算符入栈。倘若不是的话,则将此运算符栈顶的运算符从栈中弹出,将该字符入栈。
  5. 重复上述操作,直至扫描完整个简单算术表达式,确定所有字符都得到正确处理,我们便可以将中缀式表示的简单算术表达式转化为逆波兰表示的简单算术表达式。

计算逆波兰表达式的值:

  1. 构造一个栈,存放运算对象。
  2. 读入一个用逆波兰式表示的简单算术表达式。
  3. 自左至右扫描该简单算术表达式并判断该字符,如果该字符是运算对象,则将该字符入栈。若是运算符,如果此运算符是二目运算符,则将对栈顶部的两个运算对象进行该运算,将运算结果入栈,并且将执行该运算的两个运算对象从栈顶弹出。
  4. 重复上述操作直至扫描完整个简单算术表达式的逆波兰式,确定所有字符都得到正确处理,我们便可以求出该逆波兰算术表达式的值。

三、核心代码

// 适用于无符整数四则运算, 但运算结果可能是负数,如减法
(function () {
  'use strict'
  const rpn = {
    _precedence: {'/': 2, '*': 2, '-': 1, '+': 1, '#': 0},
    
    /**
     * operations
     * @private
     */
    _operation: {
      '+': (a, b) => (+a) + (+b),
      '-': (a, b) => (+a) - (+b),
      '*': (a, b) => (+a) * (+b),
      '/': (a, b) => (+a) / (+b)
    },

    /**
     * split expression to array
     * @private
     * @param exp - infix expression
     * @returns {Array|null}
     */
    _splitExp: function (exp) {
      return exp.match(/\d+|[^\d\s\t]/g);
    },

    /**
     * check a character, is or not an operator
     * @private
     * @param char - character
     * @return {boolean}
     */
    _isOperator: function (char) {
      return /^[\/\*\-\+#]$/.test(char);
    },

    /**
     * check character, is or not a bracket
     * @private
     * @param char - character
     * @retuens {boolean}
     */
    _isBracket: function (char) {
      return /^[\(\)]$/.test(char);
    },

    /**
     * check string, is or not a number
     * @private
     * @param str - character
     * @returns {boolean}
     */
    _isNumber: function (str) {
      return /^\d+$/.test(str);
    },

    /**
     * check exp, is or not a valid expression
     * @param {string} exp - expression 
     * @returns {boolean} - 
     */
    _isValidExpression: function (exp) { // 含有除数字、括号、操作符以外的符号即为非法
      return !/[^\d\s\t\+\-\*\/\(\)]/.test(exp);
    },

    /**
     * transfer infix expression to reverse polish notation
     * @param exp - infix express
     * @returns {string|null}
     */
    infix2rpn: function(exp) {
      if (!rpn._isValidExpression(exp)) return null;  // 用于保证以下处理的是合法的表达式

      var arrExp = rpn._splitExp(exp);  // 输入串分割
      var opStack = [];                 // 运算符栈
      var rpnStack = [];                // 存放逆波兰式结果
      
      arrExp = arrExp.concat('#');      // 加入最低优先级的算符 '#'
      
      var i,                        // 用于遍历arrExp
          item,                     // 遍历arrExp时暂存
          op,                       // 暂存opStack中的操作符
          len = arrExp.length;      // 记录arrExp长度
      for (i = 0; i < len; i ++) {
        item = arrExp[i];
        if (rpn._isNumber(item)) {
          rpnStack.push(item);
        } else if (rpn._isOperator(item)) {  
          while (opStack.length) {
            op = opStack[opStack.length-1];        // push性能低于pop和数组按索引取值,要尽量避免push
            if(op === '(') {                // 栈顶运算符是左括号,需单独处理
              break;
            } else if (rpn._precedence[item] > rpn._precedence[op]) { // 否则,栈顶是运算符。并且如果...
              // 当前算符优先级大于算符栈栈顶优先级
              break;
            } else {                    // 当前算符优先级小于等于算符栈栈顶优先级
              rpnStack.push(opStack.pop()); // 弹出算符栈栈顶算符并放入逆波兰式结果栈中
            }
          }
          opStack.push(item);           // 将运算符压入
        } else {                        // item是括号
          if (item === '(') {           // 是 '('
            opStack.push(item);
          } else  {  // 否则,item是 ')'
            while (opStack[opStack.length-1] !== '(') {
              rpnStack.push(opStack.pop());
            }                   // ')' 遇 '(' ,相抵消
            opStack.pop();
          }
        }
      } 
      return rpnStack.length ? rpnStack.join(' ') : null;
    },


    /**
     * calculate reverse polish notation - 本函数目前只支持二元运算
     * @param exp - reversed polish notation
     * @returns {number|null}
     */
    rpnCalculate: function (exp) {
      if (!rpn._isValidExpression(exp)) return null;  // 用于保证以下处理的是合法的表达式

      var arrExp = rpn._splitExp(exp);
      var calcStack = [];
      var item;                       // in arrExp
      var param1, param2;           // 运算对象

      var i, len = arrExp.length;
      for (i = 0; i < len; i ++) {
        item = arrExp[i];
        if (rpn._isNumber(item)) {
          calcStack.push(+item);    // 先将item转换为数值再压栈
        } else {                    // 否则item就是运算符
          param2 = calcStack.pop();
          param1 = calcStack.pop();
          calcStack.push(rpn._operation[item](param1, param2));// 执行运算并将结果压栈
        }
      }  
      return calcStack.pop();
    },

    /**
     * calculate expression
     * @param exp - expression string
     * @returns {number|null}
     */
    calculate: function (exp) {
      return rpn.rpnCalculate(rpn.infix2rpn(exp));
    }
  }
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = rpn;
  }

  if (typeof window !== 'undefined') {
    window.rpn = rpn;
  }
}());

四、总结

  1. 以上代码只支持实现四则运算,因为我在此只是想呈现算法的本质,如果要深入,那么就要考虑算符的特征等问题了(单目运算,双目运算等),这样本文就变得难以阅读了。
  2. 注意我没有把左、右括号看成是算符,虽然将左括号压入了栈中,但对左右括号有单独的处理方式。
  3. 由于本人学识有限,难免有错误之处,欢迎各位批评指导。

五、参考资源

javascript:逆波兰式表示法计算表达式结果

JavaScript中缀表达式转为逆波兰式(四则运算)

波兰式、逆波兰式与表达式求值

逆波兰表达式工具

猜你喜欢

转载自www.cnblogs.com/wen-k-s/p/10925987.html
今日推荐