JavaScript -- 编写一个函数来解决数学表达式(不使用eval函数)

3

最终我想要做到这一点:

2x + 3 = 5

通过先从两边减3得到2x = 2,然后再将两边同时除以2得到x = 1,解出x。我一直在思考如何用JavaScript编写一个可以返回按顺序完成的步骤及结果数组的函数。显然,“eval”无法实现此功能,因此我们似乎必须重新创建等式。

起初,我想先忽略X,只尝试编写一个可以解决简单等式的函数,而不使用eval或任何内置函数。

我认为第一步是使用.split将术语拆分,但我遇到了一些问题,因为我需要为多个符号进行拆分。例如,如果我要评估简单表达式:3-6*3/9+5。因此,在我们进入运算顺序之前,仅拆分每个项(并对其进行分类)就已经很困难了,这也是我目前的主要具体问题。

我最初只是简单地一个接一个地拆分,但我遇到了一些问题,特别是考虑到顺序。

function solve(eq) {
    var minuses = eq.split("-"),
            pluses = minuses.map(x=> x.split("+")),
            timeses = pluses.map(x=>x.map(y=>y.split("*"))),
      dividers = timeses.map(x=>x.map(y=>y.map(z=>z.split("/"))));
            console.log(minuses, pluses, timeses, dividers);
}

solve("3 - 6 * 3 / 9 + 5");

如您所见,对于每个连续的运算符,我需要遍历上一个数组中的每个元素来拆分它,然后剩下的就是一个包含数组的数组等等...
那么 1) 如何更有效地拆分这些项,而不是为每个项创建一个新变量,并手动递归遍历每个项?看起来我只需要一些类似于最后一个示例中的“分隔符”数组的字典来跟踪操作的顺序(现在不考虑括号或指数):["*","/","+","-"] -- 并且根据该数组生成类似于上面示例中的最后一个数组的内容(仅包含常数),并以某种方式跟踪存储的数组遵循的元素...
2) 如何给定值数组求解表达式? 我有点困惑于逻辑,我想我需要从最后一个数组开始向上解决常量,同时跟踪当前的运算符是哪个,但我不确定具体怎么做。

你想按照DMAS规则评估表达式吗? - Maheer Ali
@MaheerAli 嗯,我想遵循运算顺序,即 dmas,把除法放在乘法之前,本质上与先乘后除是一样的吗?所以无论哪种方式,但我认为没有任何正确的方法可以通过先进行加减法来解决它。 - B''H Bi'ezras -- Boruch Hashem
你可能需要先阅读 https://dev59.com/zHVD5IYBdhLWcg3wR5ko 和 https://dev59.com/BnVD5IYBdhLWcg3wBW3d。数学解析器并不是很简单,所以你可以考虑使用第三方库来解决。至于第二个问题,你可以构建一个二叉表达式树,以 = 为根节点,然后从右到左移动根节点,同时平衡左右子节点的值。 - blaz
@blaz 我不计划使用任何第三方库,重点是从头开始制作。基本思路只是使用.split分割术语,并通过向后工作解决问题,但我遇到的唯一障碍就是如何干净利落地做到这一点。 - B''H Bi'ezras -- Boruch Hashem
2个回答

4

虽然您的问题不需要构建,但是二叉表达式树 是解决数学查询逻辑的好方法。

因此,对于查询3 - 6 * 3 / 9 + 5,代表二叉表达式树如下:

plus
  |_minus
  | |_3
  | |_divide
  |   |_times
  |   | |_3
  |   | |_6
  |   |_9
  |_5

为解决上述树问题,需要从叶子节点开始递归地解决到根节点。

此外,您不需要构建一棵树。它只是帮助我们看清这里的解析逻辑:

  • 获取查询中最后一个减号或加号表达式,并解决该表达式的左右子树。
  • 如果没有加/减,则获取最后一个乘法/除法表达式并解决左右子树。
  • 如果遇到数字,则返回该数字的值。

根据以上逻辑,下面是实现代码:

function solve(str) {
  var expressionIndex = Math.max(str.lastIndexOf("-"), str.lastIndexOf("+"));
  if (expressionIndex === -1) {
    expressionIndex = Math.max(str.lastIndexOf("*"), str.lastIndexOf("/"));
  }
  if (expressionIndex === -1) {
    var num = Number.parseInt(str.trim());
    if (isNaN(num)) {
      throw Exception("not a valid number");
    } else {
      return num;
    }
  } else {
    var leftVal = solve(str.substring(0, expressionIndex).trim());
    var rightVal = solve(str.substring(expressionIndex + 1).trim());
    switch (str[expressionIndex]) {
      case "+":
        return leftVal + rightVal;
      case "-":
        return leftVal - rightVal;
      case "*":
        return leftVal * rightVal;
      case "/":
        return leftVal / rightVal;
    }
  }
}

function parse(str) {
  var expressionIndex = Math.max(str.lastIndexOf("-"), str.lastIndexOf("+"));
  if (expressionIndex === -1) {
    expressionIndex = Math.max(str.lastIndexOf("*"), str.lastIndexOf("/"));
  }
  if (expressionIndex === -1) {
    var num = Number.parseInt(str.trim());
    if (isNaN(num)) {
      throw Exception("not a valid number");
    } else {
      return { type: "number", value: num };
    }
  } else {
    var leftNode = parse(str.substring(0, expressionIndex).trim());
    var rightNode = parse(str.substring(expressionIndex + 1).trim());
    return {
      type: "expression",
      value: str[expressionIndex],
      left: leftNode,
      right: rightNode
    };
  }
}

console.log(solve("3 - 6 * 3 / 9 + 5"));
console.log(parse("3 - 6 * 3 / 9 + 5"));

上面是一个只包含加减乘除(不包括括号)非常简单查询的解决方案。 要解决像您第一个示例这样的方程需要更多的工作。
编辑:添加一个解析函数以返回树形结构。

这个解决方案是否遵循BODMAS规则? - Vikash_Singh
不,它只是DMAS。 - blaz

2
你可以按照以下步骤完成:
  • 首先使用split(),并通过+-进行分割,这将在乘法和除法之后发生。
  • 然后在数组上使用map()并再次使用split()通过*/进行分割。
  • 现在我们有一个函数,它将对带运算符的数字数组进行求值,得到单个数字。
  • 将嵌套数组传递给完成乘法和除法的函数。
  • 然后将该结果再次传递给sovleSingle,并进行加法和减法。

该函数与eval相同,只要没有括号()

注意:这不关心+-中哪个先出现,也不关心*/中哪个先出现。但是*,/应该在+,-之前出现。

function solveSingle(arr){
  arr = arr.slice();
  while(arr.length-1){
    if(arr[1] === '*') arr[0] = arr[0] * arr[2]
    if(arr[1] === '-') arr[0] = arr[0] - arr[2]
    if(arr[1] === '+') arr[0] = +arr[0] + (+arr[2])
    if(arr[1] === '/') arr[0] = arr[0] / arr[2]
    arr.splice(1,1);
    arr.splice(1,1);
  }
  return arr[0];
}

function solve(eq) {
  let res = eq.split(/(\+|-)/g).map(x => x.trim().split(/(\*|\/)/g).map(a => a.trim()));
  res = res.map(x => solveSingle(x)); //evaluating nested * and  / operations.
   
  return solveSingle(res) //at last evaluating + and -
  
  
}

console.log(solve("3 - 6 * 3 / 9 + 5")); //6
console.log(eval("3 - 6 * 3 / 9 + 5")) //6


这个解决方案是否遵循BODMAS规则? - Vikash_Singh
2
@VikashSingh 不,它只遵循DMAS规则。因为这里没有括号。 - Maheer Ali
那非常聪明——我不知道你可以同时分割两个东西。 - B''H Bi'ezras -- Boruch Hashem
你为什么要复制它自己? - B''H Bi'ezras -- Boruch Hashem
让我们在聊天中继续这个讨论 - Maheer Ali
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接