广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >JavaScript解析数学表达式的过程详解
  • 127
分享到

JavaScript解析数学表达式的过程详解

2024-04-02 19:04:59 127人浏览 泡泡鱼
摘要

目录概要问题拆解问题解决问题从左到右计算计算从左到右+ -拆分左右* /拆分左右()整体优化完整代码总结概要 本文以一个 ? 的解题思路,来分享如何解决问题。 解决的过程,可以作为解

概要

本文以一个 ? 的解题思路,来分享如何解决问题。

解决的过程,可以作为解决工作中一般问题的通用思路。

希望同学有所收获。

问题

通过js解析数学表达式字符串

(1 + (2 - (3 * 4 / 5 + 6 - (7 + 8))) + (9 - 10) * 11 + 12) + 13

表达式中包含基本的数学运算符号+-*/()小括号,数字都是正整数。

下面记录了个人的思考过程。

拆解问题

正常在做数学运算的时候,一般都是先进行左侧的运算,拿左边的运算结果继续和右边的继续运算。从左到右运算时,可能会遇到不同的操作符。

  • 如果遇到+-,直接对运算符左右两侧数字进行加减运算,拿到结果后和下一个继续运算。

  • 如果遇到了*/,则优先计算乘除,在进行加减运算。

  • 如果遇到了 () ,则小括号内的表达式被视为一个整体参与运算,在得到运算结果后再参与小括号外的运算。

以上是对一个程序问题场景的拆分,我们可以得到下面几个原则

  • 运算是从左到右。
  • 表达式中+-,直接拆分成左右两个
  • 表达式中*/,则*/可以被视为一个整体做优先计算。如都是*/同上。
  • 表达式中(),被视为一个整体

JS解析数学表达式,被拆解为上面的四个原则,即是我们需要解决的问题。所以在遇到一个大的问题时,我们第一步应该是对问题进行拆解

在对一个个小问题进行解答的时候,我们就把一个大的问题解决了。

下面逐一解决这四个问题。

解决问题

从左到右计算

从左到右计算,有两个关键点从左到右计算

计算

我们先分析计算,在进行+ - * /计算时,即利用运算符号对两侧数字进行运算。下面用一个图表示下1+2

这样就确定了一个运算操作的基本数据结构,即二叉?中间节点存储运算符号left节点储存左侧的数值right节点存储右边的数值

从左到右

这里举一个简单的加法运算表达式1 + 2 + 3 + 4来分析从左到右。我们从左到右遍历这个这个表达式,可以得到下图二叉?

但是遍历这样的数据结构得到的运算过程是从右到左(深度遍历先计算 3 + 4 一直到 1)。所以我们尝试从右到左遍历这个表达式,可以得到下图。

现在我们就明确了编码需要做的具体事项,即从右到左遍历表达式,形成一个二叉?。基本的代码如下:

const expression = 'xxxx';
const parse = (expression, l = 0, r = expression.length - 1) => {
    for (let i = r; i >= l; i--) {
        const v = expression[i];
        let isNumber = true;
        
        if (i === l) {
          if (isNumber) {
            return {
              type: 'number',
              value: Number(expression.slice(l, r + 1).trim()),
            };
          }
        }
        if (/[0-9]/.test(v) || v === ' ') {
            continue;
        }
        isNumber = false;
        return {
            type: v,
            left: parse(expression, l, i - 1),
            right: parse(expression, i + 1, r),
        }
    } 
}

+ -拆分左右

+ -进行左右拆分,这个可以基于上面的代码,稍调整下逻辑即可:

...
return {
    type: v,
    left: parse(expression, l, i - 1),
    right: parse(expression, i + 1, r),
}
...

更改为

...
if (v === '+' || v === '-') {
    return {
        type: v,
        left: parse(expression, l, i - 1),
        right: parse(expression, i + 1, r),
    }
}
...

* /拆分左右

相较于+ - 拆分左右,* /更加复杂些。我们应该先拆分场景

可以整理出两个场景,仅包含* /混合运算

  • 1 + 2 * 3 / 4
  • 1 * 2 * 3 / 4

我们在从右到左遍历表达式时,我们在遇到* /,需要继续向左遍历,判断表达式左边是否有+ -

如果遇到了+ -,则按照 + -拆分左右 的原则解析。

如果是仅包含 * /,则我们需要拿遇到的第一个运算符/,拆分左右。

我们调整下parse的逻辑

...
let firstTimeOrDivideOperator = null; // 记录遇到的第一个 * / 运算符
let firstTimeOrDivideOperatorIdx = null; // 记录遇到的第一个 * / 运算符的位置
...
// 遍历到最左侧,判断 * / 左边是否有 + -
if (i === l) {
  ...
  // * / 拆分,需要遍历到最左侧,所里拆分逻辑写这里
  return {
      type: firstTimeOrDivideOperator,
      left: parse(expression, i, firstTimeOrDivideOperatorIdx - 1),
      right: parse(expression, firstTimeOrDivideOperatorIdx + 1, r),
  }
}
...
if ((v === '*' || v === '/') && !firstTimeOrDivideOperator) {
    firstTimeOrDivideOperator = v
    firstTimeOrDivideOperatorIdx = i
}

()整体

()被视为一个整体,首先我们要知道哪两个括号是一对。一个表达式如果剔除了+ - * /后,剩下的部分可能是这个样子(()(()))

我们从右到左分析这个字符串,在整个字符串中,遇到的第一个(,右侧离它最近的那个),它们俩就是一对。然后我们将这一对()剔除。重复上面的操作即:

  • ( ( ) ( ( ) ) )
  • ( ( ) ( - - ) )
  • ( ( ) - - - - )
  • ( - - - - - - )
  • - - - - - - - -

分析上面的步骤,我们可以知道,如果遇到)我们记录下来,如果遇到(,则将将最近的)剔除。对数据结构敏感的同学,一定会想到

同时我们要记录下()位置信息。

我们调整下parse的逻辑

const stock = []; // 先进后出,每一次出栈,即一对 ()
const parenthesesPairPosition = {}
...
let parenthesesDep = 0; // 记录小括号深度
...

if (v === ')') {
    stock.push(i)
    parenthesesDep++
} else if (v === '(') {
    const last = stock.pop()
    parenthesesPairPosition[i] = last
    parenthesesDep--
}

if (i === l) {
    ...
    if (parenthesesPairPosition[i] === r) {
        return parse(expression, l + 1, r - 1)
    }
    ...
}
...

优化

优化一般是减少重复的工作。

我们可以快速定位上面解决问题内容的 * / 拆分左右 部分有重复的工作。

1 + 2 * 3 / 4 在从右向左搜索字符串串时,第一遍我们就知道了连续的* /,第二遍我可以不用遍历到最左侧,按照搜索到的第一个* /拆分左右即可。

优化上面的代码:

const parse = (expression, l = 0, r = expression.length - 1, skipSearchTimeOrDivide = false) => {
    ...
    let firstTimeOrDivideOperator = null; // 记录遇到的第一个 * / 运算符
    let firstTimeOrDivideOperatorIdx = null; // 记录遇到的第一个 * / 运算符的位置
    for (let i = r; i >= l; i--) {
        ...
        // skipSearchTimeOrDivide 为 true 表示表达式是连续的 * /
        if (skipSearchTimeOrDivide && firstTimeOrDivideOperator) {
            return {
                type: firstTimeOrDivideOperator,
                left: parse(expression, l, firstTimeOrDivideOperatorIdx - 1, true),
                right: parse(expression, firstTimeOrDivideOperatorIdx + 1, r),
            }
        }
        if (i === l) {
          ...
          return {
              type: firstTimeOrDivideOperator,
              left: parse(expression, l, firstTimeOrDivideOperatorIdx - 1, true),
              right: parse(expression, firstTimeOrDivideOperatorIdx + 1, r),
          }
        }
        ...
        if ((v === '*' || v === '/') && !firstTimeOrDivideOperator) {
            firstTimeOrDivideOperator = v
            firstTimeOrDivideOperatorIdx = i
        }
    } 
}

完整代码

补充了剔除两侧空格和小括号运算细节

const stock = []; // 先进后出,每一次出栈,即一对 ()
const parenthesesPairPosition = {}
// 剔除两侧空格
const removeBlank = (expression, l, r) => {
    while (expression[l] === ' ') {
        l++
    }
    while (expression[r] === ' ') {
        r--
    }
    return [l, r]
}
// 剔除两侧小括号
const removeParentheses = (l, r) => {
    if (parenthesesPairPosition[l] === r) return [++l, --r]
    return [l, r]
}
const parse = (expression, l = 0, r = expression.length - 1, skipSearchTimeOrDivide = false) => {
    let isNumber = true;
    let parenthesesDep = 0; // 记录小括号深度
    let firstTimeOrDivideOperator = null; // 记录遇到的第一个 * / 运算符
    let firstTimeOrDivideOperatorIdx = null; // 记录遇到的第一个 * / 运算符的位置
    [l, r] = removeBlank(expression, l, r);
    [l, r] = removeParentheses(l, r);
    for (let i = r; i >= l; i--) {
        const v = expression[i];
        if (v === ')') {
            stock.push(i)
            parenthesesDep++
        } else if (v === '(') {
            const last = stock.pop()
            parenthesesPairPosition[i] = last
            parenthesesDep--
        }
        // skipSearchTimeOrDivide 为 true 表示表达式是连续的 * /
        if (skipSearchTimeOrDivide && firstTimeOrDivideOperator) {
            return {
                type: firstTimeOrDivideOperator,
                left: parse(expression, l, firstTimeOrDivideOperatorIdx - 1, true),
                right: parse(expression, firstTimeOrDivideOperatorIdx + 1, r),
            }
        }
        if (i === l) {
          if (isNumber) {
            return {
              type: 'number',
              value: Number(expression.slice(l, r + 1).trim()),
            };
          }
          if (parenthesesPairPosition[i] === r) {
              return parse(expression, l + 1, r - 1)
          }
          // * / 拆分,需要遍历到最左侧,所里拆分逻辑写这里
          return {
              type: firstTimeOrDivideOperator,
              left: parse(expression, l, firstTimeOrDivideOperatorIdx - 1, true),
              right: parse(expression, firstTimeOrDivideOperatorIdx + 1, r),
          }
        }
        if (/[0-9]/.test(v) || v === ' ') {
            continue;
        }
        isNumber = false;
        // parenthesesDep === 0 进行表达式分析的时候要确保是同一层级
        if (parenthesesDep === 0 && (v === '+' || v === '-')) {
            return {
                type: v,
                left: parse(expression, l, i - 1),
                right: parse(expression, i + 1, r),
            }
        }
        if (parenthesesDep === 0 && (v === '*' || v === '/') && !firstTimeOrDivideOperator) {
            firstTimeOrDivideOperator = v
            firstTimeOrDivideOperatorIdx = i
        }
    } 
}
const exec = ast => {
    const recursion = ast => {
        if (ast.type === '+') {
            return recursion(ast.left) + recursion(ast.right)
        } else if (ast.type === '-') {
            return recursion(ast.left) - recursion(ast.right)
        } else if (ast.type === '*') {
            return recursion(ast.left) * recursion(ast.right)
        } else if (ast.type === '/') {
            return recursion(ast.left) / recursion(ast.right)
        } else if (ast.type === 'number') {
            return ast.value
        }
    }
    return recursion(ast)
}
const expression = '(1 + (2 - (3 * 4 / 5 + 6 - (7 + 8))) + (9 - 10) * 11 + 12) + 13'
console.log(exec(parse(expression)) === eval(expression))

总结

解决一般问题的通用思路:

  • 拆分问题
  • 基于问题拆分场景,根据不同的情况编写逻辑
  • 优化代码,分析代码中重复的工作等等

同时我们也要拓展编程相关基本知识,不断学习。这样在面对问题时,可以快速想到可能的解决方案。 例如上面的问题,同学对数据结构有所了解的话,则分析小括号可以迅速想到用去解决。另外一点就是编译原理中也有讲到扫描运算表达式时,从右到左扫描。

到此这篇关于javascript 解析数学表达式的过程详解的文章就介绍到这了,更多相关js解析表达式内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: JavaScript解析数学表达式的过程详解

本文链接: https://www.lsjlt.com/news/150736.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • JavaScript解析数学表达式的过程详解
    目录概要问题拆解问题解决问题从左到右计算计算从左到右+ -拆分左右* /拆分左右()整体优化完整代码总结概要 本文以一个 的解题思路,来分享如何解决问题。 解决的过程,可以作为解决...
    99+
    2022-11-13
  • JavaScript 正则表达式详解
    目录1. 正则表达式创建2. 使用模式2.1 使用简单模式2.2 使用特殊字符3. 应用3.1 切分字符串3.2 分组3.3 贪婪匹配3.4 正则表达式标志3.5 test() 方法...
    99+
    2022-11-12
  • 详解JavaScript高级正则表达式
    目录JavaScript高级正则表达式1.正则表达式概述1.1什么是正则表达式1.2正则表达式的特点2.正则表达式在js中的使用2.1正则表达式的创建2.2测试正则表达式3.正则表达...
    99+
    2022-11-12
  • JavaScript中Generator函数yield表达式示例详解
    以上就是JavaScript中Generator函数yield表达式示例详解的详细内容,更多请关注编程网其它相关文章!...
    99+
    2022-11-22
    javascript
  • 详解Java函数式编程和lambda表达式
    目录为什么要使用函数式编程JDK8接口新特性函数接口方法引用类型推断变量引用级联表达式和柯里化为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论。函数式与命令...
    99+
    2022-11-12
  • C++学习之Lambda表达式的用法详解
    目录简介捕获原理Lambda回调简介 Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中...
    99+
    2022-11-13
  • Java学习之Lambda表达式的使用详解
    目录Lamda表达式函数式接口Lambda表达式的推导函数式接口的不同类型Lambda表达式与函数式接口的简单应用Lambda表达式的优缺点Lamda表达式 λ希腊字母...
    99+
    2022-12-26
    Java Lambda表达式用法 Java Lambda表达式 Java Lambda
  • Javascript中正则表达式的应用详解
    目录stringsearchreplacematch:RegExp总结正则表达式 在前端中的应用也是比较常见的,我们在有时候也需要 用js 对某些字符串进行查找\捕获 或者 替换. ...
    99+
    2022-11-13
  • 详解R语言中的表达式、数学公式、特殊符号
    目录##一、R语言的“表达式”##二、产生“表达式”的函数####1、expression 函数####2、quote函数####3、bquote 和 substitute 函数##...
    99+
    2022-11-11
  • 正则表达式regular expression详述(二)284415过程讲解
    正则表达式详述(二)    以下这些不是正则表达式的新增对象请参阅对应的JavaScript对象的属性    $_属性   ...
    99+
    2023-05-20
    正则表达式regular expression详述(二)
  • 正则表达式regular expression详述(一)284475过程讲解
     正则表达式是regular expression,看来英文比中文要好理解多了,就是检查表达式符不符合规定!!正则表达式有一个功能十分强大而又十分复杂的对象RegExp,在...
    99+
    2023-05-20
    正则表达式regular expression详述
  • Java中正则表达式匹配过程实例详解
    目录下面是Java正则表达式的语法字符:正则表达式简单的匹配过程:(1) 基础匹配过程(2)贪婪模式(3)非贪婪模式 (4)零宽度匹配过程总结正则表达式:定义字符串的模式,...
    99+
    2022-11-13
  • Java中缀表达式转后缀表达式流程详解
    目录一、栈1、栈的基本介绍2、栈的底层实现二、中缀表达式转后缀表达式1、拆解中缀表达式2、中缀转后缀的算法3、中缀转后缀代码解析4、对后缀表达式进行计算一、栈 1、栈的基本介绍 栈是...
    99+
    2022-11-13
  • C#表达式和运算符详细解析
    目录类型转换1、表达式1.2 运算符分类2、数学运算符3、赋值运算符4、关系运算符5、布尔运算符6、位运算符6.1 &按位与运算6.2 或|按位运算6.3 异或^按位运算符6...
    99+
    2022-11-13
  • JavaScript正则表达式中g标志详解
    目录缘起解密过程搜索引擎源码层面结论缘起 有一天在思否社区看到有个问题,大致描述如下 const list = ['a', 'b', '-', 'c', 'd']; const re...
    99+
    2022-11-13
  • JAVALambda表达式与函数式接口详解
    Lambda表达式的诞生是为了解决JAVA创建匿名内部类代码冗余的问题。例子如下: public class Lambda { public static void main...
    99+
    2022-11-13
  • Python3的正则表达式详解
    目录1.简介2.切分字符串3.分组4.贪婪匹配5.编译总结1.简介 # 正则表达式:用来匹配字符串的武器; # 设计思想:用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符...
    99+
    2022-11-13
  • Java 8的Lambda表达式详解
    本篇内容介绍了“Java 8的Lambda表达式详解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!功能接口只包含一个方法的接口被称为功能接口...
    99+
    2023-06-17
  • JavaScript中正则表达式的实际应用详解
    实际工作中,JavaScript正则表达式还是经常用到的。所以这部分的知识是非常重要的。 一、基础语法: 第一种:字面量语法 var expression=/pattern/f...
    99+
    2022-11-12
  • Python+eval函数实现动态地计算数学表达式详解
    目录Python 的 eval()第一个参数:expression第二个参数:globals第三个参数:locals用 eval() 计算表达式布尔表达式数学表达式通用表达式Pyth...
    99+
    2022-11-11
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作