BUAA-OO-Unit 1: 表达式展开
第一单元的主题是对表达式进行括号展开,主要训练目标是体会层次化设计的思想的应用和工程实现。本单元一共有三次作业,难度递增,从包含含加、减、乘、乘方以及括号的单变量多项式展开,依次加入指数函数、自定义函数和求导运算。以下将对这三次作业逐个进行分析并总结。
第一次作业
本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的
单变量表达式,输出恒等变形展开所有括号后的表达式。
UML类图
本次作业代码的UML类图如下:
从图中可以看出,所建的类大致可以分为解析类、储存类和运算类,具体每个类的设计考虑见下。
架构设计
在分析题目和参考训练题后,我将本次作业分为预处理、解析、储存、展开与优化等五个任务。
字符串预处理
由于输入的字符串中会含有空格、连续的加减号等,需要进行预处理以方便之后的操作。在Preprocessor
类中:
-
删除所有空格和制表符
-
通过遍历合并连续的加减号
-
将连续的加号替换为单个加号,并移除表达式开头的加号
最后输出一个字符串交给负责解析的类。
表达式解析:递归下降
根据递归下降的思想,我们要自上而下建立语法树,进行词法和语法的分析。
Lexer
这一部分是词法分析器,主要负责将经过预处理后的字符串分解成一系列有意义的单元,称为“token”。这个过程是解析器的第一步。在第一次作业中token 包含+
, -
, *
, (
, )
, ^
, x
以及数字。
Lexer
中的核心公共方法是next()
,通过if-else
语句识别数字和特定的单字符token。通过peek()
方法可以访问当前的token。该方法使得外部代码能够在不改变Lexer
状态的情况下检查当前token。
Parser
这一部分是语法分析器,使用Lexer
实例来逐个读取输入字符串中的token,然后根据这些token构建表达式(Expr
)、项(Term
)和因子(Factor
)对象。仿照训练题的代码,Parser
类由以下三个主要方法组成:
parseExpr()
:解析整个表达式,包含一个或多个项。这个方法循环调用parseTerm()
,直到遍历完所有的token或遇到右括号)
为止,表示表达式的结束。加减号并没有在此处处理,而是在parseTerm
中进行处理,作为项的正负号。parseTerm()
:解析单个项,由一个或多个因子组成(通过乘号连接)。这个方法会处理加减号、识别乘号,并根据这些信息构建Term
对象。通过循环调用parseFactor()
方法,同时进行系数的相乘和指数的相加,最终在Expr
的list
中增加一个Term
。parseFactor()
:解析单个因子,因子可能是数字、变量、带括号的表达式或变量的指数形式。这个方法根据当前token的类型决定如何构建因子对象,并处理指数。遇到带括号的表达式则调用parseExpr
递归地解析。
表达式展开
经过以上处理后,得到的Expr
中含有一个装有Term
的List
, 而Term
由两个List
组成,分别装表达式因子和其他因子。其他因子在parseTerm
部分以及进行计算,得到两个数字分别代表系数和x
的指数,即:
F ( x ) = ∑ i a i x b i F(x) = \sum_{i} a_i x^{b_i} F(x)=i∑aixbi
基于此,我新建了Operator
类和MulExpr
类进行处理。在MulExpr
中我构建了两个公开方法mulExpr
和mulTerm
,可以读入两个表达式/项,返回其相乘的结果。在Operator
类遍历Expr
的Term
列表,取出exprFactor
列表不为空的项,对其调用表达式乘法和项乘法将括号展开,最终返回一个不带括号的表达式。
其中需要注意的是,为了提高程序的性能,处理更复杂的表达式展开,需要在mulExpr
之后调用合并同类项的方法,即下面的优化。
结果优化与输出
为了缩短最终输出的字符串的长度,需要对展开后的表达式进行合并同类项,即将x
的指数相同的项的系数相加。这里我用了HashMap
来储存指数作为key
,系数作为value
,具体实现的代码如下:
HashMap<Integer, BigInteger> sites = new HashMap<Integer, BigInteger>();
for (int i = 0; i < numTerm; i++) {
Term term = input.getTerm(i);
BigInteger coe = term.getCoe().getValue();
int exp = term.getExp().getExp();
if (sites.containsKey(exp)) {
BigInteger newValue = sites.get(exp).add(coe);
BigInteger value = sites.replace(exp, newValue);
} else {
sites.put(exp,coe);
; }
}
之后输出时需要依次重写 Term
和Expr
的toString
方法,同时在其中需要进行一些特判以进一步缩短输出长度,包括:
- 系数为0的项不输出,如果最后字符串为空则输出0;