Windows 多功能计算器

  这是一款开源免费的多功能计算器,运行于 Windows。作为一款计算器软件,它可不只是能计算加减乘除而已。它支持输入一个复杂的表达式,并解析其中的语法错误,然后提示用户在错误处加入修改,直到没有语法错误为止。如果没有语法错误,它会自动计算结果,然后将计算过程显示在屏幕上。

  项目代码已在 GitHub 上开源免费发布,作者将对此项目提供持续更新与维护。


版本 M.3.1 支持的功能:(以用户的角度)

  1. 操作数支持多位运算。一个操作数可以是十位数或者更高位数的数

  2. 操作数支持小数点、负数,运算结果支持显示小数、分数。

  3. 表达式可以含多个操作数、多个运算符,还可以带括号

  4. 对输入表达式提供实时自动语法检查与错误定位,并支持检查后的修改

  5. 对输入无误的表达式进行实时自动无损计算,无运算累计误差,并可选显示详细的计算过程

  6. 使用 GUI 界面来显示上面的表达式输入、报错显示、运算过程

  7. 提供界面按钮以供鼠标点击输入。按钮功能包含击键变色、文本全选、光标左移与右移,选中文本的删除与替换、撤消与重做

  8. 提供右键菜单以供撤消、重做、剪切、复制、粘贴、删除、全选

  9. 支持键盘输入

  10. 提供 Windows 下免安装 JDK 直接运行的 EXE 文件。此 EXE 程序拥有程序名、自制程序图标


版本 M.3.1 运行效果图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

基本信息

开发环境

  • JDK 17.0.1 2021-10-19

  • Maven 3.8.3

  • IntelliJ IDEA 2021.2.2 (Ultimate Edition)

编程语言

  • Java

  • FXML

  • CSS

技术内幕

架构模式

  本项目采用了架构模式 MVC。具体来说,项目分为以下几个部分:

  • V:提供计算器各组件及主界面的实现

  • M:提供运算符、操作数等的底层实现

  • DAO:提供对 M 的一些常规处理与封装

  • Service:借助 DAO,对从 V 中传来的数据进行处理并反馈

  • C:用于实现 V 与 S 之间的通信与解耦

在这里插入图片描述

  图中本项目的各个组件之间使用 URL 来进行通信,对 RPC 技术进行了模拟。其中,URL 遵循 RESTful 规范,格式为 /组件路径/操作或数据名,如 /view/inputBox/leftShift 代表向前端的组件 inputBox 来发送 leftShift 信息、/service/expression 代表向后端发送 expression 信息。

设计模式

  本项目使用的设计模式有:

  • 职责链模式:
    • 用于实现多级控制器,完成视图与服务之间的通信与解耦
  • 适配器模式:
    • 用于解决 JavaFX 技术与 Spring 技术不相容的问题
  • 单例模式:
    • 用于保证全局“懒加载”变量的线程安全
  • 中介者模式:
    • 用于实现主界面中各组件之间的通信与解耦
  • 观察者模式:
    • 提供实时计算与计算过程实时显示的功能
  • 解释器模式:
    • 提供表达式解析的功能
  • 组合模式:
    • 将运算符、操作数归一化
  • 装饰模式:
    • 用于增强系统默认提供的 UI 组件
  • 原型模式:
    • 用于实现多种操作数等的复制问题
  • 工厂模式:
    • 用于异常类的创建
    • 用于各组件等的创建【版本 M.2.0 起废弃,改用 Spring 进行依赖注入与初始化】
  • 模板方法模式:
    • 提供协变运算方法的具体与抽象【版本 M.1.2 起废弃】

模块介绍

  本项目包含以下模块:

  • calculator-entrance:存放计算器的启动程序入口

  • calculator-exception:本项目需要使用的各种异常

  • calculator-ui:计算器的 UI 界面

  • calculator-ui-depended:此模块源自模块 calculator-ui。为了解决 Maven 模块间的循环依赖问题才分离出该模块

  • calculator-backend:计算器的后端程序

包名介绍

  本项目中的包名(省略前缀 org.wangpai.calculator)controller、model、service、view、exception 分别对应于上面提到的控制器、模型、服务、视图、异常。

测试

  • 本项目中的测试的类包名后缀如果为某测试框架的框架名,则表明该测试类在测试时使用了该测试框架。如后缀为 junit5,代表该测试类使用了测试框架 JUnit5 。

  • 如果包名后缀不为任何测试框架的框架名,而完全与被测试类的包名一致,则意味着被测试类往往为 GUI 组件,不方便使用测试框架进行测试。这种类的测试往往依靠手动运行来完成。

核心算法

操作数运算算法

  1. 将操作数与操作数之间的运算分离。
  2. 对所有的操作数定义一个抽象超类,但具体的操作数互相之间不设计为继承关系。
  3. 对所有的运算定义一个抽象超类,但具体的运算符互相之间不设计为继承关系。
  • 曾经的设计误区:

    1. 将所有的操作数按它们在数学上的范围,范围小的为超类,范围大的为子类。虽然从数学上,范围小的数是范围大的数的一种特殊状态,但从程序设计上讲,范围更大的数需要使用范围更小的数用做底层表示。比方说,虽然从数学上,整数是有理数的一个特殊状态,但从程序设计上讲,有理数可用整数分子、整数分母来表示。因此,只能让范围小的为超类,范围大的为子类。

    2. 将所有的运算类按操作数之间的继承关系,也设计相应的继承关系。

    3. 将运算分为协变运算与稳定运算。对于稳定运算,其形参的类型的稳定的。对于协变运算,其形参的类型将由操作数类型的扩大而多变,因此会形成多个重载方法。

      设计一种导航方法。对于协变运算(如加减乘除),只对外提供抽象运算类的协变运算导航方法。该方法将利用反射,根据形参的真实类型来自行决定需要调用的子类方法。

    4. 将方法的形参根据继承关系进行重排列。调用抽象类协变运算导航方法时,如果第一个形参是第二个形参的超类,就将它们交换顺序,然后才调用相应的具体运算方法。重排列能减少接近一半的具体协变运算方法的编写。

  • 为什么说上面的是设计误区:

    • 使用了反射,大大增加方法的调用的危险性,大大影响了代码的可读性,并让编译器的静态语法检查失效。

    • 使用的反射、导航方法、重排列,由于让编译器的静态语法检查失效,因此需要人为保证事先编写好所有可能用到的方法。但实际上,方法应该按需编写,且依靠人为保证会带来遗漏。

    • 不同范围上的数,虽然在数学上有包含关系,但从程序设计上讲,它们是一种组合关系。因为范围更大的数没有用上继承而来的字段,只使用了自己的字段,而自己字段的类型是超类的类型。

    • 将应该使用组合关系的类设计为继承关系,在方法调用上会存在协变的问题。

表达式检查的算法

  1. 对表达式进行【静态检查】和【动态检查】。如果检查不通过,向提示框发送异常信息,本算法结束。如果通过,进行下一步。其中,静态检查指的是,无需计算就能判断出是否存在语法错误。动态检查指的是,需要计算才能判断出是否存在语法错误。

  2. 判断输入的表达式是否不以等号结尾但以数字或结尾。如果是尝试补齐等号,然后进行下一步。如果不是,进行 5。

  3. 对补充等号的表达式进行【静态检查】和【动态检查】。如果通过,进行下一步。如果检查不通过,说明表达式不完整但没有语法错误,且自行添加等号是不合理的,那么就向提示框发送默认信息,本算法结束。

  4. 对补充等号的表达式进行实时计算,并向提示框发送计算结果。

  5. (进行到此处说明 1、2 均为通过,3 不通过。)判断输入的表达式是否完整,如果是,向结果框发送计算过程,本算法结束。如果不是,进行下一步。

  6. 如果用户输入的只是等号,本算法直接结束。如果是其它情况,向提示框发送默认信息,本算法结束。

静态检查与动态检查算法

  1. 制作一个输出流类,用该表达式初始化该输出流,使之从表达式左边开始依次输出表达式元素。

  2. 进入循环。循环的继续条件是,【输出流中不为空】且【运算符栈为空或运算符栈最上面的一个运算符为等号】

    1. 对输出元素进行静态检查。从输出流取出一个元素,判断它和前面已读取的表达式之间有没有语法错误。如果有,反馈异常,本算法结束。如果没有,进行下一步。

    2. 对输出元素进行动态检查。对前面通过静态检查的元素加入已读取表达式中,并判断此表达式是否可以进行部分计算。如果可以,进行下一步。如果不可以,进行下一次循环。

    3. 对表达式进行部分计算。如果在此计算过程中发生错误,反馈异常,本算法结束。如果没有,进行保存计算结果等相关操作,然后进行下一步。

  3. 上面的循环结束后,本算法结束。

单个字符语法检查的算法【静态检查算法】

  • 此算法需要与下面的 表达式计算的算法 交替配合使用,一般先执行一次本算法,然后执行一次表达式计算的算法,接着再调用本算法,以此类推。

  • 只需要考虑当前输入的符号以及当前输入的符号与其之前的符号之间有无错误,不需要考虑当前输入的符号与其之后的符号之间有无错误。

  • 如果本算法没有报错,则视为通过了静态检查。

  1. 如果在一个操作数里,输入了两个小数点,报错。

  2. 如果前面没有第一个操作数或右括号,但是后面输入了非数字(例外有:左括号、一元运算符),报错。

  3. 连续输入两个非数字符,例外是【第二个为左括号,第一个为其它】、【第一个为右括号,第二个为其它】

  4. 如果本次输入的是数字:

    1. 如果输入的数字不属于一个小数的小数部分,但是其最高位又为 0,报错。

    2. 如果输入的数字前面有右括号,报错。

  5. 如果左括号前面有数字、小数点,报错。

  6. 如果输入的是右括号:

    1. 如果输入的右括号前面直接为左括号,报错。

    2. 如果右括号前面有非左括号的运算符,报错。

    3. 将已读取表达式备份,然后检查加入输入的右括号后,表达式是否满足括号匹配。如果是右括号多于左括号,报错。其它情况不报错。

  7. 如果输入的是等号,检查已读取表达式是否满足括号匹配。如果不满足,报错。

表达式计算的算法【动态检查算法】

  1. 如果输入字符为等号且运算符栈为空,将操作数缓存栈中的数加载到操作数栈计算表达式栈,将等号加入运算符栈计算表达式栈表达式栈,然后将等号退回输出流,最后本次算法结束。如果不是,进行下一步。

  2. 如果输入字符为数字或小数点,将其加入操作数缓存栈,然后本次算法结束。如果不是,进行下一步。

  3. 运行到此时,说明输入字符为普通的运算符,那么就将操作数缓存栈中的数加载到操作数栈计算表达式栈。然后进行下一步。

  4. 如果运算符栈为空,将上面输入的运算符加入运算符栈计算表达式栈表达式栈,然后本次算法结束。如果不是,进行下一步。

  5. 比较上面输入的运算符与运算符栈栈顶的运算符的优先级。根据优先级的比较结果来进行相应的操作,然后本次算法结束。其中,如果优先级的比较结果为:

    • 小于:将上面输入的运算符加入运算符栈计算表达式栈表达式栈

    • 等于:这说明最近的两个运算符是一对括号,且此对括号中就只包含一个操作数。因此,需要将左括号从计算表达式栈表达式栈弹出

    • 大于:

      1. 运算符栈中弹出一个提供运算的运算符,从操作数中弹出两个操作数,从计算表达式栈弹出三个元素。然后根据弹出的元素进行计算,并进行下一步。

      2. 如果计算中没有发生异常,进行下一步。如果发生异常,就反馈动态检查结果失败,然后本次算法结束。

      3. 将运算结果保存至操作数栈计算表达式栈,然后将本次输入的运算符退回输出流,最后本次算法结束。

  • 该算法解决的难点:

    1. 构造了一个输出流类,提供输入退回、剩余字符转化等功能,这大大简化了本算法及相关算法的复杂度。

    2. 使用有理数作为操作数,实现了运算过程中的无损运算。

    3. 不将小数点看作普通的运算符,而看成操作数的一部分。这样能减少对运算符优先级的比较,降低运算符优先级比较算法的复杂度。

    4. 构造了小数类型,便于将操作数缓存栈中含小数点的字符数组转化为有理数。

    5. 运算符栈操作数栈分离,这样能便于优先级的比较和数的单次运算,回避了运算符与操作数类型不兼容、不能轻易保存在同一集合的问题。

    6. 引入计算表达式栈。它利用了 Java 中所有的类型都直接或间接继承至类 Object 的原理,将原本类型不相关的运算符与操作数保存在同一集合中,解决了运算符与操作数类型不兼容的问题。这大大简化了显示计算过程的算法复杂度。

显示计算过程的算法

  1. 制作一个输出流类,用该表达式初始化该输出流,使之从表达式左边开始依次输出表达式元素。

  2. 显示一次用户已输入的表达式。

  3. 进入下面的循环。循环的继续条件是,【输出流中不为空】且【运算符栈为空或运算符栈最上面的一个运算符为等号】。此循环结束后,本算法结束。

  4. 如果输入字符为等号且运算符栈为空,将操作数缓存栈中的数加载到操作数栈计算表达式栈,将等号加入运算符栈计算表达式栈表达式栈,然后将等号退回输出流,最后本次算法结束。如果不是,进行下一步。

  5. 如果输入字符为数字或小数点,将其加入操作数缓存栈,然后本次算法结束。如果不是,进行下一步。

  6. 运行到此时,说明输入字符为普通的运算符,那么就将操作数缓存栈中的数加载到操作数栈计算表达式栈。然后进行下一步。

  7. 如果运算符栈为空,将上面输入的运算符加入运算符栈计算表达式栈表达式栈,然后本次算法结束。如果不是,进行下一步。

  8. 比较上面输入的运算符与运算符栈栈顶的运算符的优先级。根据优先级的比较结果来进行相应的操作,然后进行下一次循环。其中,如果优先级的比较结果为:

    • 小于:将上面输入的运算符加入运算符栈计算表达式栈表达式栈

    • 等于:这说明最近的两个运算符是一对括号,且此对括号中就只包含一个操作数。因此,需要将左括号从计算表达式栈表达式栈弹出

    • 大于:

      1. 运算符栈中弹出一个提供运算的运算符,从操作数中弹出两个操作数,从计算表达式栈弹出三个元素。然后根据弹出的元素进行计算,并进行下一步。

      2. 如果计算中没有发生异常,进行下一步。如果发生异常,就反馈动态检查结果失败,然后本次算法结束。

      3. 将运算结果保存至操作数栈计算表达式栈,然后将本次输入的运算符退回输出流,进行下一步。

      4. 计算表达式栈、输出流的内容拼接输出。

大整数相除防溢出递归算法

  • 本算法解决的问题:有两个大整数,已经开发出来大整数之间的 求整数商求余数运算。现在来求它们之间的除运算,并将结果转化为 double 类型。并假设这两个大整数,它们超出了 double 类型的范围,但是它们的商在 double 类型的范围之内。

  • 算法核心思想:

    1. 先把大整数拆成一些小整数。由于这种拆分通过一种 求余 运算即可实现,因此是可行的。

    2. 然后根据分数的性质,分子、分母同时除以一个数,其结果不变。所以再将分子、分母同时除以一个数就可以让分子、分母都变小。

      这种除法对大整数而言是一种 求整数商 运算,因此是可行的。

      这种除法对小整数而言是一种在浮点数范围内的浮点数除法运算,因此也是可行的。

    3. 使用递归的方法重复【1】、【2】的操作,直至分子、分母均位于 double 类型的范围内。

    4. 将分子、分母转化为 double 类型,然后使用浮点数除数运算得出结果。

通信算法

  1. 如何低耦合地获取接收者的句柄?

    原待改进设计:一个控件拥有可以直接访问任何其它控件对象的权限。

    答:改为获取控制器的句柄。

  2. 如何来使控制器拥有不同的行为来真正取代目标控件。

    原问题:控制器一定是没有目标控件的方法的,那么信息发送者如何把控制器当成目标控件呢?

    答:使用 URL。在 URL 中给出具体的目标控件信息与方法名、参数等。

  3. 如何优化控制器的设计,避免一个控制器的算法过于复杂。

    原问题:

    1. 如果只使用一个控制器,则该控制器将直接与所有的控件进行通信,职责过重。如果使用多个控制器,如何决定哪个控件与哪个控制器进行通信。

    2. 如果使用多级控制器,每一个控制器如何判断发送给它的信息来自上级还是下级?

    答:将通信行为划分成 4 个方法,2 个接口,使用多级控制器与具体控件进行通信。

    具体来说,将控制器分为 2 种(分别实现上述的 2 个接口),每种控制器有 2 个不同的方法。

    • 一种控制器是中间控制器。它分为两种:

      • 非中央控制器。它只做 2 件事情:

        • 将下级控制器向上传递的信息直接原封不动地向上传递。【方法 passUp】

        • 将上级控制器向下传递的信息继续向下传递。它需要决定将信息向下发送给哪个下级控制器。【方法 passDown】

      • 中央控制器。它只做 2 件事情:

        • 将下级控制器向上传递的信息准备向下传递。【方法 passUp】

        • 决定将向下传递的信息传递给哪个下级控制器。【方法 passDown】

    • 另一种是终端控制器。它要做 4 件事情:

      • 将来自具体控件的信息交给【方法 passUp】来处理。【方法 send】

      • 将来自【方法 send】的信息向上传递。【方法 passUp】

      • 将上级控制器向下传递的信息交给【方法 receive】来处理。【方法 passDown】

      • 将来自【方法 passDown】的信息发送给具体控件。【方法 receive】

    这样划分的理由:

    1. 当控制器接收到下层控制器发送的信息时,它并不知道应该如何找到接收方,所以它只能将信息发送给中央控制器,让中央控制器来判断。而找到中央控制器的办法就是,将信息发送给上级控制器。

    2. 当控制器接收到上层控制器发送的信息时,它清楚的知道这个消息只能向下传递。不过,它还需要判断发送给哪个下级控制器。

    3. 可以按照不同组件的层次,分级处理信息,减少中央控制器的复杂度。虽然这种设计使得每条信息都要经过中央控制器,好像没有减轻中央控制器的负担。但实际上,中央控制器只需要决定将消息传给哪个二级控制器,而不需要定位到具体的控件。因为二级控制器的数量一定小于具体控件的数量,所以这确实减轻了中央控制器的负担,而将原来的负担分散给了各级控制器。

    4. 可以实现前端界面与后端服务之间的解耦。

    5. 每个控制器都是无状态的。即便是在多线程中,也不用担心线程安全的问题。

    6. 所有的 UI 组件都无逻辑的。即 UI 代码中不包含任何处理数据的逻辑,它只会向后端发送与接收数据。

  4. 本算法与一般的需借助 Tomcat 服务器的 MVC 项目有什么关联之处?

    答:本项目中的控制器还承担了路由选择的工作,而对于一般的 MVC 项目,此工作是由网络中的各级路由器与 Tomcat 服务器来共同完成的。因此,如果将本项目应用于一般的 MVC 项目,只需要保留本项目中信息向下传递的机制,而去掉信息向上传递的机制即可。

独出心裁的设计

将 Map 当做 Redis 数据库来使用

  将一个 Map 作为全局变量给出,这样一来,一个地方的对象可以向这个 Map 对象存放一些数据,另一个对象可以通过键值取出该数据。

懒执行

  将所有的除 UI 界面的初始化操作放到一个新建线程里执行,这样就能大大提高应用(UI)的启动时间,改善了用户体验。

JavaFX 与 Spring 适配

  Spring 是通过 XML 文件来利用普通的 Java 类,而 JavaFX 也正好类似,但这正是它们不相容的原因。包含它们数据的 XML 文件之间肯定是不能直接通信的,但它们利用的 Java 类只是一个光有算法的空壳,外界也无法通过这个 Java 类来直接与它们进行通信,因此令 JavaFX 与 Spring 使用同一个 Java 类也无法综合它们的长处,只会让这个类的职责过重。因此,只能让 JavaFX 与 Spring 分别使用不同的类,然后让它们建立连接。具体算法如下:

  1. JavaFX 与 Spring 都有一个创建它们的元件的容器,从该容器中可以提取它们创建的元件。从中选择一个方便操作的容器,这里选择的是 Spring 容器。

  2. 将 Spring 容器作为全局变量给出,所有的类都可以直接获得该容器。

  3. 为每个 JavaFX 组件设计一个 Linker 类,这个 Linker 类对象一方面与原来的 Spring Bean 相联系,另一方面与其中一个 JavaFX 组件相绑定。

  4. 如何实现上面的两方面绑定?方法如下:

    • 与原来的 Spring Bean 相联系:为了能方便做到这一点,可以让这个 Linker 对象也成为一个 Spring Bean,然后利用基于注解的依赖注入即可。

    • 与其中一个 JavaFX 组件相绑定:一个 JavaFX 可以使用一个 Java controller 类作为控制外壳,可以让这个 controller 类从 Spring 容器中获得这个 Linker对象,然后与之绑定。

使用 URL 来方便地进行各 UI 组件的之间的交互

  在前端框架中(如 React),一般使用 props 等来实现父组件与子组件之间的通信。但在 JavaFX 中,FXML 没有提供这种功能。这里由于实现了 URL 控制器与 Linker,因此各组件可以互相使用 URL 来直接进行跨级调用,而且可以通过控制对 URL 解析来自由决定对外开放的方法。其中,URL 遵循 RESTful 规范。

  借助于本项目的 通信算法,可以做到任意 UI 语言中任意组件之间的通信,而与该 UI 语言有没有提供组件间的通信机制无关。

旧算法

操作数运算算法(旧版本 M.1.1)

  1. 将操作数与操作数之间的运算分离。

  2. 对所有的操作数定义一个抽象超类,并将在数学上范围大的数定义为范围小的数的子类。

  3. 对所有的运算定义一个抽象超类,但具体的运算符之间不设计为继承关系。

  4. 将运算分为协变运算与稳定运算。对于稳定运算,其形参的类型的稳定的。对于协变运算,其形参的类型将由操作数类型的扩大而多变,因此会形成多个重载方法。

    为了减少重载方法的个数,将对二元及以上的运算的操作数进行排列,将它们中的子类排列在前,超类排列在后,然后再调用相应的运算方法。这样的好处是,可以不编写形参类型为超类在前、子类在后的运算方法,减少编写的运算方法个数。

    具体的步骤是,对某一个运算类的某一个名称的运算方法,先定义一个形参类型为操作数的抽象超类的导航方法。该使用反射检查形参的类型,并将它们按继承关系,将子类排列在前。然后按排列后的形参顺序调用相应的方法,同时追加相应的补充运算来抵消重排列操作数带来的副作用。

  • 该算法解决的难点:

    • 继承一般情况下,应该是含义上更通用、抽象,范围上更大的概念作为超类,但本设计则相反。以整数、有理数为例。看起来整数应该是有理数的特例,但从设计上,有理数内部需要用整数来构造。如果将整数定义为有理数的子类,则在依赖上将形成循环:创建一个有理数时,需要先创建它的整数分子、分母,因此会调用整数的构造器。又因为有理数是整数的超类,所以在创建整数时,会调用有理数的构造器。这就形成了循环调用。

      由于协变运算的重载,为了减少重载方法的个数,这里也不适合使用组合来表示,下面将解释其中的缘由。

    • 形参的重排序。对于协变运算,由于操作数类型多样,需要编写的重载方法较多。考虑到不同数的范围有大小之分,可以将范围大的数放在前面,从而减少接近一半的重载方法的编写。

    • 操作数类型的协变。对于协变运算,由于操作数之间的继承关系,可能会导致超类的运算类去处理子类的操作数。然而由于上面的继承倒置,超类的运算类不支持去处理子类的操作数。

      一种解决办法是,让子类运算类全覆盖超类的协变运算方法,不过由于协变方法的重载众多,如果覆盖出现遗漏,这将导致风险。

      另一种解决办法是,解除具体运算类之间的继承关系,而保留操作数之间的继承关系。这在一定程度上可以降低调用不当重载方法的风险,不过不能从根本解决这个问题。

显示计算过程的算法(旧版本 M.1.2)

(假设输入的表达式已经通过了语法检查以及预计算。)

  1. 如果表达式为空,什么也不干,本算法结束。否则,进行下一步。

  2. 将等号前面的原表达式输出,然后进行下一步。

  3. 将原表达式反转,以提供输入流供输入。

  4. 进入循环。当循环结束时,进入 6。循环的持续执行条件是:(下面的条件满足一个即可)

    1. 输入流不为空
    2. 已读取表达式为空
    3. 最近一个已读取字符(不是当前正在读取的字符)为等号
  5. 循环中某一次的内容为:

    1. 如果当前正在读取的字符为等号,且运算符栈为空,将操作数缓存栈的数转换成完整操作数后加入操作数栈并将已读取表达式栈插入一个转换符。然后将正在读取的字符加入运算符栈已读取表达式栈中,最后进入下一次循环。

      如果不是,进行下一步。

    2. 如果当前正在读取的字符不为上面的条件,且为数字或小数点,将正在读取的字符加入操作数缓存栈,然后从输入流中读取一次输入,进入下一次循环。

      如果不是,进行下一步。

    3. 如果当前正在读取的字符不为上面的条件,说明本次的输入是一个运算符。于是将操作数缓存栈中的数转换成完整操作数后加入操作数栈并将已读取表达式栈插入一个转换符,然后进行下一步。

    4. 如果运算符栈为空,将运算符送入运算符栈已计算表达式栈,然后从输入流中读取一次输入,进入下一次循环。如果不为空,进行下一步。

    5. 比较当前输入的运算符与已读取的最近一个运算符的优先级。如果结果为:

      • 小于:将运算符送入运算符栈已计算表达式栈,然后从输入流中读取一次输入,进入下一次循环

      • 等于:说明比较的这两个运算符为成对的括号,这就是说,这对括号里面没有其它运算符,就只有一个操作数。那么就将左括号从运算符栈已计算表达式栈中弹出。将左括号从已计算表达式栈中弹出的方法是,先将前面提到的操作数弹出,然后将左括号弹出,最后将前面被迫弹出的操作数送回栈中。

      • 大于:从操作数栈中弹出这二元运算符的两个操作数,从运算符栈中弹出这个二元运算符,从已计算表达式栈中弹出三个元素。然后使用这些弹出的操作数、运算符进行计算,并将计算结果送入操作数栈,将已读取表达式栈插入一个转换符。最后展示计算结果,将至少进行了一次运算的标识设为 true,进入下一次循环。

        展示计算结果的算法如下:

        1. 输入流栈已读取表达式栈备份。

        2. 将当前读取的运算符加入备份的输入流栈,然后将该栈反转。

        3. 备份的已读取表达式栈备份的输入流栈进行拼接。

        4. 输出备份的已读取表达式栈

  6. 如果在循环中,至少进行了一次运算,最后将结果转化为浮点数显示。如果没有, 说明表达式过于简单,就将原表达式输出。

源代码

已上传至 GitHub:

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值