表达式计算器 ExpressionRunner

表达式计算器 ExpressionRunner

实践目的

本次实践的主要任务是做一个表达式计算器,并且在一般的表达式求值方面额外地添加一些功能。

问题定义及题目分析

简单的说,该软件大体分成三部分:实现基本的带数字的表达式计算;可以将一个值赋给一个变量,同时让变量也参与运算;可以查询所用到的变量的值,并且实现在此基础上的修改或删除。具体细化此次实践的任务,主要分成 5 个部分:

表达式求值部分:

首先是需要明确参与计算的数据类型和运算符。数据由于考虑到会加入自定义变量,所以整个表达式中,既有数字量,也有代数量。针对运算符方面,规定允许有括号,但是所有的括号统一用小括号代替,这一点和绝大部分的编程语言保持一致。其次是需要实现的运算,除了基础的加减乘除以外,另外添加一个乘方运算,更贴近实际应用。

之后需要考虑的是实现整个计算的方法。如果假设任意一种运算的时间开销相同,针对一个有 N 次计算的表达式,朴素的通过分别建立两个堆栈(数据栈和运算符栈)来解析表达式的时间效率为 O(N ),基本满足使用需求。由于不能保证表达式的正确性,而且逆波兰式的容错能力和针对错误的反馈能力确实较欠缺,所以并不是把表达式转换为逆波兰式来进行计算。实际上是用了更为一般朴素的方法,以防止运行中出现不寻常的错误。

变量赋值

同样的,变量赋值需要考虑的因素也比较多。变量名是否合法,是否有赋值符号,赋值符号的右边的表达式是否合法都是需要考察的。

为了避免歧义,规定变量名只能是一段首字符不为数字的只包含大小写字母和数字字符串。其次,参考 Pascal 规定:=符号为赋值符号。赋值符号是一个双目运算符,左端是变量名,右端是值。值可以是简单的数据,也可以是一个表达式。

变量操作

变量操作的核心在于用什么样的数据结构去维护这些变量。在这里我给的方案是选择二叉平衡搜索树,也就是红黑树。维护的主要方式是通过建立数据(字符串和数值)与键值的一一对应关系,通过键值建立平衡树,来实现针对变量的各式各样的操作和管理。

目前准备实现的操作主要有查询(所有的变量或者单个变量),复制,删除,重命名。所有的命令语句参照 Linux 的命令设计,严格执行语法检测。

异常抛出

异常主要分为如下两类:编译错误(Compile Error)和运行时错误(Runtime Error)。

编译错误,顾名思义,不考虑该语句是否能够成功运行,只考虑其语法上的问题。这类问题包括语法错误(命令缺少或多出参数),变量名出错(不符合变量名的命名规范),缺少运算符或缺少运算数据错误,以及未知命令(诸如直接打一个符号)。

运行时错误,主要是在语句运行中发生的异常。这类问题包含数学异常(除以 0 等等数学问题),未知的变量(是用了一个并没有定义的变量)以及变量操作失败(其他运行时的异常导致的失败)。

界面设计

在界面处理上,由于开发时间问题,添加图形化界面需要 1-2 周的开发时间才能完成软件的正常使用。显然开发时间不够。再者,对于一个设计思路来源于命令和程序设计语言的软件,图形化界面的有无并不会起到太大的作用,所以直接放弃了图形化界面的开发。

整体上的环境界面设计思路是参照 Linux 的终端 Terminal 的界面思路,但是考虑到并不是一个集文件操作与管理于一身的小型命令解析器,所以关闭了文件路径回显,类似于 Python 的 Command Line 模式,仅仅是表达式求值操作时才会打开回显模式,而赋值语句和不需要显示结果的命令均关闭了命令回显模式。

另外借鉴 Python 中直接新建变量和直接对输入的表达式进行计算的优点,使得软件的使用更为的方便。

概要设计

表达式与变量的定义

字母的定义:

个大小写字母

数码的定义:

0~9 共 10 个数码

实数的定义:

即为程序设计语言中的实数定义

变量名的定义:

<Var> ::= <Letter> | <Var><Letter> | <Var><NumLetter>

运算量的定义:

<Num> ::= <Var> | <Real>

单目运算符的定义:

<Opr1> ::= '()'

双目运算符的定义:

<Opr2> ::= '+' | '-' | '*' | '/' | '^'

运算式的定义:

<Run> ::= <Num> | <Opr1><Num> | <Num><Opr2><Num>

运算表达式的定义:

<Exp> ::= <Run> | <Opr1><Run> | <Num><Opr2><Run> | <Run><Opr2><Num> | <Run><Opr2><Run>

变量和表达式的输入方法

借鉴 Python 中直接输入表达式就运行计算的优点,所有直接输入的表达式将直接运行计算;

借鉴 Python 中直接新建变量的方法,不需要前置说明变量的类型;

借鉴 Pascal 的赋值运算符:=,对变量的赋值语句格式写为":=",通过赋值运算符来实现对变量的赋值;

借鉴常用计算器中的 Ans 默认变量,即默认存储上一次计算的结果;

如果在表达式中出现了没有定义的变量,则报错;

不允许循环定义,类似"a := a + 1 "的变量定义表达式是非法的。

变量的查询方法

借鉴 Linux 的 ls 命令,该命令的作用是将所有的变量展示出来

借鉴 Linux 的 cat 命令,该命令的作用是显示这个变量的值

借鉴 Linux 的 cp 命令,该命令的作用是把一个变量的值赋给另一个变量

借鉴 Linux 的 rm 命令,该命令的作用是删除一个变量

借鉴 Linux 的 mv 命令,该命令的作用是修改一个变量的变量名

主控制台的控制命令

借鉴 exit 命令,该命令的作用是终止程序

借鉴 MSDOS 的 cls 命令,该命令的作用是清空所有的已经设置的变量

详细设计

表达式运算和赋值

Expression 类

设计核心

首先是实现的方法,表达式计算的主要实现是通过堆栈来实现的。即建立两个堆栈,运算符栈 sOpr 和数值栈 sNum。每进行一次运算,则 sOpr 弹出一个运算符,sNum 弹出两个数据,将结果压入 sNum 中。

其次是代码的实现方法。最终选取的是标准库的堆栈 stack 来进行维护,同时建立一个映射表,用于体现运算的优先级。所需要考虑的操作主要是一次简单运算操作,也就是“弹出数据,弹出运算符,计算,将新数据压入栈中”这样的一组操作。

最后,表达式的储存用字符串,答案用 double 记录。同时增加 toString 方法来表示包括答案在内的运算结果。公开的计算方法是 run,公开的打印方法是 print。

结构

在这里插入图片描述

  • 解释
  • 构造函数即是用一个表达式来进行构造,并且计算并直接赋值。
  • 共有的方法中包含 toString()方法和 print()方法。主要作用是对其进行整理并输出。

私有的方法中包含初始化函数_init(),两个数据通过运算符 op 的规则进行计算的函数_calc(),栈中弹出数据并计算的过程_operation(),以及针对整个字符串的计算函数_calculate()。

核心函数_calculate 的思路如下:除了维护两个堆栈以外,额外维护一个指示器,用于表示当前读入的位置。针对读入方法分以下几种分别处理:当读入的是一个变量名时,将检查变量,并且做出相应的处理;当读入的是一个实数时,将直接将其压入相应的栈中;当读入的是左括号时,将运算符压入运算符栈中;当读入的是后括号时,表示这时需要进行计算,所以只要当前的运算符栈的栈顶元素不为左括号,就进行数据弹出并计算;当读入的是一个运算符时,只要元素符栈非空,并且其栈顶的运算符的优先级比当前读入的更高时,表示表示需要计算此时读入的是较低运算级的运算符,需要将过往的结果先行计算才行。

如果在运算中出现问题,一般将结果直接置为 NaN,如果是编译器可以理解的数量(inf),则将结果置为 inf ,并且抛出异常。

Assignment 类

设计核心

需要明确的是,Assignment 类主要执行两个任务:判断变量名是否合法,判断变量赋的值是否合法。

在构造这个类的实例时,就会实现进行判断变量名,并且抛出相应的异常。而且通过维护一个布尔值 brun 来判断是否需要进行表达式的运行操作。

结构

在这里插入图片描述

解释

构造函数的作用就是根据语法来判断这个变量名是否合法。

run 函数的作用是对这个赋值进行运算,并且完成添加操作。

exps 是赋值表达式,name 是变量名,brun 是规定是否合法的赋值表达式。

变量查询和主控制台命令

Variables 类

设计核心

核心的数据结构是二叉平衡搜索树(红黑树),因为红黑树的查询、修改、删除操作的效率很高,容错性好。具体使用了标准库中较高效的红黑树实现 map 来作为所有变量的容器来对变量进行维护。

另外,命令的解析过程主要是将命令分类并且正确提交到处理命令的方法中(与 Linux 和 Windows 的消息映射仍然有所区别),所以命令的正确运行和语法判断都将放在相应的命令处理方法中。

最后,为了方便其他类能够更好的使用,同时也是为了更加安全,整个类对变量的集合只提供一些简单却有效的方法,以防止数据集合出现恶意修改或者其他问题。

结构
在这里插入图片描述

解释

私有成员中是核心的数据集合 vars,而且为了减少代码量,将用于维护整个集合的迭代器重命名为 itor;

私有函数 testString 的作用是判断传入的命令是否是一个完整命令;

add 方法是用于添加一个变量;

query 方法是用于查询一个变量,并且返回一个迭代器;

returnEndFlag 方法是直接返回整个变量容器的结束标志迭代器;

前置 CMD 的方法均为命令的实现方法,先一一说明:

cls 命令

语法:cls

返回一个布尔值

成功清空返回 TRUE,失败则返回 FALSE

用于清空所有的自定义变量

ls 命令

语法:ls

返回一个布尔值

显示所有的变量(变量数大于 1)

cat 命令

语法:cat

返回一个布尔值

的确存在这个值,就返回 TRUE,否则返回 FALSE

显示所对应的变量的值

cp 命令

语法:cp

返回一个布尔值

成功复制这个值,就返回 TRUE,否则返回 FALSE

将的值复制到中

如果不存在,则新建一个,然后接着赋值

rm 命令

语法:rm

返回一个布尔值

成功删除这个值,就返回 TRUE,否则返回 FALSE

删除所对应的变量

mv 命令

语法:mv

返回一个布尔值

成功重命名这个值,就返回 TRUE,否则返回 FALSE

将的变量的名称修改为

Interface 类

设计核心

主要负责管理两部分:是否终止和输出提示符。通过维护一个布尔值来表示是否终止,并且为这个布尔值提供两个方法。

结构

在这里插入图片描述

解释

构造函数的作用是输出欢迎界面(借鉴 Windows 的命令提示符和 Python 的 Command Line 模式);

WaitForInputCommand的作用就是现行输出4位字符”>>> ”提示用户输入(借鉴Windows的命令提示符和Linux的Terminal);

terminated 就是表示是否终止的布尔值;

TerminateInterface 的作用就是直接修改 terminated 为 false,isTerminated 的作用是直接返回 terminated。

全局函数和全局变量

设计核心

用于全局的模块主要有命令解析器,数据库以及主界面。并没有将命令解释器直接封装,原因是封装后该类的作用仅仅是一个“函数包”,还需要额外新建实例才能实现,所以没有封装整个解析器。

解析器的作用主要是分类,并调用相应的模块进行处理。所以需要对用户的输入进行“整理,修饰,解析”才能正确的提交命令。

主函数的实现异常简单,通过不断地一行一行的读取输入,将输入提交至解析器分析并提交,最后检查是否需要继续执行即可。

结构

全局函数与变量

在这里插入图片描述

主函数

在这里插入图片描述

解释

建立两个全局变量,分别是数据库和主界面;

receiver 就是核心的命令解析器,他的核心就是对用户输入调用 modfyString 进行修饰,然后通过一次检查是否含有关键字(通过调用 checkStringHead 检查输入语句的头是否包含命令的关键字,然后调用 checkStringIsAssignment 检查是否包含赋值运算符,最后是调用 checkStringIsExpression 检查是否为一个运算表达式)来选择具体的提交方法。

异常抛出模块

结构

在这里插入图片描述

解释

前置 Run 的表示是在计算运行中可能出现的错误(依次分别为缺少运算符、缺少运算量和数学错误);

前置 Com 的表示是在编译命令时可能出现的错误(依次分别为不正确的变量名、非法运算符);

前置 Qry 的表示是在变量查询中的可能出现的错误(依次分别为变量集合为空、变量已经存在、没有变量);

前置 Main 的表示是整个程序中都有可能出现的错误(依次分别为错误的命令、语法错误)。

调试与测试分析

版本号:1.0.0.3 更新时间:2014/07/02 12:52

--修复了mv命令和rm命令的一些Bug

该 Bug 通常会导致出现不寻常的错误

--修复了赋值表达式中直接输入:=的错误

该 Bug 会导致程序崩溃

版本号:1.0.0.2 更新时间:2014/07/02 01:08

--修复了自定义变量名称的一些Bug

该 Bug 通常会导致不能正常处理变量名而出错

--修复了判断传入参数的消息映射系统的一些Bug

一个 Bug 发生在赋值语句的判断中,可能会出现不能正常识别赋值语句的变量的问题

另一个 Bug 发生在表达式的判断中,可能会出现将不正确的表达式进行运算的问题

版本号:1.0.0.1 更新时间:2014/07/01 14:51

--修复了cp命令和rm命令的一些Bug

该 Bug 通常会导致复制或重命名到已经存在的变量时出错

--修复了cls命令的Bug

该 Bug 通常会导致把系统自带的默认变量同时清除

版本号:1.0.0.0 更新时间:2014/06/30 23:11

--主程序完成

结果分析及总结

软件中用到的数据结构或算法的时间复杂度、空间复杂度分析

用于维护所有的自定义变量的二叉平衡搜索树(红黑树)

原理

红黑树的原理是,通过额外的给节点一个颜色属性(红色或黑色)来判断相连的节点是否需要通过旋转来进行平衡。

简单的说,通过事先规定:1.红黑树是一棵满二叉树;2.根节点和指向空 NIL 的节点均为黑色;3.整棵树满足一性质,对于每一个树上的节点,从节点到它所有后代叶节点的简单路径上,均包含相同数目的黑色节点。

当然红黑树的主要平衡方法仍然是旋转。通过旋转,可以使整棵树的高度保持在 lgN 以内,即实现平衡。从而保证了每一次操作的时间复杂度都不超过 O(lgN)。

空间复杂度分析

整棵树的额外空间开销较小。节点的结构包含 5 个属性:颜色,数据,地址,左子节点指针,右子节点指针。假设整棵树上总共有 N 个节点,那么总体上空间复杂度是 O (N )。另外如果额外建立一个哨兵来维护所有的空节点的话,同样可以节约很多内存。

时间复杂度分析

  • 对该数据结构而言,主要实现的数据操作有:检索、修改、删除。
  • 检索操作即根据二叉搜索树的性质对元素进行检索,期望时间复杂度为 O(lgN)。
  • 删除操作即先根据二叉搜索树的性质对元素进行检索,如果检索成功则将其从二叉树中删除,期望时间复杂度为 O(lgN)。
  • 修改操作即先根据二叉搜索树的性质对元素进行检索,如果检索成功则将其先建立副本然后再从二叉树中删除,之后再将事先建立的副本修改并放回,期望时间复杂度为 O(lgN)。

用于计算表达式的堆栈结构的算法

算法再次说明

建立两个堆栈:运算符栈 sOpr 和数值栈 sNum。针对所进行一次运算,具体操作是 sOpr 弹出一个运算符,sNum 弹出两个数据,将其进行计算,并将结果压入 sNum 中。

建立一个字符串指示指针 it,它的作用是根据当前所指示的字符来进行智能判断,并选择相应的方法进行处理。简而言之,就是能够正确的读取变量、数值以及运算符,并且进行相应的操作。

程序框图
在这里插入图片描述

时间复杂度分析

如果有 Q 个运算符,用到了 m 个变量(总共有 M 个变量),N 个数值,进行一次运算的时间复杂度是 O(Q + m lgM + N )。

软件运行截图

普通的表达式计算

在这里插入图片描述

添加自定义变量与自定义变量的运算

在这里插入图片描述

ls 命令、cat 命令

在这里插入图片描述

cp 命令、mv 命令、rm 命令

在这里插入图片描述

cls 命令

在这里插入图片描述

程序退出 exit 命令

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shejizuopin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值