Clojure: 实现简单的数学表达式计算

左心房为你撑大大i 提交于 2019-11-30 00:48:18

我之前在知乎上回答了问题 按照运算符优先数法,画出算术表达式求值时,操作数栈和运算符栈的变化过程 。这次一方面也算是温故而知新,另一方面借此领略Clojure函数式编程之美。为了节省篇幅,本文只考虑 +、-、*、/ 这几种基本运算符以及只考虑小于10的整数运算。

栈变化图示

我们人类使用的算术表达式是中缀表达式,而计算机采用后缀表达式,也即逆波兰式。第一种方式是将表达式转为后缀表达式,再进行求值;第二种方式是直接对中缀表达式进行求值。本文就直接采用第二种方式了。

比如要对表达式 9-2*4+9/3 进行求值。首先需要使用2个栈来分别保存运算数和运算符,分别记为 opnd 和 optr。对表达式逐家符读入:

1、读入字符 '9',因为是运算数,则放入 opnd 栈中,剩余待读入字符串为:-2*4+9/3,此时 opnd 栈和 optr 栈如下图所示:


2、读入字符 '-',发现是运算符,则先和 optr 栈顶作比较,因为 optr 栈是空栈,故直接入栈 optr。剩余待读入字符串为:2*4+9/3,此时 opnd 栈和 optr 栈如下图所示:


3、接下来读入字符'2',发现是运算数,则入栈 opnd,剩余待读入字符串为:*4+9/3,此时opnd 栈和 optr 栈如下图所示:


4、接下来读入字符 '*',发现是运算符,则跟 optr 栈顶字符 '-' 进行比较,'*' 优先级高于 '-',因此直接将 '*' 入栈 optr,剩余待读入字符串为:4+9/3,此时 opnd 栈和 optr 栈如下图所示:


5、接下来读入字符 '4',因为是运算数,则直接入栈 opnd,剩余待读入字符串为:+9/3,此时 opnd 栈和 optr 栈如下图所示:


6、接下来读入字符 '+',发现是运算符,则跟 optr 栈顶元素 '*' 进行比较,发现 '+' 的优先级小于 '*',则将 optr 出栈,得到运算符 '*',将 opnd 出2次栈,得到字符 '4' 和 '2',令 '4' 和 '2' 分别作为运算符 '*' 的左右操作数(即 4 * 2)得到结果为 '8',将 '8' 入栈 opnd。剩余待读入字符为:9/3,此时 opnd 栈和 optr 栈如下图所示:


7、此时 '+' 运算符还未入栈 optr,继续与 optr 栈顶元素 '-' 进行比较,'+' 优先级不小于 '-',则出栈 optr 得到运算符 '-',将 opnd 出2次栈,得到字符 '9' 和 '8',令 '9' 和 '8' 分别作为运算符 '-' 的左右操作数(即 9 - 8)得到结果为 '1',将 '1' 入栈 opnd,此时 optr 栈为空,将 '+' 运算符入栈 optr,剩余待读入字符为:9/3,此时 opnd 栈和 optr 栈如下图所示:


8、读入字符 '9',入栈 opnd,剩余待读入字符串为:/3,此时 opnd 栈和 optr 栈如下图所示:

9、读入字符 '/',发现是运算符,跟 optr 栈顶 '+' 进行比较,因为 '/' 的优先级高于 '+',则直接将 '/' 入栈 optr,剩余待读入字符串为:3,此时 opnd 栈和 optr 栈如下图所示;


10、读入字符 '3',入栈 opnd,此时表达式已全部读取完成,此时 opnd 栈和 optr 栈如下图所示:


11、optr 栈不为空,则出栈得到运算符 '/',从 opnd 出栈2次,得到 '9' 和 '3' 分别作为左右操作数进行运算(即9/3),得到结果为 '3',并入栈 opnd。此时 opnd 栈和 optr 栈如下图所示:


12、optr 栈不为空,则出栈 optr,得到运算符 '+',从 opnd 出栈2次,得到 '1' 和 '3' 分别作为运算符 '+' 的左右操作数进行运算(即1+3),得到结果 '4'并入栈 opnd,此时 opnd 栈和 optr 栈如下图所示:


13、此时 optr 栈已为空,则将 opnd 出栈,得到 '4',即为表达式 9-2*4+9/3 的求值结果。

原理

1、首先需准备2个栈结构,分别用于保存操作数和运算符;

2、需对运算符的优先级进行划分;

3、表达式从左到右逐字符读入;

4、操作数直接入栈;

5、运算符入栈前需先进行判定:如果运算符栈为空,则直接入栈。否则取出栈顶,进行优先级比较,如果待读入运算符比栈顶运算符优先级高,则直接入栈待读入运算符。否则须先出栈,进行运算完后方入栈待读入运算符;

6、当执行计算时,从操作数栈中连续出栈2次,先出栈的为右操作数,后出栈的为左操作数;

7、当表达式全部读取完成,则以运算符栈是否为空为依据,依次出栈对操作数进行计算,计算的结果再次入栈操作数栈;

8、当运算符栈为空时,出栈操作数栈,即为此次表达式的计算结果。

实现

本文限于篇幅,只考虑的运算符为:+、-、*、/,操作数为小于10的整。所以无须处理复杂的运算符优先级关系,并且在读入表达式时,只须对单个字符判断为操作数或运算符。

一、为什么是Clojure

clojure是基于JVM的函数式编程语言,它借签了Haskell、Lisp等函数式语言的优点,并形成自己的独有风格。它也是一门动态语言:变量不可变,atom、ref、promise、future、delay使它天生适用于多核高并发开发任务。完备的宏定义、数据协议和类型、多重方法等赋予这门语言无限的可能。极尽JVM之所能,易于和java的互操作性,立于JVM的强大生态之上,又还有什么理由不选择Clojure呢?

二、安装Clojure

Clojure是基于JVM的语言,所以你应该已经安装好JVM环境了。

wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
mv lein ~/bin/
chmod a+x ~/bin/lein

lein脚本已经处于你的PATH之中,如若不然,请在 ~/.profile 里添加:

export PATH=~/.profile:$PATH

然后执行:

source ~/.profile

第一次执行 lein 会进行自我安装,安装完毕即可通过 lein repl 进入交互式开发环境。

我们通过 lein new 来创建项目:

lein new calc

三、代码实现

栈直接用java提供的 java.util.Stack 类,源代码在 src/calc/core.clj 中,如下:

(ns calc.core
  (:import [java.util Stack]))

; 运算符及优先级定义
(def ^:private op_map {\+ 5, \- 5, \* 6, \/ 6})

(defn isdigit? [c] (#{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9} c))

(defn -main [& args]
  (let [_expr (seq (nth args 0))]
    (loop [expr _expr opnd (Stack.) optr (Stack.)]
      (if (empty? expr)
        (do
          ;(println opnd optr)
          (loop [not_ept (not (empty? optr))]
            (if (not not_ept) (println (str "计算结果为:" (.. opnd (pop))))
              (do
                (let [op_c (.. optr (pop)) rn (.. opnd (pop)) ln (.. opnd (pop))
                      v (case op_c
                          \+ (+ ln rn)
                          \- (- ln rn)
                          \* (* ln rn)
                          \/ (/ ln rn))]
                  ;(println op_c ln rn v opnd optr)
                  (.. opnd (push v))
                  (recur (not (empty? optr))))))))
        (let [[c & _rest] expr] (if (isdigit? c) (.. opnd (push (Integer. (str c))))
                                  (if (empty? optr) (.. optr (push c))
                                                        (loop [not_ept (not (empty? optr))]
                                                          (if (not not_ept)
                                                            (.. optr (push c))
                                                            (let [op_c (.. optr (peek))]
                                                              (if (> (op_map c) (op_map op_c)) (.. optr (push c))
                                                                (do
                                                                  (.. optr (pop))
                                                                  (let [rn (.. opnd (pop)) ln (.. opnd (pop))
                                                                        v (case op_c
                                                                            \+ (+ ln rn)
                                                                            \- (- ln rn)
                                                                            \* (* ln rn)
                                                                            \/ (/ ln rn))]
                                                                    (.. opnd (push v))
                                                                    ;(println ln rn v opnd optr)
                                                                    (recur (not (empty? optr)))))
                                                                ))))))
          (recur _rest opnd optr))))))

在 project.clj 中配置入口,增加一行:

:main calc.core/-main

在命令行中执行:

$ lein run 9-2*4+9/3
计算结果为:4

此程序只是为了演示,并不通用。写惯了命令式程序,很容易被函数式语言弄得晕头转向。欲练神功,先自废武功~

另:感谢 敏敏郡主 ^_^






易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!