文章目录
程序结构
分析
Complexity Metrics(复杂度分析)


Dependency Metrics(依赖度分析)

类图

Pre:将输入的已去掉空白字符的表达式中的所有自定义函数替换Lexer:词法分析,并将+/-(符号)转化成1 * / (-1) *,避免符号和运算符混乱Parser:语法分析,构建表达式树Expr,Term,Ex,Factor(interface):递归下降层次,均含toString()方法Expr:任何符合形式化表述的结构Term:外层不含+/-(运算符)的ExprEx:外层不含*的TermFactor:不含^/exp()的ExUnknown:变量Number:常数Derive:求导因子
Cal:单项式类Polya:多项式类,含所有计算方法以及toString()方法
优点: 采用递归下降,可扩展性强,实现Factor接口,统一管理,将预处理、解析与计算完全分离,实现高内聚低耦合,遵循单一职责原则。
缺点: 单项式的设计上仍然没有实现抽象化层次,违背开闭原则,若要实现sin(),lg()则改动较大。
架构设计
hw_1

- 递归下降层次:
Expr -> Term -> Power -> Factor(interface)Power处理以^为运算的结构,为二元运算,故多项式类要存在对应“乘方”运算public Poly pow(Poly other) {}Factor接口只有Unknown(变量),Number(常数)- 每个类中均有
toString()方法,返回后缀表达式
Poly类中有一个HashMap,key为变量的指数,value为系数,即结果均可写成{value*x^key}的形式。定义加、减、乘、乘方方法,实现计算功能-
根据后缀表达式,将基项
(Unknown,Number)实例化Poly类,再一一计算public Poly(String base) { this.polys = new HashMap<>(); if (base.equals("x")) { this.polys.put(BigInteger.ONE, BigInteger.ONE); this.maxIndex = BigInteger.ONE; } else if (base.equals("")) { this.maxIndex = new BigInteger("-1"); } else if (new BigInteger(base).toString().equals("0")) { this.maxIndex = new BigInteger("-1"); } else { this.polys.put(BigInteger.ZERO, new BigInteger(base)); this.maxIndex = BigInteger.ZERO; } } -
有
toString()方法,用于将最终计算结果转化为字符串输出
-
hw_2

- 首先引入了自定义函数,我采用了单独建一个预处理
Pre类- 先用
=分隔自定义函数表达式,再用正则表达式获取形参,并记录形参的顺序String[] postF = fx.split("="); if (postF.length == 2) { this.fx = postF[1]; } else { this.fx = ""; } String patternTerm = "f\\((.+)\\)"; Pattern re = Pattern.compile(patternTerm); Matcher matcher = re.matcher(postF[0]); while (matcher.find()) { fp = matcher.group(1).split(","); } this.numF = fp.length; for (int i = 0; i <= this.numF - 1; i++) { mapF.put(fp[i], i); } major()主执行函数遍历表达式,依次拓印字符,若检测到f g h,则截取配对的f/g/h(*)字符串作为参数调用f/g/h()函数,该函数保证传入的f/g/h(*)调用自定义函数形式的字符串会返回完全不含自定义函数的替换字符串public String major(String input) { StringBuilder str = new StringBuilder(); int pos = 0; while (pos < input.length()) { if (input.charAt(pos) == 'f' || input.charAt(pos) == 'g' || input.charAt(pos) == 'h') { pos = pos + 2; int tmp = pos; int left = 0; while (left != -1) { //配对括号 if (input.charAt(pos) == '(') { left++; } else if (input.charAt(pos) == ')') { left--; } pos++; } int end = pos; pos = tmp; str.append("("); if (input.charAt(pos - 2) == 'f') { str.append(f(input.substring(pos - 2, end))); } else if (input.charAt(pos - 2) == 'g') { str.append(g(input.substring(pos - 2, end))); } else { str.append(h(input.substring(pos - 2, end))); } str.append(")"); pos = end - 1; } else { str.append(input.charAt(pos)); } pos++; } return str.toString(); }- 接下来就是
f/g/h()函数:为了保证加传入的字符串完全去除自定义函数,故采用递归调用major()函数的方法,替换时,将实参传入major()的结果去替换,直至实参不再含有自定义函数if ((c.equals("x") && (at == 0 || at == fx.length() - 1 || fx.charAt(at - 1) != 'e')) || (c.equals("y")) || (c.equals("z"))) { str.append("("); str.append(major(paras.get(mapF.get(c)))); str.append(")"); }
x y z替换成特殊字符,避免一些bug - 先用
- 将
Power类替换为Ex类,同时处理^,exp(),因为它们都属于指数范畴,运算的优先级相同,故集中处理- 存放两个容器,分别存
Ex和Factor,属性mode标记三种情况,用于确定后缀表达式的格式:
以及一些形式分类:0 : Unknown / Number / (Expr) —— 本身 1 : a ^ b —— a b ^ 2 : exp( c ) —— c e1. exp(a)^b 2. exp(c) 3. x^d 4. ()^e 5. Unknown / Number / (Expr) Parser解析Ex时,通过括号配对以及正则表达式确定是哪个分类,并对mode赋值,只要是形式1 2,就反复调用parserEx(),否则调用parserFactor()退出
注意若是形式1,则注意做标记,下次不再判定为1,避免死循环
- 存放两个容器,分别存
- 修改多项式类,增加单项式类。由于第一次作业较为简单,我没有设置单项式类,本次增设。多项式类
Polya、单项式类Cal。注意到结果均可表示成{a * x^b * exp(c)}- 先定义单项式(
Cal):x^b * exp(c)private Polya ex; //c private BigInteger index; //b - 再定义多项式(
Polya):<Cal,a>private final TreeMap<Cal, BigInteger> polys; - 实现各种运算(在计算过程中逢0即删是个好习惯),新增
exp()public Polya exp() { if (this.polys.isEmpty()) { Cal cal = new Cal("3"); Polya polya = new Polya(); polya.addCal(cal, new BigInteger("1")); return polya; } Polya polya = new Polya(); Cal cal = new Cal("3"); cal.setEx(this); cal.getEx().string = cal.getEx().toString(); polya.polys.put(cal, new BigInteger("1")); return polya; } - 为实现
Cal作为键值的比较,还要重写compareTo()方法
这里的public class Cal implements Comparable<Cal> { //省略 @Override public int compareTo(Cal o) { if (this == null && o == null) { return 0; } else if (this == null) { return -1; } else if (o == null) { return 1; } else { if (this.index.compareTo(o.index) != 0) { return this.index.compareTo(o.index); } else { return this.ex.getString().compareTo(o.ex.getString()); } } } }getString()原本用的toString(),但由于每次都要调用toString(),时间成本很高,故在Polya中添加字符串属性,记录其上一次toString结果,并在任何发生修改的地方更新该属性,用getString()方法获取其值
而使用TreeMap可以自动排序,保证返回字符串的唯一性private String string; - 深克隆:
public Polya clone() { Polya polya = new Polya(); for (Cal cal : this.polys.keySet()) { polya.polys.put(cal, this.polys.get(cal)); } return polya; }
- 先定义单项式(
hw_3
(类图同最终类图)
- 自定义函数定义的问题,我只需要先对自定义函数定义表达式按输入顺序依次进行替换处理即可
ArrayList<String> queue //记录顺序
由于有序,后面自定义函数调用if (!queue.isEmpty()) { if (queue.get(0).equals("f")) { fPre(fx); } else if (queue.get(0).equals("g")) { //省略 } } if (queue.size() >= 2) { if (queue.get(1).equals("f")) { fPre(fx); this.fx = major(this.fx); } //省略 } if (queue.size() >= 3) { if (queue.get(2).equals("f")) { fPre(fx); this.fx = major(this.fx); } //省略 }major()时,前面的自定义函数已经填置完毕
fPre(),gPre(),hPre()均为hw_2中的初始化内容拆分,没有变化 - 新增求导因子:
- 我将其继承到
Factor接口,词法解析curToken为"d":if (lexer.pop().equals("d")) { lexer.next(); Factor derive = new Derive(parseExpr()); lexer.next(); return derive; } Polya新增求导算法:public Polya derive() { Polya polya = new Polya(); if (this.polys.isEmpty()) { return new Polya(); } else if (this.polys.size() == 1) { for (Cal cal0 : this.polys.keySet()) { Polya polya0 = new Polya();//系数 //省略 Polya polya1 = new Polya();//对x^n求导项 if (cal0.getIndex().compareTo(BigInteger.ONE) >= 0) { //省略 } Polya polya2 = new Polya();//对exp(a)求导项 if (!cal0.getEx().polys.isEmpty()) { //省略 } return polya1.add(polya2).mul(polya0); } } else { for (Cal cal : polys.keySet()) { Polya polya0 = new Polya(); polya0.polys.put(cal, this.polys.get(cal)); polya0.string = polya0.toString(); polya = polya.add(polya0.derive()); } return polya; } return new Polya(); }
- 我将其继承到
新迭代
变量不再只有x,增加y z
Pre完全不需改动,仍可完成自定义函数替换Lexer对解析变量部分稍加改动if (c == 'x' || c == 'y' || c == 'z') { pos++; curToken = String.valueOf(c); ifOp = true; }Unknown原本就有name属性以及构造方法,不用改变Parser所有原来分析x的地方,都或上y z即可,如:if (lexer.pop().equals("x")||lexer.pop().equals("y")||lexer.pop().equals("z")) { lexer.next(); return new Unknown(lexer.pop()); } //ParseFactor- 计算类
Polya,Cal:Cal新增属性,指数分为x y z的指数:private Polya ex; private BigInteger xIndex; private BigInteger yIndex; private BigInteger zIndex;compareTo随之做适当修改Polya对运算方法进行微调:- 加、减、乘、乘方、e指较为简单
- 求导运算则再多出两个求导项
(y z)相加即可
bug分析
hw_2强测&互测
仅有一处bug:对自定义函数表达式未做去空白符处理,公测时是做了处理的,但在改bug时误删了没有发现,样例中也没有这种情况,导致强测才发现。还是测试时捏测试用例不够耐心,没有覆盖很多情况。
hw_3互测
仅有一处bug:词法解析时没有处理前导零
return new BigInteger(str.toString()).toString();
还是跟上面一样,测试不够严密。
hack策略
- 阅读代码分析时间性能,针对性构造容易导致对方运行时间过长的样例
- 例如自定义函数替换方法,计算时删零,多层
exp()嵌套,过于频繁调用某种方法
- 例如自定义函数替换方法,计算时删零,多层
- 分析对方优化策略,观察是否会引发bug(过度化简)
- 最典型的
exp(-1)和exp((-x))的区别
- 最典型的
- 看对方代码哪里写的很混乱就着重hack这里
- 搭建评测机,找到bug后逐渐缩短表达式,找到“病根”
结果优化
- 最基本的优化:系数为±1且不为常数的项,将
1省略;指数为1,将^1省略;正系数项提前;第一项若为正不加+ - 此外,我只做了
exp()里括号的减省:- 先取出
exp()内部的多项式转化成字符串String s = cal.getEx().getString(); - 正则表达式
String patternTerm1 = "^(-?)(\\d*)\\*x(\\^\\d+)?$"; //matcher1 String patternTerm2 = "^(-?)(\\d*)\\*(exp\\(.+\\))(\\^\\d+)?$"; //matcher2 - 严判标准
判定顺序:String patternTerm = "^-\\d+$";//负常数,不用加括号 通过括号匹配找外层运算符: "^-\\d+$" -> return 0; +/- -> return 1; * -> keep 2; //上面两个到最后也没有再返回2 default -> return 0;
1、严判标准若为1,则保留括号
2、matcher1.find()或matcher2.find():系数绝对值取出拿到exp()外面做指数,系数若正,就省略括号,否则保留括号
3、严判标准若为2,则保留括号
4、其余情况均省略括号
- 先取出
心得体会
这几次作业我体会到了良好架构的重要性,可扩展性。通过作业以及研讨课对SOLID原则有了更深刻的认知。掌握了递归下降法。掌握了当Map容器键值是类对象时,如何重写compareTo()方法以实现键值比较。
此外,当面临需要重构或没有思路时,一定要敢于发散地去想,敢于试错,不存在完美的架构,只存在适合自己的架构,自己摸索出来的才是自己的,不要因为别人的架构好就盲目修改,更不要还没自己构建无脑效仿别人,这就失去了这门课的意义。
304






