这篇文章主要讲一下如何用JS实现一个算数表达式的求值方式,例如输入一个算数表达式字符串 ' 23 + 4 * 5 ',如何返回这个表达式的最终结果呢? 可能大家会觉得这个很简单,直接用Function或eval将表达式作为代码执行不就可以了吗?可能的实现如下:
function evalExpr(expr) {
let func = new Function(`return ${expr}`);
return func();
}
这篇文章主要是从算法层面上分析一个表达式的求值过程。
一、如何将一个算数表达式转化成一棵二叉树:
关于什么是二叉树,不熟悉的同学可以看一下相关的资料或数据;还是以表达式 23 + 4 * 5为例,转换成二叉树的结构如下:
从图中可以看出,每一个叶子节点(没有子节点)对应一个操作数,操作符对应的节点都不是叶子节点。
接下来通过代码看一下转换过程:
1、首先定义节点的类型,这个例子中节点可能有两种类型,操作数节点或者操作符节点
// 节点类型,操作符或操作数
const nodeType = {
operator: 0,
operand: 1
};
2、为二叉树中的单个节点建立数据模型,节点包含四个字段:值、类型、左子节点、有子节点;
// 二叉树的节点
function BTNode(value, type, left = null, right = null) {
this.value = value; // 节点的值,例如对于操作符节点,可能的取值为: + 、-、*、/
this.type = type; // 节点类型,取值为 nodeType.operator or nodeType.operand
this.left = left; // 左子节点
this.right = right; // 右子节点
}
3、接下来就是如何将一个表达式转换成二叉树了,简单起见,先不考虑表达式中有括号的情况:
首先讲一下具体的转换原理,还是以23 + 4 * 5为例,转化过程如下:
(1)将表达式转换成一个数组,[23, +, 4, * 5], 创建一个空的栈(即一个空的数组): const stack = [];
(2) 从左到右依次遍历每一个元素:
a、如果是一个操作数,创建一个新的操作数节点,接下来查看栈顶的元素是否是值为 * 或 / 的操作符节点,如果 是,则将新的操作树节点作为栈顶元素的右子节点,否则直接将新的节点压入栈中;
b、如果是一个操作符,则创建一个新的操作符节点,接下来将栈顶的元素取出,并作为新节点的左子节点,然后 将新的节点压入栈中;
(3)从左向右遍历栈中的节点,将栈中的每一个节点作为他前一个节点的右子节点;并返回第一个节点。
按照上述的过程,在转化 23 + 4 * 5的过程中,stack的结构变化一次如下:
const operReg = /[\+\-\*\/]/;
const isOper = t => operReg.test(t);
// 将字符串表达式转换成二叉树
function toBT(expr) {
const tokens = expr.replace( /[\+\-\*\/]/g, _ => `${_}`).trim()
.split(/s+/)
.map(t => t.trim());
const nodes = [];
for (let token of tokens) {
if (isOper(token)) {
const node = new BTNode(token, nodeType.operator, nodes.pop());
nodes.push(node);
} else {
const value = parseFloat(token);
if (Number.isNaN(value)) {
throw `Invalid express for '${token}'`;
}
const node = new BTNode(value, nodeType.operand);
const top = nodes[nodes.length - 1];
if (top && top.type === nodeType.operator && (top.value === '*' || top.value === '/')) {
top.right = node;
} else {
nodes.push(node);
}
}
}
for (let i = 0; i < nodes.length - 1; i++) {
const node = nodes[i];
node.right = nodes[i + 1];
}
return nodes[0];
}
4、接下来实现一下二叉树的遍历功能
遍历包括前序遍历、中序遍历、后序遍历;前序遍历就是先遍历父节点,再遍历左子节点,再遍历右子节点;中序遍历的顺序是:左子节点、父节点、右子节点;后序遍历的顺序为:左子节点、右子节点以及父节点。中序遍历的结果就是我们通常看到的表达式;后面的求值会用到后序遍历的结果。后序表达式就是先书写操作数再书写操作符,例如23 + 4 x 5对应的后序表达式为: 23 4 5 x +。
后序表达式虽然不利于阅读,但是用于计算表达式的值时非常的方便,且不需要借助于括号就可是调整运算的优先级。
遍历功能的实现如下:
const proto = BTNode.prototype;
proto.nodeType = nodeType;
// 中序遍历
proto.middleTraverse = function () {
return traverse(this, 'middle');
}
// 前序遍历
proto.forwardTraverse = function () {
return traverse(this, 'forward');
}
// 前序遍历
proto.backwardTraverse = function () {
return traverse(this, 'backward');
}
function traverse(node, type = 'mid') {
if (!node instanceof BTNode) {
throw `The param 'node' must be type of BTNode`;
}
if (node.type === nodeType.operand) {
return [node.value];
}
switch(type) {
case 'forward':
return [node.value, ...traverse(node.left, type), ...traverse(node.right, type)];
case 'middle':
return [...traverse(node.left, type), node.value, ...traverse(node.right, type)];
case 'backward':
return [...traverse(node.left, type), ...traverse(node.right, type), node.value];
}
}
5、后序表达式的求值
后序表达式的求值过程很简单:
遍历后序表达式中的每一个元素:
(1)如果是操作数则直接压入栈中,
(2)如果时操作符,从栈顶中取出两个元素,并进行操作符对应的运算,并将运算结果重新压入栈中;
具体的实现如下:
function evalBackwardExpression(exprs) {
const exprsStack = [];
for (let token of exprs) {
if (isOper(token)) {
const rNum = exprsStack.pop();
const lNum = exprsStack.pop();
let result;
switch(token) {
case '+':
result = lNum + rNum;
break;
case '-':
result = lNum - rNum;
break;
case '*':
result = lNum * rNum;
break;
case '/':
result = lNum / rNum;
break;
}
exprsStack.push(result);
} else {
exprsStack.push(token);
}
}
return exprsStack[0];
}
综上所述,整个求值过程包括以下几个步骤:
(1) 生成二叉树;
(2)遍历二叉树获取后序表达式;
(3)对后序表达式求值。
上面的代码中在将表达式转换为二叉树的过程中没有考虑表达式中有括号的情况,接下来对toBT 方法进行扩展以支持有括号的表达式。对于有括号的表达式,如果将括号中的整体看成是一个操作数,则整个解析过程和之前是一致的。
括号的处理过程大致如下:
如果是“(”, 直接入栈;
如果是右小括号,则一次取出第一个左括号之后的所有节点,按照3.(3)中的步骤组装成单个节点压入栈中;
扩展后的实现如下:
// 将字符串表达式转换成二叉树
const shouldMerge = node => node &&
node.type === nodeType.operator &&
(node.value === '*' || node.value === '/');
function toBT(expr) {
const tokens = expr.replace(/[\+\-\*\/\(\)]/g, _ => ` ${_} `)
.trim()
.split(/s+/)
.map(t => t.trim());
console.log(tokens);
const nodes = [];
for (let token of tokens) {
if (isOper(token)) {
const node = new BTNode(token, nodeType.operator, nodes.pop());
nodes.push(node);
} else if(token === '(') {
nodes.push('(');
} else if(token === ')') {
let node = nodes.pop();
let preNode;
try {
while ((preNode = nodes.pop()) !== '(') {
preNode.right = node;
node = preNode;
}
} catch(err) {
throw 'bracket count not match';
}
const top = nodes[nodes.length - 1];
if (shouldMerge(top)) {
top.right = node;
} else {
nodes.push(node);
}
} else {
const value = parseFloat(token);
if (Number.isNaN(value)) {
throw `Invalid express for '${token}'`;
}
const node = new BTNode(value, nodeType.operand);
const top = nodes[nodes.length - 1];
if (shouldMerge(top)) {
top.right = node;
} else {
nodes.push(node);
}
}
}
for (let i = 0; i < nodes.length - 1; i++) {
const node = nodes[i];
node.right = nodes[i + 1];
}
return nodes[0];
}
文章到这里就结束了,希望对大家有所帮助~~~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。