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

 更新时间:2022年06月07日 10:52:00   作者:照着剧本演重名了  
这篇文章主要介绍了JavaScript 解析数学表达式的过程详解,本文以一个的解题思路,来分享如何解决问题,解决的过程,可以作为解决工作中一般问题的通用思路,对js解析表达式相关知识感兴趣的朋友一起看看吧

概要

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

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

希望同学有所收获。

问题

通过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解析表达式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JavaScript实现简单获取本地图片主色调

    JavaScript实现简单获取本地图片主色调

    想象一个场景,就是如何根据一张图片大概提取出它的主色调呢?获取主色调后,可能会用来设置某些背景颜色,这里,利用 JS 简单获取本地图片主色调,希望对大家有所帮助
    2023-03-03
  • ES5和ES6中类的区别总结

    ES5和ES6中类的区别总结

    这篇文章主要给大家介绍了ES5和ES6中类的区别的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • 三剑客:offset、client和scroll还傻傻分不清?

    三剑客:offset、client和scroll还傻傻分不清?

    这篇文章主要给大家介绍了三剑客:offset,client和scroll的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • js实现超简单的展开、折叠目录代码

    js实现超简单的展开、折叠目录代码

    这篇文章主要介绍了js实现超简单的展开、折叠目录代码,通过javascript操作鼠标点击事件控制页面元素样式的动态改变实现该功能,非常简单实用,需要的朋友可以参考下
    2015-08-08
  • js css样式操作代码(批量操作)

    js css样式操作代码(批量操作)

    用js控制css样式,能让网页达到良好的的用户体验甚至是动画的效果。并且考虑到效率。
    2009-10-10
  • 一个简易的js图片轮播效果

    一个简易的js图片轮播效果

    这篇文章主要为大家详细介绍了一个简易的js图片轮播效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • JavaScript装饰者模式原理与用法实例详解

    JavaScript装饰者模式原理与用法实例详解

    这篇文章主要介绍了JavaScript装饰者模式原理与用法,结合实例形式总结分析了JavaScript装饰者模式相关概念、功能、原理、用法及操作注意事项,需要的朋友可以参考下
    2020-03-03
  • 移动端js图片查看器

    移动端js图片查看器

    这篇文章主要为大家详细介绍了js图片查看器的制作方法,可以实现图片的滑动等效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • Nuxt3 布局layouts和NuxtLayout的使用详解

    Nuxt3 布局layouts和NuxtLayout的使用详解

    layouts是Nuxt3提供的一种方便开发者快速实现自定义布局的约定,是基于Vue3的一个开发框架,基于服务器端渲染SSR,可以更加方便的用于Vue的SEO优化,这篇文章主要介绍了Nuxt3 布局layouts和NuxtLayout的使用,需要的朋友可以参考下
    2023-04-04
  • js结合css实现登录后才能复制的效果实例

    js结合css实现登录后才能复制的效果实例

    很多网站都有登录后才能复制的限制,什么原理呢?css属性user-select:none,通常会采用这种方式来禁止复制文本。但浏览开发者工具-审查元素,取消此样式后,就可以选中文本了。想要完整地禁止复制,还需要通过js控制选择的内容。
    2023-07-07

最新评论