使用栈和后缀表达式解析算术表达式

最近补习数据结构(谁叫我不是科班出身呢),看了栈的应用之一,做算术表达式解析计算,但那个例子中没有把括号的逻辑加进去,苦想一段时间后还是没有结果,于是Google出一些表达式解析的方案,觉得用后缀表达式解析是最方便的,遂用Ruby实现了一个,支持由数字、加减乘除和括号组成的表达式,因为主要目的是对栈的应用,所以一些地方能简则简。

class MyParser
  PRECEDENCE = [
    ['(', ')'],
    ['|'],
    ['&'],
    ['+', '-'],
    ['*', '/']
  ]
  
  REG_VARIABLE = /\d+(?:\.\d+)?/
  REG_OPERATOR = /[\+\-\*\/\|\&]/
  REG_PARENTHESIS = /[\(\)]/
  REG_SPACE = /\s+/
  
  def self.exec(exp)
    exec_postfix parse_to_postfix(exp)
  end
  
  # 将表达式解析成后缀表达式,这里后缀表达式改用数组
  def self.parse_to_postfix(exp)
    postfix_exp, stack = [], []
    # 每次从exp中取出第一段标志符(数字,操作符,括号),进行分析,exp中的标志符取完后退出循环
    until exp.nil? or exp.size == 0
      case exp
      when /^(#{REG_VARIABLE})/ # 数字
        token = $1
        postfix_exp << token
      when /^(#{REG_OPERATOR})/ # 操作符
        token = $1
        # 这个if和else可以合在一起的,但我觉得这样逻辑更清晰,脑袋笨了冒得法啊
        if stack.empty?
          stack << token
        else
          # 原来这里的逻辑错了,现已改正
          # 当读取的操作符优先级不高于栈中的操作符优先级时,需要依次将栈中的操作符弹出送到表达式中
          while stack.size > 0 && precedence(token) <= precedence(stack.last)
            postfix_exp << stack.pop
          end
          stack << token
        end
      when /^(#{REG_PARENTHESIS})/ # 括号
        token = $1
        case token
        when '('
          stack << token
        when ')'
          postfix_exp << stack.pop while !stack.empty? && stack.last != '('
          # 因为一般')'都会有匹配的'(',正常情况下这里stack的最后一个元素一定是'('
          raise "mismatched parenthesis '#{token}'" if stack.last != '('
          stack.pop
        else
          raise "parenthesis '#{token}' wrong"
        end
      when /^(#{REG_SPACE})/ # 空格,这个只是额外处理,和表达式转换的核心逻辑无关
        token = $1
      else
        raise "unknown token! expression is '#{exp}'"
      end
      exp = exp[token.size..-1]
    end

    until stack.empty?
      # 检查括号是否匹配的,因为stack中只会压入'(',不会压入')'
      # 如果表达式括号匹配的话,到这里stack中所有的'('应该都已经被处理了
      raise "mismatched parenthesis '('" if stack.last == '('
      postfix_exp << stack.pop
    end
    postfix_exp
  end
  
  # 根据后缀表达式计算结果
  def self.exec_postfix(postfix_exp)
    stack = []
    postfix_exp.each do |token|
      case token
      when REG_VARIABLE # 变量直接压入stack
        stack << token.to_f
      when REG_OPERATOR # 碰到操作符,从stack中弹出两个数,进行计算,再将结果压入
        raise 'stack size < 2'if stack.size < 2
        d2, d1 = stack.pop, stack.pop
        stack << exec_exp(d1, token, d2)
      end
    end
    raise 'final stack size must be 1' if stack.size != 1
    stack.first
  end
  
  # 计算操作符的优先级,返回一个数字,数字越大优先级越高
  def self.precedence(op)
    re = PRECEDENCE.index { |group| group.include?(op) }
    raise "unknown operator '#{op}'" if re.nil?
    re
  end
  
  # 输入两个数和二元运算符,进行计算,返回结果
  # 例:exec_exp(1, '+', 2)  =>  3
  def self.exec_exp(d1, op, d2)
    case op
    when REG_OPERATOR
      d1.send(op, d2)
    else
      raise "operator '#{op}' wrong"
    end
  end
end

# 测试一下,看算的对不对
puts 'TEST CALCULATE'
[
  ['1+2*3', 7.0], # 前面是表达式,后面是期望的计算结果,下同
  ['1*2-3', -1.0],
  ['10-(4-2)*3', 4.0],
  ['2*(24-(3*10))+11', -1.0]
].each do |exp, result|
  puts exp
  postfix_exp = MyParser.parse_to_postfix(exp)
  puts "  #{'parse'.ljust(9)} = #{postfix_exp}"
  puts "  #{'calc'.ljust(9)} = #{MyParser.exec_postfix postfix_exp}"
  puts "  #{'expect'.ljust(9)} = #{result}"
end

# 测试后缀表达式是否正确
puts 'TEST POSTFIX EXPRESSION'
[
  ['1|2&3+4|5', '1234+&|5|'],
  ['1-2|3*4&5', '12-34*5&|'],
  ['(1|2)&3+(4|5)', '12|345|+&']
].each do |exp, result|
  puts exp
  postfix_exp = MyParser.parse_to_postfix(exp).join
  puts "  #{'parse'.ljust(9)} = #{postfix_exp}"
  puts "  #{'expect'.ljust(9)} = #{result}"
end

运行 结果如下:

1+2*3
  parse     = ["1", "2", "3", "*", "+"]
  calc      = 7.0
  expect    = 7.0
1*2-3
  parse     = ["1", "2", "*", "3", "-"]
  calc      = -1.0
  expect    = -1.0
10-(4-2)*3
  parse     = ["10", "4", "2", "-", "3", "*", "-"]
  calc      = 4.0
  expect    = 4.0
2*(24-(3*10))+11
  parse     = ["2", "24", "3", "10", "*", "-", "*", "11", "+"]
  calc      = -1.0
  expect    = -1.0

 嗯,至少没算错……

下面说说大概思路:

基本逻辑就是两步,先把输入的表达式(我们一般的表达式写法都是中缀表达式)转换成后缀表达式,然后用后缀表达式进行计算。比如这样:

# 中缀表达式,我们通常写的形式
1*2+3
# 这是后缀表达式的写法,基本上是把运算符放到数字后面,
# 读法为从前往后读,碰到运算符,就把运算符前的两个数字代入计算,计算结果就取代了原来的两个数字和运算符,以此类推
12*3+
# 再看个复杂点的,中缀表达式如下
1+2*3
# 后缀表达式,
123*+
# 这个是有括号的中缀表达式
(1+2)*3
# 换成后缀表达式就是这样,可以看出,后缀表达式是没有括号和运算符优先级的,比较方便由计算机处理
这正是要转换成后缀表达式的原因
12+3*

后缀表达式很容易被计算机处理,逻辑也不复杂。

其实数据结构教程里的示例代码的原理和上面的Ruby代码十分类似,只是一个直接计算,一个是转换成后缀表达式。

个人感觉数据结构的示例,把对栈的操作和数据计算混在一起,比较乱。而且要处理括号的逻辑,可能要采用递归才能实现,比较麻烦。

 

参考资料

 

这篇文章是思路来源,我用Ruby重新实现了一把,补充一些细节,里面讲表达式转换逻辑和后缀表达式计算逻辑比较详细

利用堆栈解析算术表达式

这篇是我原来比较倾向的思路,完全采用递归,将表达式不停拆分解析,直到最基本的元素,可惜代码太长,比较复杂,懒得看了

解析表达式--Java编程艺术

这是RednaxelaFX 童鞋补充的资料,Wikipedia上表达式转换的文章,感叹一句:砖业人士啊

Shunting-yard algorithm

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值