算法:如何实现一个计算器

146 阅读5分钟

前言

面试官问:如何实现一个加减法的计算器,用户输入等号时进行计算,返回结果。前提是不能使用evalFunction类似的方式实现。

面试官又说:现在要让这个计算器支持乘除法。

最后面试官又来了一句:还想让这个计算器支持小括号。

现在就按照面试流程一步步来实现吧。

实现第1步:加减计算器

假设有这样一段输入5+20-13

首先需要对输入进行分隔,划分出每个语义,如20,不能理解为20

比较方便的划分方式是按照符号(+-)来分隔。

分隔字符

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;
}

image.png

从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;
}

image.png

重复上次的计算

直到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);
}

image.png

优化一下代码

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();
}

image.png

增加乘除法运算

修改input2Tokens()中的分隔符正则。

const pattern = /[\+\-\*\/]/g;

image.png

修改获取最高优先级符号的方法。

// 获取优先级最高的符号的位置
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;
}

image.png

增加括号运算

修改input2Tokens()中的分隔符正则

增加()

const pattern = /[\+\-\*\/\(\)]/g;

image.png

获取最深级别的子表达式

因为括号中的优先级最高,需要找到层级最深的小括号中及其中的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);
}

image.png

全部完整代码

// 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);

已进行部分优化

对计算器算法的优化