301
社区成员
发帖
与我相关
我的任务
分享本次作业的主要任务是表达式化简,通过本次作业,我学习了使用类来管理对象,并且对面向对象的基本思想有了一定的了解。
本次作业的目标是尽可能达成模块化设计,尽可能使用面向对象的方法,比如更多的使用接口,抽象类等。

我的架构除了MainClass外,主要可以分为:
表达式解析单元(紫色部分):通过递归下降的方法解析表达式,并使用存储单元建立一颗层次化存储表达式的“树”
表达式存储单元 (蓝色和绿色部分) : 递归下降仅仅是帮助我们解析嵌套的语法层次关系,而语法解析后,我们需要将该表达式以一颗自顶向下的树的形式存储起来,并且为了方便处理,我们没有直接存储字符串,而是该单元每一个类中都存储了一个ItemListManager管理的多项式。
多项式存储与处理单元 (棕黄色部分): 该部分由一个储存多项式的ItemList和一个管理多项式的ItemListManager组成,其中ItemList的每一个成员由系数,xyz的指数共四个参数组成,同时我在ItemListManager定义了常用的管理方法。这些方法可以大致分为三类:1.增删改查的管理方法;2.加减乘乘方的计算方法; 3.整理表达式以降低复杂度和删除冗余项的方法。 通过完善该部分的方法,我们可以极大的减少上面两部分的复杂程度。
作业的文档中主要提供了递归下降和正则两种思路,但是正则表达式不利于后续扩展。于是就选择了递归下降的方法。
2.1递归下降、
本次作业的表达式,是由一系列 EBNF 描述的形式化表述来定义的语法规则推导而成的语句。语法规则的每一条称为一个产生式,位于产生式左侧的符号称为非终结符(例如 表达式,项 等),与之相对的,语法规则中没有出现在产生式左侧的符号称为终结符(例如 x, +, 1 等)。
而递归下降则是一种自顶向下的法分析方法,其做法是为每一个非终结符(即语法规则中每个位于产生式左侧的符号)编写一个可递归调用的分析子程序(方法),通过方法之间的相互调用来处理嵌套的语法层次关系。
有了形式化表述的语法规则,我们便可以利用递归下降法来编写解析程序,并且解析程序的结构与形式化表述中定义的语法规则是基本一致的,这使得表达式解析部分的代码架构很清晰,并且十分易于扩展。
2.1.1怎么递归下降
明确语法
分析表达式的各种语法组成及他们之间的关系
根据语法规则,每一个非终结符都建立一个相对应的解析函数
每个函数负责自己的非终结符
解析中遇到下一级的子产生式的时候,就再调用下一级,从而完成递归调用
明确函数的返回值
如果遇到语法错误,就不要头铁的继续解析了,赶紧抛一个错误
3.1 存储与结构化 : 用类管理表达式
如上图,本题我们建立了三大类,即表达式,项,因子三大类,而我们又不难发现因子可以分为两种:常量因子和幂因子,其中常量因子可以看成由一个带符号常数组成,而幂因子可以看成一个幂底数因子加上一个幂指数因子组成。
分析清楚这一结构后,我们需要明确另外一点,也就是我们这些类存储的究竟是什么,是一个字符串,还是他的子类,还是别的什么?
有一种比较常见的方式是,我们在表达式的因子类里存储多项式因子,而在其上的类中仅存储更底层的对象,最后我们在输出时才进行计算。
但是我实际上采用的方式却是直接每一个类中都存储一个多项式,然后每个类添加更底层类时,直接在原有的多项式基础上通过运算添加新的项中的多项式即可。之所以采取这种方法,主要是考虑到当我们为每个类添加更底层的类后,底层类不会再发生变化。
根据我的方案,我们每一个类都需要有一个ItemListManager,管理自己的多项式;并且需要有一个getItemListManager方法获取该类管理的多项式。
下面重点介绍每个类如何新增多项式:
表达式类是我们整个表达式大树的最顶层 , 他需要一个addTerm的方法,用于添加项,调用该方法时我们需要传入一个项和一个符号,用于确定加减关系。然后我们只需要调用ItemListManager为我们提供的加减法方法即可完成多项式管理。
项类时这颗大树的第二层,我们的项在初始化时就已经默认在ItemListManager中添加了系数为1,指数均为0的一项。然后当我们需要addFactor时,我们只需要传入一个因子,然后就会通过调用ItemListManager中的乘法,将新加的项与原来的项乘在一起,获得一个新的项。
因子类重点介绍幂因子,该类在创建时,我们需要添加Factor型的幂底数,以及int型的幂指数,然后我们就可以通过getItemListManager方法和ItemListManager提供的pow方法初始化该因子的ItemList。
3.2 词法解析器 : Lexer
词法解析器作用是为我们不断地提供解析后的下一个“词汇单元”。
我们应该如何完成该Lexer呢?
初始化
next方法
我们可以将我们的表达式分为几个最基本的单词符号:
带正负和前导零的数字,变量,左括号,右括号,加减乘和乘方符号
我们next的功能便是取出下一个单词符号,因而我们重点就是要做好对这些符号的判断,然后将其取出。
peek
获取当前取出的单词
getNum
功能是取出下个带正负和前导零的数字,用于辅助next方法的实现。
getSign
该方法返回我们目前strOut中存储的符号类型,使用该方法获得类型,主要是简化Parser中需要经常判断符号类型的负担,然后可以通过类型判断是否出错。
3.3 语法解析器 : Parser
语法解析器就是利用Lexer,将我们的表达式解析为可以理解的层次化结构。
因此该部分主要实现以下方法:
public class Parser {
private final Lexer lexer;
//1.初始化方法
public Parser(Lexer lexer) {
this.lexer = lexer;
}
//2.解析表达式
public Expr parseExpr() {
//1.创建对象
Expr expr = new Expr();
//2.判断第一个字符是不是正负号
if lexer.peek() is '+' or '-' then
flag = '+' or '-';
lexer.next
else
flag = '+'
//3.添加第一个Term
expr.addTerm(parseTerm(), flag);
//4.循环判断,如果下一个token是+/-,就继续添加项
while lexer.peek() is '+' or '-' then
expr.add...
//5.返回expr
return expr
}
//3.解析项
public Term parseTerm() {
//1.创建对象
Term term = new Term();
//2.判断第一个字符是不是正负
if lexer.peek() is '+' or '-' then
//添加一个 1 或 -1 的项
...
//3.添加第一个项
term.addFactor(parseFactor());
//4.循环添加项
while lexer.peek() is '*' then
term.add...
//5.返回
return term;
}
//4.解析因子
public Factor parseFactor() {
//判断因子类型,然后添加
//先省略,再添加
}
//5.解析幂因子
...//后续添加
}
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
| Lexer.getChar(int) | 1 | 2 | 2 | 2 |
| Lexer.getNum() | 3 | 1 | 4 | 4 |
| Lexer.getSign() | 0 | 1 | 1 | 1 |
| Lexer.next() | 13 | 3 | 9 | 12 |
| Lexer.peek() | 0 | 1 | 1 | 1 |
| MainClass.main(String[]) | 0 | 1 | 1 | 1 |
| Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
| Parser.leftBracket() | 6 | 2 | 5 | 5 |
| Parser.parseExpr() | 7 | 1 | 6 | 6 |
| Parser.parseFactor() | 15 | 6 | 9 | 10 |
| Parser.parseTerm() | 3 | 1 | 4 | 4 |
| compare(ItemList, ItemList) | 6 | n/a | n/a | n/a |
| expr.Expr.Expr() | 0 | 1 | 1 | 1 |
| expr.Expr.addTerm(Term, int) | 2 | 1 | 2 | 2 |
| expr.Expr.arrange() | 1 | 1 | 2 | 2 |
| expr.Expr.getItemListManager() | 0 | 1 | 1 | 1 |
| expr.Expr.setFlag(int) | 0 | 1 | 1 | 1 |
| expr.Expr.toString() | 0 | 1 | 1 | 1 |
| expr.Number.Number(BigInteger) | 0 | 1 | 1 | 1 |
| expr.Number.getItemListManager() | 0 | 1 | 1 | 1 |
| expr.Power.Power(Factor, int) | 0 | 1 | 1 | 1 |
| expr.Power.getItemListManager() | 0 | 1 | 1 | 1 |
| expr.Term.Term() | 0 | 1 | 1 | 1 |
| expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
| expr.Term.arrange() | 1 | 1 | 2 | 2 |
| expr.Term.getItemListManager() | 0 | 1 | 1 | 1 |
| expr.Term.setFlag(int) | 0 | 1 | 1 | 1 |
| expr.VarNum.VarNum(String) | 1 | 1 | 1 | 4 |
| expr.VarNum.getItemListManager() | 0 | 1 | 1 | 1 |
| itemlist.ItemList.ItemList(BigInteger, BigInteger, BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
| itemlist.ItemList.getCoefficient() | 0 | 1 | 1 | 1 |
| itemlist.ItemList.getxExp() | 0 | 1 | 1 | 1 |
| itemlist.ItemList.getyExp() | 0 | 1 | 1 | 1 |
| itemlist.ItemList.getzExp() | 0 | 1 | 1 | 1 |
| itemlist.ItemList.setCoefficient(BigInteger) | 0 | 1 | 1 | 1 |
| itemlist.ItemList.toString() | 25 | 2 | 14 | 14 |
| itemlist.ItemListManager.ItemListManager() | 0 | 1 | 1 | 1 |
| itemlist.ItemListManager.add(ItemListManager) | 2 | 1 | 3 | 3 |
| itemlist.ItemListManager.addItem(BigInteger, BigInteger, BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
| itemlist.ItemListManager.addItem(ItemList) | 0 | 1 | 1 | 1 |
| itemlist.ItemListManager.iterator() | 0 | 1 | 1 | 1 |
| itemlist.ItemListManager.mergeItemLists() | 8 | 1 | 6 | 6 |
| itemlist.ItemListManager.multiply(ItemListManager) | 3 | 1 | 3 | 3 |
| itemlist.ItemListManager.multiplyCoefficient(BigInteger) | 1 | 1 | 2 | 2 |
| itemlist.ItemListManager.pow(int) | 3 | 2 | 3 | 3 |
| itemlist.ItemListManager.removeItem(ItemList) | 0 | 1 | 1 | 1 |
| itemlist.ItemListManager.removeZeroItem() | 4 | 1 | 4 | 4 |
| itemlist.ItemListManager.sortItemLists() | 9 | 4 | 4 | 4 |
| itemlist.ItemListManager.sub(ItemListManager) | 2 | 1 | 3 | 3 |
| itemlist.ItemListManager.toString() | 8 | 1 | 4 | 4 |
| itemlist.ItemListManager.traverseItemLists() | 8 | 1 | 4 | 4 |
| Class | OCavg | OCmax | WMC | |
| Lexer | 3.17 | 11 | 19 | |
| MainClass | 1 | 1 | 1 | |
| Parser | 5 | 10 | 25 | |
| expr.Expr | 1.33 | 2 | 8 | |
| expr.Number | 1 | 1 | 2 | |
| expr.Power | 1 | 1 | 2 | |
| expr.Term | 1.2 | 2 | 6 | |
| expr.VarNum | 3 | 5 | 6 | |
| itemlist.ItemList | 2.86 | 14 | 20 | |
| itemlist.ItemListManager | 2.5 | 4 | 40 | |
| Package | v(G)avg | v(G)tot | ||
| 4 | 48 | |||
| expr | 1.35 | 23 | ||
| itemlist | 2.77 | 61 | ||
| Module | v(G)avg | v(G)tot | ||
| P0 | 2.59 | 132 | ||
| Project | v(G)avg | v(G)tot | ||
| project | 2.59 | 132 |
总结
本次作业主要考察了对于递归下降的理解,以及对于存储表达式的数据结构的考察,整体难度不算很大,但是因为我没有相关经验,反而花费了大量时间。
本次作业我使用Python写的评测机自动测试后没有遇到新的bug。
本次作业相对于上一次作业,增加了指数函数和自定义函数。
我的处理流程大概是先读取自定义函数,然后每读入一个自定义函数,都解析为形参表达式。
然后读入表达式,读取过程中自定义函数作为因子处理,解析为实参表达式因子即可。
要想实现自定义函数,首先关注自定义函数解析部分,主要有两种方法,第一种是字符串替换,第二种是基于语义的解析。本来我打算使用字符串替换的,但是研讨课上助教老师指导我们想要考察我们的是第二种方法,于是我就改作了解析。
具体来说,我先在主类中循环读入函数表达式,然后使用递归下降解析函数,最后将解析后的函数存储在一个Map中。然后将这个Map存储在MyFuncList的静态变量中。之后定义了MyFuncListReplace类,用于形参的替换。最后简单更改语义解析部分即可。
a. 主类新增代码见下:
int n = scan.nextInt();//读入函数个数
scan.nextLine();
Map<String, String> funcMap = new HashMap<>();//存储函数名和函数体
for (int i = 0; i < n; i++) {
String strIn = scan.nextLine();
FuncParser parser = new FuncParser(new Lexer(strIn));
funcMap.put(parser.getFuncName(), parser.getFuncBody());
}
//将funcMap中的函数添加到itemList中
MyfuncList.addItem(funcMap);
b. MyfuncList部分主要功能是实现了自定义函数的存储以及增删查改方法**和replace方法。
c. MyFuncListReplace部分主要实现了自定义函数的替换,MyfuncList的replace方法即调用了该类的方法。分为两个类主要是因为如果使用一个类完成该部分的话代码太长了。
d. 语法解析部分新增了自定义函数类,继承因子类,然后Parser中新增解析自定义函数方法即可。
因为我上次函数实现上是解析函数式而非字符串替换,所以迭代的话就不需要考虑自定义函数内嵌套函数的问题了,因此我本次迭代内容就只有求导因子了,任务量相对少很多。
1.1 求导因子
求导因子的加入不需要添加新的存储相关的类,只需要修改语法解析部分即可,具体而言,除却Parser的修改外,只新加了一个继承了因子类的Differ类。该类的基本代码直接见下:
public class Differ implements Factor {
//变量
private ItemListManager itemListManager; //itemListManager对象
private String derStr;
private int derNum = 0; //x:0, y:1, z:2//求哪个变量的导数
//方法
public Differ(ItemListManager itemListManager, String derStr) //初始化
public ItemListManager diff(ItemListManager itemListManager, int derNum) //求导
private ItemListManager diffItemNomal(ItemList item, int derNum) //常规部分求导
//继承部分
public ItemListManager getItemListManager() { return itemListManager; }
}
| Package | v(G)avg | v(G)tot | |
|---|---|---|---|
| 2.8 | 14 | ||
| analyse | 4.72 | 85 | |
| expr | 1.83 | 55 | |
| itemlist | 2.85 | 274 | |
| Module | v(G)avg | v(G)tot | |
| hk3 | 2.87 | 428 | |
| Project | v(G)avg | v(G)tot | |
| project | 2.87 | 428 |
因为化简过程为递归调用,化简代价函数过高的表达式时可能会TLE,如果需要继续迭代的话我可能会考虑使用新的化简方法或者设置一个时间戳记录程序运行时间,如果时间过长则直接输出化简到某一步的表达式。
经过本阶段的学习,我对于java语言本身有了一定的认识,对他的基本特性有所了解,最让我印象深刻的就是浅克隆问题,让我Debug了非常之久。
我还对面向对象本身有了更深的了解,之前一直只知道有面向对象和面向过程的区别,却不知道二者区别,经过三周的切身实践,我对这两种方法的不同之处有了更深的认识,对于什么是继承和多态,及二者重要之处有了更深的认识,对于模块化和工厂化的开发模式带来的易于维护的好处也有了切身感受。
此外我还对包括IDEA在内的工具有了更多的使用经验,对于如何通过搜索引擎帮助学习也有了更多的技巧收获。
最后的收获来自于Debug,经过艰苦卓绝的Debug,我发现我找到并修复bug的能力也变得更强了。
希望下阶段的学习也能收获满满,不过也希望不要太难。