最近项目中遇到一个问题,当面对一个表达式: -1+(2-0.3)*-4/5-6 的时候,该如何输出其计算结果,四则运算,这不简简单单直接eval冲他妈的
let str = '1+2*(3-4)/5'
eval(str) // 0.6
然后就发现,项目疯狂警告 : Use of eval is strongly discouraged.
好嘛 作为一个码怪(指用七行代码,输出了三千七百行报错的人),肯定不能用这么简单粗暴的方法搞定,但苦思冥想不明白该如何处理乘法除法和括号的优先级,最后还是去请求好歌们的帮助,在谷歌上发现,大家在说四则运算前,都会讲到另一个东西——波兰表达式
简述以下波兰式的作用:
-
将我们平时使用的 (1+2)*3 (中缀式)
-
转成另一种方式表达: * + 1 2 3(前缀式)
-
或是另一种 逆波兰表达式 : 1 2 + 3 * (后缀式)
后缀式和调度场算法
前缀式和后缀式都能无歧义的表现运算顺序,还能不需要括号的参与,而为了方便在代码中计算,我们(他们)一般都用后缀式,因为都是从左到右的二元运算,而将中缀式转为后缀式,我们则需要用到一个叫做调度场算法的东西,它大概是这么个意思:
- 声名一个后缀式队列,用来存放后缀表达式,生成一个符号栈,用来存放运算符
- 分割中缀式,然后遍历它
- 遇到数字,直接入后缀式队列
- 如果遇到运算符,符号栈为空则直接入符号栈
- 如果遇到运算符,符号栈不为空,则对比当前符号和栈顶符号,若当前符号优先级与栈顶符号优先级平级或低级,则栈顶运算符入后缀式队列,直到当前运算符优先级高于栈顶符后,入符号栈
- 如果遇到左括号(,入符号栈,左括号后的继续照前面的规则走
- 如果遇到右括号),则将符号栈的内容一个个弹出加入后缀式队列,直到遇到(为止,弹出左括号,此时左括号只出栈不入队列
- 当遍历结束后,如果符号栈中存在元素,则全部出栈进入后缀式队列,此时便得到一个完成转换的后缀式数组。
下面式维基上大家都贴的一张图我不贴好像不太合群,很直观
芭芭拉没卵用,直接上代码
确保输入的字符串为正确的中缀表达式
// 为了防止别人输入错误的中缀式,先做一个检查
// 检查输入得表达式是否正确
const calcCheck = str => {
// 去除 转换中文括号为英文括号
// 去除所有空格 文字 和非运算符号
// 将所有负数抓换为 (0-x)的方式,不然无法进行负数运算
// 他妈的正则真好用
return str
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/[^\d|\(|\)|\+|\-|\*|\|\./]*/g, '')
.replace(/\D(\-\d+(\.{1}\d+)|\-\d)/g, text => `${text[0]}(0${text.substring(1)})`);
};
规定各运算符的运算规则和运算优先级
// 运算符号 加减乘车 和 次方,priority为运算优先级,越大越优先
const calcSymbol = {
'+': {
fn: (x, y) => x + y,
priority: 1,
},
'-': {
fn: (x, y) => x - y,
priority: 1,
},
'*': {
fn: (x, y) => x * y,
priority: 2,
},
'/': {
fn: (x, y) => x / y,
priority: 2,
},
'^': {
fn: (x, y) => Math.pow(x, y),
priority: 3,
},
};
// 检查a运算符优先级是否低于或等于
const checkSymbol = (a, b) => {
if (!calcSymbol[a] || !calcSymbol[b]) return false;
let priorityA = calcSymbol[a].priority;
let priorityB = calcSymbol[b].priority;
return priorityA <= priorityB;
};
将中缀式转换为逆波兰式
const getPolandExpression = expression => {
// 后缀式队列
let resPoland = [];
// 符号栈
let symbolStack = [];
expression.forEach((v, i) => {
// 如果是运算符
if (calcSymbol[v]) {
// 对比符号栈
// 运算符比栈顶优先级低或相等则将运算符栈顶符号加入表达式队列
// 此符号此时如果依然比栈顶优先级低或相等,继续循环
// 直到不符条件再把此符号入栈
if (symbolStack.length) {
while (checkSymbol(v, symbolStack[0])) {
// 弹出栈顶 入队列
let s = symbolStack.shift();
resPoland.push(s);
}
}
// 如果是运算符 则进入符号栈
symbolStack.unshift(v);
} else if (v === '(') {
// 如果是左括号,一并压入符号栈
symbolStack.unshift(v);
} else if (v === ')') {
// 如果是右括号,开始把符号栈疯狂出栈,直到遇到左括号'('
let symbol = symbolStack.shift();
while (symbol !== '(') {
resPoland.push(symbol);
symbol = symbolStack.shift();
}
} else {
// 如果是数字则进入数字队列
resPoland.push(Number(v));
}
});
return [...resPoland, ...symbolStack];
};
计算逆波兰式的结果
// 以 -1+(2-0.3)*-4/5-6 为例
const calcStr = (str, decimal = 2) => {
// 检查表达式格式,确保只有括号加减乘除和数字,并且没有空格
// 表达式中的负数转换成了(0-x): -1+(2-0.3)*(0-4)/5-6
str = calcCheck(str);
if (!str) return false;
// 如果第一个字符为-,那说明这段表达式可能从负数开始,转换为 0-xxxxxx
if (str[0] === '-') {
// 0-1+(2-0.3)*(0-4)/5-6
str = '0' + str;
}
// 将表达式分割成数组
// ["0", "-", "1", "+", "(", "2", "-", "0.3", ")", "*", "(", "0", "-", "4", ")", "/", "5", "-", "6"]
let strArr = str.match(/\d+(\.\d+)|\d+|\+|\-|\*|\/|\(|\)/g);
// 逆波兰表达式
// [0, 1, "-", 2, 0.3, "-", 0, 4, "-", "*", 5, "/", "+", 6, "-"]
let reversePoland = getPolandExpression(strArr);
// 计算结果
let calcResults = [];
// 循环直至表达式没有值
while (reversePoland.length) {
// 读取逆波兰表达式的第一个值
let item = reversePoland.shift();
// 如果item式符号且结果集里至少又两个数字则开始运算
if (calcSymbol[item] && calcResults.length >= 2) {
// 取出计算结果的头两个数字
let y = calcResults.shift();
let x = calcResults.shift();
let res = calcSymbol[item].fn(x, y);
// 再把计算结果放会计算结果集的第一位
calcResults.unshift(res);
} else {
// 否则加入计算结果集
calcResults.unshift(item);
}
}
// 返回calcResults计算结果的第一个元素即是最终的计算结果
// 一个正确的波兰表达式,它的符号必定比数字少一个,且最后的运算接口集必定只有一个数字
// 前面的判断条件太麻烦,于是最终的结果集如果length不为1,那么说明表达式肯定错了
return calcResults.length === 1 ? calcResults[0].toFixed(decimal) : 0;
};
PS:
在最终计算中,保留小数部分用了toFixed,其实式不严谨的,因为toFixed并不是四舍五入,因为我懒目前项目对精度要求几乎没有,所以问题不大
关于精度的问题,推荐看另外两位大佬的文章: