使用js解析并计算一个“数学表达式”的字符串

1,278 阅读6分钟

前言

今天中午和同事聊天的时候,他说看到一个题目,给定一个数学表达式的字符串,如何得到字符串的计算结果。

想了一下,主体思路应该是使用借助“栈”来计算的。

比如 : 2 + 3 - 5

定义一个stack

push 2

stack = [2]

push +

push = [2, '+']

push 3

stack = [2, '+', 3]

我们可以判断最末尾的这几个字符或者数字,能否进行一次计算,如果可以,我们就pop出末尾的几个,然后把结果再 push 到stack。

pop 3次 stack = []

依次拿到了 2 + 3

计算结果 = 5

于是

push 5

stack = [5]

并以此类推。

什么时候应该pop

push的过程很简单,依次推入stack就行。

什么情况可以pop

什么时候应该pop呢。考虑之后,我觉得应该分为以下几种情况:

1、末尾3位是["(", number, ")"]

这个不用说,括号里只包围了一个数字,相当于直接把括号拿掉

然后把number push进入

2、末尾5位是["(", number, symbol, number, ")"]

这个也好理解。括号优先级最高

pop5次,然后把取出来的内容计算结果,push进入。

前两种是有括号闭合的情形。括号里面如果有再多的内容,都可以通过下面几个规则,变换为前2个规则。

3、末尾3位是 [number, * or /, number]

因为乘除的优先级最高,所以,遇到乘除运算符,可以直接把数字pop出来,计算结果并推入。

4、末尾5位是 [number, + or -, number, + or -, number]

虽然 + - 运算的优先级很低,但是如果是连续2个 + - 运算。我们可以先把前面的3位计算出结果result。

然后依次 push: result, + or 1, number

什么情况不可以pop

1、末尾是 [number, + or -, number]

这种情况是不可以直接运算的,因为后面也许接上了乘除运算呢。

代码示例

最后,进行一次测试,来计算表达式:

-12+23+32-22/22-2+(23)+(4+2)*5

function computeExpression (e) {
  // 去掉字符串中的空格
  e = e.replace(' ', '')
  
  // 用来匹配和分割字符串
  a = e.match(/((^\+)|(^\-))*((\d)+)|\+|\-|\*|\/|\(|\)/g)

  console.log('expression', e)
  console.log('split expression', a)
  
  // 循环入栈
  while (a.length) {
    pushToStack(a)
  }

  // 最后如果只剩下3个内容,我们就直接计算结果。
  if (stack.length === 3) {
    return basicCompute(...stack)
  }
}

// 入栈
function pushToStack (a) {
  const s = a.shift()
  console.log('push to stack', s)
  stack.push(s)
  console.log(stack, 'stack')
  
  // 每次入栈都尝试,末尾是否可计算
  compute(stack, s.length)
}

// 末尾可以pop出来计算 的情形
const canCompute = stack =>
  canHigh(stack) ||
  canLow(stack) ||
  canBrackets(stack) ||
  singleNumberInBrackets(stack)

// 判断可以计算的函数:括号表达式
const canBrackets = stack => {
  const len = stack.length
  return (
    stack.length >= 5 &&
    isRightBracket(stack[len - 1]) &&
    isNumber(stack[len - 2]) &&
    isFlag(stack[len - 3]) &&
    isNumber(stack[len - 4]) &&
    isLeftBracket(stack[len - 5])
  )
}

// 判断可以计算的函数:括号内只有单个数字
const singleNumberInBrackets = stack => {
  const len = stack.length
  return (
    stack.length >= 3 &&
    isRightBracket(stack[len - 1]) &&
    isNumber(stack[len - 2]) &&
    isLeftBracket(stack[len - 3])
  )
}

// 判断可以高阶数学运算的函数:乘除运算
const canHigh = stack => {
  const len = stack.length
  return (
    stack.length >= 3 &&
    isNumber(stack[len - 1]) &&
    isHighFlag(stack[len - 2]) &&
    isNumber(stack[len - 3])
  )
}

// 判断可以低阶阶数学运算的函数:加减运算
const canLow = stack => {
  const len = stack.length
  return (
    stack.length >= 4 &&
    isLowFlag(stack[len - 1]) &&
    isNumber(stack[len - 2]) &&
    isLowFlag(stack[len - 3]) &&
    isNumber(stack[len - 4])
  )
}

// 计算
var compute = stack => {


  // 括哈内单数字的入栈和出栈过程
  if (singleNumberInBrackets(stack)) {
    console.log('compute singleNumberInBrackets')

    let rightFlag = stack.pop()
    console.log('pop out of stack right bracket', rightFlag)

    let number = stack.pop()
    console.log('pop out of stack number', number)

    let leftBracket = stack.pop()
    console.log('pop out of stack left', leftBracket)

    stack.push(number)
    console.log('push number to stack', number)
  }

  // 括号内包含一个表达式的计算过程
  if (canBrackets(stack)) {
    console.log('can brackets')

    let rightFlag = stack.pop()
    console.log('pop out of stack right bracket', rightFlag)

    let right = stack.pop()
    console.log('pop out of stack right', right)

    let flag = stack.pop()
    console.log('pop out of stack flag', flag)

    let left = stack.pop()
    console.log('pop out of stack left', left)

    let leftBracket = stack.pop()
    console.log('pop out of stack left', leftBracket)

    const res = basicCompute(left, flag, right)
    stack.push(res)
    console.log('push result to stack', res)
  }

  // 可以进行乘除计算的过程
  if (canHigh(stack)) {
    console.log('can high')

    let right = stack.pop()
    console.log('pop out of stack right', right)

    let flag = stack.pop()
    console.log('pop out of stack flag', flag)

    let left = stack.pop()
    console.log('pop out of stack left', left)

    const res = basicCompute(left, flag, right)
    stack.push(res)
    console.log('push result to stack', res)
  }

  // 可以进行加减计算的过程
  if (canLow(stack)) {
    console.log('can low')

    let last = stack.pop()
    console.log('pop out of stack', last)

    right = stack.pop()
    console.log('pop out of stack right', right)

    flag = stack.pop()
    console.log('pop out of stack flag', flag)

    left = stack.pop()
    console.log('pop out of stack left', left)

    const bc = basicCompute(left, flag, right)
    console.log('push result to stack', bc)
    stack.push(bc)

    console.log('push to stack', last)
    stack.push(last)
  }

  //计算完后,再次判断末尾能不能触发连锁反应
  if (canCompute(stack)) {
    compute(stack)
  }
}


// 基本运算
const basicCompute = (a, b, c) => {
  if (b == '+') {
    return ~~a + ~~c
  } else if (b === '-') {
    return ~~a - ~~c
  } else if (b === '*') {
    return ~~a * ~~c
  } else {
    return ~~a / ~~c
  }
}

const isHighFlag = flag => /\*|\//.test(flag)
const isLowFlag = flag => /\+|\-/.test(flag)
const isFlag = flag => /\+|\-|\*|\//.test(flag)
const isNumber = num => /(\+|\-)*\d*/.test(num)

const isLeftBracket = flag => flag === '('
const isRightBracket = flag => flag === ')'

const expression = '-12+23+3*2-2*2/2*2-2+(2*3)+(4+2)*5'
let stack = []
const res = computeExpression(expression)
console.log(res)

以下是详细的日志过程,记录了出栈和入栈的过程:

expression -12+23+3*2-2*2/2*2-2+(2*3)+(4+2)*5
split expression [  '-12', '+', '23', '+', '3',  '*',   '2', '-',  '2', '*',  '2',   '/', '2',  '*', '2',  '-',   '2', '+',  '(', '2',  '*',   '3', ')',  '+', '(',  '4',   '+', '2',  ')', '*',  '5']
push to stack -12
[ '-12' ] stack
push to stack +
[ '-12', '+' ] stack
push to stack 23
[ '-12', '+', '23' ] stack
push to stack +
[ '-12', '+', '23', '+' ] stack
can low
pop out of stack +
pop out of stack right 23
pop out of stack flag +
pop out of stack left -12
push result to stack 11
push to stack +
push to stack 3
[ 11, '+', '3' ] stack
push to stack *
[ 11, '+', '3', '*' ] stack
push to stack 2
[ 11, '+', '3', '*', '2' ] stack
can high
pop out of stack right 2
pop out of stack flag *
pop out of stack left 3
push result to stack 6
push to stack -
[ 11, '+', 6, '-' ] stack
can low
pop out of stack -
pop out of stack right 6
pop out of stack flag +
pop out of stack left 11
push result to stack 17
push to stack -
push to stack 2
[ 17, '-', '2' ] stack
push to stack *
[ 17, '-', '2', '*' ] stack
push to stack 2
[ 17, '-', '2', '*', '2' ] stack
can high
pop out of stack right 2
pop out of stack flag *
pop out of stack left 2
push result to stack 4
push to stack /
[ 17, '-', 4, '/' ] stack
push to stack 2
[ 17, '-', 4, '/', '2' ] stack
can high
pop out of stack right 2
pop out of stack flag /
pop out of stack left 4
push result to stack 2
push to stack *
[ 17, '-', 2, '*' ] stack
push to stack 2
[ 17, '-', 2, '*', '2' ] stack
can high
pop out of stack right 2
pop out of stack flag *
pop out of stack left 2
push result to stack 4
push to stack -
[ 17, '-', 4, '-' ] stack
can low
pop out of stack -
pop out of stack right 4
pop out of stack flag -
pop out of stack left 17
push result to stack 13
push to stack -
push to stack 2
[ 13, '-', '2' ] stack
push to stack +
[ 13, '-', '2', '+' ] stack
can low
pop out of stack +
pop out of stack right 2
pop out of stack flag -
pop out of stack left 13
push result to stack 11
push to stack +
push to stack (
[ 11, '+', '(' ] stack
push to stack 2
[ 11, '+', '(', '2' ] stack
push to stack *
[ 11, '+', '(', '2', '*' ] stack
push to stack 3
[ 11, '+', '(', '2', '*', '3' ] stack
can high
pop out of stack right 3
pop out of stack flag *
pop out of stack left 2
push result to stack 6
push to stack )
[ 11, '+', '(', 6, ')' ] stack
compute singleNumberInBrackets
pop out of stack right bracket )
pop out of stack number 6
pop out of stack left (
push number to stack 6
push to stack +
[ 11, '+', 6, '+' ] stack
can low
pop out of stack +
pop out of stack right 6
pop out of stack flag +
pop out of stack left 11
push result to stack 17
push to stack +
push to stack (
[ 17, '+', '(' ] stack
push to stack 4
[ 17, '+', '(', '4' ] stack
push to stack +
[ 17, '+', '(', '4', '+' ] stack
push to stack 2
[ 17, '+', '(', '4', '+', '2' ] stack
push to stack )
[  17,  '+', '(',  '4', '+', '2',  ')'] stack
can brackets
pop out of stack right bracket )
pop out of stack right 2
pop out of stack flag +
pop out of stack left 4
pop out of stack left (
push result to stack 6
push to stack *
[ 17, '+', 6, '*' ] stack
push to stack 5
[ 17, '+', 6, '*', '5' ] stack
can high
pop out of stack right 5
pop out of stack flag *
pop out of stack left 6
push result to stack 30
47

最后我们得到的结果是47, 然后把表达式复制到搜索引擎,计算一下吧:

image.png

最后的结果和我们计算结果一致。

后记

这里介绍了一下如何用堆栈来计算 一个“数学表达式”的字符串的结果。简单介绍了入栈和出栈的过程。