沒有任何方法,除非你才華橫溢。
——艾略特
js實現 - 逆波蘭式
2019-05-26 by 文科生
最近編譯原理實驗有涉及到逆波蘭式,而且聽聞有人在前端面試過程中被問到逆波蘭式算法的實現,之前的離散數學課程中也有涉及到逆波蘭式,作為一名前端人員,終於按耐不住想用js去實現求逆波蘭式的算法。我查閱了大量的資料,發現有的算法雖然基本實現了對應的功能,但在細節處理方面略顯不妥;而有的算法寫的過於龐雜,想要完全讀懂則代價昂貴,而且代碼的臃腫不是因為算法本身復雜,而是加入了一些算符,這對理解算法本質是不利的。於是我干脆自己用js寫一遍該算法,目的是為了整理自己的思想,將該算法的本質呈現給位前端愛好者,希望大家面試順利!本文將實現:
- 普通算術表達式轉換為逆波蘭式
- 求逆波蘭式的值
上圖demo可以去本人github下載
一、逆波蘭式定義
將運算對象寫在前面,而把運算符號寫在后面。用這種表示法表示的表達式也稱做后綴式。逆波蘭式的特點在於運算對象順序不變,運算符號位置反映運算順序。
二、算法描述
根據普通算術表達式求逆波蘭式:
- 首先構造一個運算符棧,此運算符在棧內遵循越往棧頂優先級越高的原則。
- 讀入一個用中綴表示的簡單算術表達式,為方便起見,設該簡單算術表達式的右端多加上了優先級最低的特殊符號“#”。
- 從左至右掃描該算術表達式,從第一個字符開始判斷,如果該字符是數字,則分析到該數字串的結束並將該數字串直接輸出。
- 如果不是數字,該字符則是運算符,此時需比較優先關系。
做法如下:將該字符與運算符棧頂的運算符的優先關系相比較。如果,該字符優先關系高於此運算符棧頂的運算符,則將該運算符入棧。倘若不是的話,則將此運算符棧頂的運算符從棧中彈出,將該字符入棧。- 重復上述操作,直至掃描完整個簡單算術表達式,確定所有字符都得到正確處理,我們便可以將中綴式表示的簡單算術表達式轉化為逆波蘭表示的簡單算術表達式。
計算逆波蘭表達式的值:
- 構造一個棧,存放運算對象。
- 讀入一個用逆波蘭式表示的簡單算術表達式。
- 自左至右掃描該簡單算術表達式並判斷該字符,如果該字符是運算對象,則將該字符入棧。若是運算符,如果此運算符是二目運算符,則將對棧頂部的兩個運算對象進行該運算,將運算結果入棧,並且將執行該運算的兩個運算對象從棧頂彈出。
- 重復上述操作直至掃描完整個簡單算術表達式的逆波蘭式,確定所有字符都得到正確處理,我們便可以求出該逆波蘭算術表達式的值。
三、核心代碼
// 適用於無符整數四則運算, 但運算結果可能是負數,如減法
(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;
}
}());
四、總結
- 以上代碼只支持實現四則運算,因為我在此只是想呈現算法的本質,如果要深入,那么就要考慮算符的特征等問題了(單目運算,雙目運算等),這樣本文就變得難以閱讀了。
- 注意我沒有把左、右括號看成是算符,雖然將左括號壓入了棧中,但對左右括號有單獨的處理方式。
- 由於本人學識有限,難免有錯誤之處,歡迎各位批評指導。