前言
面试官问:如何实现一个加减法的计算器,用户输入等号时进行计算,返回结果。前提是不能使用eval或Function类似的方式实现。
面试官又说:现在要让这个计算器支持乘除法。
最后面试官又来了一句:还想让这个计算器支持小括号。
现在就按照面试流程一步步来实现吧。
实现第1步:加减计算器
假设有这样一段输入5+20-13。
首先需要对输入进行分隔,划分出每个语义,如20,不能理解为2和0。
比较方便的划分方式是按照符号(+-)来分隔。
分隔字符
function input2Tokens(input) {
const pattern = /[\+\-]/g;
const tokens = [];
let matches;
// 每次匹配开始的位置
let lastIndex = 0;
// 匹配符号
// exec每次会从上次匹配的位置往后去匹配
while ((matches = pattern.exec(input))) {
const { index } = matches;
// 符号位前面有数字 将从开始位到符号位之间所有字符作为一个token
if (lastIndex < index) {
tokens.push(input.slice(lastIndex, index));
}
// 符号位
tokens.push(matches[0]);
lastIndex = index + 1;
}
// 最后一个符号位到结尾的全部数字作为一个token
tokens.push(input.slice(lastIndex));
return tokens;
}
从tokens中取符号及数字进行计算
每次取一个优先级最高的符号(这里只有加减符号优先级是一样的)。
同时取出符号两端的数字,同符号一起进行计算。
将计算结果再放入tokens中,放回的位置为左侧端的数字的位置。
// 获取优先级最高的符号的位置
function getHighestPrioritySignIndex(tokens) {
return tokens.findIndex(token => ['+', '-'].includes(token));
}
// 取出符号和两端的数字
function getCalcTokens(tokens, signIndex) {
return tokens.splice(signIndex - 1, 3);
}
// 计算符号及两端数字
function calcTokens(tokens) {
let [left, sign, right] = tokens;
left = Number(left);
right = Number(right);
if (sign === '+') return left + right;
if (sign === '-') return left - right;
}
// 汇总
function calculate(tokens) {
const signIndex = getHighestPrioritySignIndex(tokens);
const result = calcTokens(getCalcTokens(tokens, signIndex));
// 将结果放回tokens
tokens.splice(signIndex - 1, 0, result);
return tokens;
}
重复上次的计算
直到tokens为空时,计算结果也无需再放入tokens中。
function calculate(tokens) {
const signIndex = getHighestPrioritySignIndex(tokens);
const result = calcTokens(getCalcTokens(tokens, signIndex));
if (tokens.length === 0) return result;
// 将结果放回tokens
tokens.splice(signIndex - 1, 0, result);
return calculate(tokens);
}
优化一下代码
function calculate(input) {
const tokens = input2Tokens(input);
function _calculate() {
const signIndex = getHighestPrioritySignIndex(tokens);
const result = calcTokens(getCalcTokens(tokens, signIndex));
if (tokens.length === 0) return result;
// 将结果放回tokens
tokens.splice(signIndex - 1, 0, result);
return _calculate();
}
return _calculate();
}
增加乘除法运算
修改input2Tokens()中的分隔符正则。
const pattern = /[\+\-\*\/]/g;
修改获取最高优先级符号的方法。
// 获取优先级最高的符号的位置
function getHighestPrioritySignIndex(tokens) {
// 乘除法优先级更高
let index = tokens.findIndex(token => ['*', '/'].includes(token));
if (index === -1) {
index = tokens.findIndex(token => ['+', '-'].includes(token));
}
return index;
}
修改calcTokens()方法。
// 计算符号及两端数字
function calcTokens(tokens) {
let [left, sign, right] = tokens;
left = Number(left);
right = Number(right);
if (sign === '+') return left + right;
if (sign === '-') return left - right;
if (sign === '*') return left * right;
if (sign === '/') return left / right;
}
增加括号运算
修改input2Tokens()中的分隔符正则
增加(与)。
const pattern = /[\+\-\*\/\(\)]/g;
获取最深级别的子表达式
因为括号中的优先级最高,需要找到层级最深的小括号中及其中的tokens(难度最高)。
// 获取深度最大的子表达式
function getDeepestChildTokens(tokens) {
// 最大深度
let maxDepth = 0;
// 最大深度的左括号位置
let maxDepthIndex = -1;
// 当前深度
let depth = 0;
tokens.forEach((token, index) => {
if (token === '(') {
depth++;
if (depth > maxDepth) {
maxDepth = depth;
maxDepthIndex = index;
}
} else if (token === ')') {
depth--;
}
});
if (maxDepth) {
// 从最大深度的左括号位置往后查找对应的右括号
const maxDepthEndIndex = tokens.indexOf(')', maxDepthIndex);
// 截取字表达式
const childTokes = tokens.splice(
maxDepthIndex,
maxDepthEndIndex - maxDepthIndex + 1
);
return [
maxDepthIndex,
// 去除两端的括号
childTokes.slice(1, -1),
];
}
return [];
}
todo:使用栈(回溯)的方式可以更快。
修改calculate()方法。
function calculate(input) {
function _calculate(tokens) {
const [childIndex, childTokens] = getDeepestChildTokens(tokens);
if (childTokens) {
tokens.splice(childIndex, 0, _calculate(childTokens));
return _calculate(tokens);
}
const signIndex = getHighestPrioritySignIndex(tokens);
const result = calcTokens(getCalcTokens(tokens, signIndex));
if (tokens.length === 0) return result;
// 将结果放回tokens
tokens.splice(signIndex - 1, 0, result);
return _calculate(tokens);
}
const tokens = input2Tokens(input);
return _calculate(tokens);
}
全部完整代码
// 1+2+3-5
function input2Tokens(input) {
const pattern = /[\+\-\*\/\(\)]/g;
const tokens = [];
let matches;
// 每次匹配开始的位置
let lastIndex = 0;
// 匹配符号
// exec每次会从上次匹配的位置往后去匹配
while ((matches = pattern.exec(input))) {
const { index } = matches;
// 符号位前面有数字 将从开始位到符号位之间所有字符作为一个token
if (lastIndex < index) {
tokens.push(input.slice(lastIndex, index));
}
// 符号位
tokens.push(matches[0]);
lastIndex = index + 1;
}
// 最后一个符号位到结尾的全部数字作为一个token
tokens.push(input.slice(lastIndex));
return tokens;
}
// 获取优先级最高的符号的位置
function getHighestPrioritySignIndex(tokens) {
// 乘除法优先级更高
let index = tokens.findIndex(token => ['*', '/'].includes(token));
if (index === -1) {
index = tokens.findIndex(token => ['+', '-'].includes(token));
}
return index;
}
// 取出符号和两端的数字
function getCalcTokens(tokens, signIndex) {
return tokens.splice(signIndex - 1, 3);
}
// 计算符号及两端数字
function calcTokens(tokens) {
let [left, sign, right] = tokens;
left = Number(left);
right = Number(right);
if (sign === '+') return left + right;
if (sign === '-') return left - right;
if (sign === '*') return left * right;
if (sign === '/') return left / right;
}
// 获取深度最大的子表达式
function getDeepestChildTokens(tokens) {
// 最大深度
let maxDepth = 0;
// 最大深度的左括号位置
let maxDepthIndex = -1;
// 当前深度
let depth = 0;
console.log(tokens);
tokens.forEach((token, index) => {
if (token === '(') {
depth++;
if (depth > maxDepth) {
maxDepth = depth;
maxDepthIndex = index;
}
} else if (token === ')') {
depth--;
}
});
if (maxDepth) {
// 从最大深度的左括号位置往后查找对应的右括号
const maxDepthEndIndex = tokens.indexOf(')', maxDepthIndex);
// 截取字表达式
const childTokes = tokens.splice(
maxDepthIndex,
maxDepthEndIndex - maxDepthIndex + 1
);
return [
maxDepthIndex,
// 去除两端的括号
childTokes.slice(1, -1),
];
}
return [];
}
function calculate(input) {
function _calculate(tokens) {
const [childIndex, childTokens] = getDeepestChildTokens(tokens);
if (childTokens) {
tokens.splice(childIndex, 0, _calculate(childTokens));
return _calculate(tokens);
}
const signIndex = getHighestPrioritySignIndex(tokens);
const result = calcTokens(getCalcTokens(tokens, signIndex));
if (tokens.length === 0) return result;
// 将结果放回tokens
tokens.splice(signIndex - 1, 0, result);
return _calculate(tokens);
}
const tokens = input2Tokens(input);
return _calculate(tokens);
}
let result;
// result = calculate('1+20+3-5');
// result = calculate('2+3*4-6/2');
result = calculate('100/((3+2)*4)-7');
console.log(result);