301
社区成员
发帖
与我相关
我的任务
分享
Parser分析语法、Lexer分析词法,剩下的Expr、Term、Number、Variable、Power等都是递归下降过程中需要处理的成分,其中比较特殊的有:Function表示自定义函数、ExpFunc表示指数函数、Differential表示求导因子;右侧的三个类负责将后缀表达式生成基本项并进行运算:Polynomial、Monomial分别是表达式和基本项,PolyCal负责计算表达式;下方的两个类都是静态的类,无需实例化便可以调用其中的方法:FunctionMode用于存储自定义函数的变量以及定义式,Printer 用于输出最终的结果。

PolyCal、Parser和Printer三个类的代码冗长。其中PolyCal和Printer两部分主要是因为偏向面向过程,而Parser中则是因为有三个方法中都有很相似的代码,应该专门写一个满足该功能的方法进行复用。
#这三个方法分别用来处理自定义函数、求导因子和指数函数,有很高的相似度
private String getFunction() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(expression.charAt(pos++));//读入fhg
stringBuilder.append(expression.charAt(pos++));//读入左括号
int flag = 1;
while (pos < expression.length() && flag != 0) {
if (expression.charAt(pos) == '(') {
flag++;
} else if (expression.charAt(pos) == ')') {
flag--;
}
stringBuilder.append(expression.charAt(pos));
++pos;
}
return stringBuilder.toString();
}
private String getDiff() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(expression, pos, pos + 3);
pos = pos + 3;
int flag = 1;
while (pos < expression.length() && flag != 0) {
if (expression.charAt(pos) == '(') {
flag++;
} else if (expression.charAt(pos) == ')') {
flag--;
}
stringBuilder.append(expression.charAt(pos));
++pos;
}
return stringBuilder.toString();
}
private String getExpFunc() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(expression, pos, pos + 4);
pos = pos + 4;
int flag = 1;
while (pos < expression.length() && flag != 0) {
if (expression.charAt(pos) == '(') {
flag++;
} else if (expression.charAt(pos) == ')') {
flag--;
}
stringBuilder.append(expression.charAt(pos));
++pos;
}
return stringBuilder.toString();
}




刚读完指导书时感到十分茫然,不知道这么庞大的工程要从哪里入手。不过好在课程组在课程平台以及公众号提供了示例代码。在看完training以及公众号中的示例代码后,又考虑到后期需要扩展多层括号嵌套,于是我采用了递归下降解析表达式的方法。由于training代码中要求我们实现后缀表达式的生成,于是我选择引入单项式(大家所说的基本项)利用栈计算后缀表达式的方法来将表达式展开。因此,从hw1开始我的架构就可以明确的分为两部分:一部分参照training部分提供的递归下降方法解析,另一部分专门设计cal类来处理“+-*/^”的计算并打印输出结果。

Parser、Lexer解析器外,Monomial是基本项类、Polynomial是表达式类、PolyCal是表达式计算类。
hw2大概是第一单元最困难的一次作业。自定义函数利用实参替换形参就可以解决,重要的是如何把指数函数融入后缀表达式计算的过程。老师在鼓励大家去重构优化自己的架构,身边的同学也在尝试去重构。在这样的氛围中我也开始思考如何去优化自己的架构。起初我想要去优化掉后缀表达式计算的过程,因为可以递归下降分析表达式的过程直接实现计算。在思考了一段时间后我决定延续我hw1中的 架构,不做重大改动。首先,我认为优化掉后缀表达式后会导致分析表达式成分与计算表达式的过程发生一定程度上的耦合,有可能出现意想不到的问题。其次,我发现了“+-*/^”是二元运算符,而指数函数的本质是一个一元运算符,因此指数函数也可以用后缀表达式来表示,例如exp(x)的后缀表达式就是“x e”(用e来表示指数函数),这样指数函数便也可以完美融入后缀表达式计算的过程了。

ExpFunc和Function类来解析指数函数和自定义函数。除此之外还可以发现右侧多了两个静态类。其中FunctionMode中用HashMap来记录自定义函数的种类、变量类型以及定义式;Printer专门用来输出结果,实际上就是吧hw1中PolyCal中输出结果部分的代码转移到了Printer类中来使PolyCal类的功能更加专一且没那么臃肿。
hw3添加了求导的过程并允许了自定义函数的定义过程中可以包含自定义函数。经历了hw2的思考,hw3便比较轻松了。由于在hw2中我选择在递归下降的过程中处理自定义函数并返回一个factor,于是它本身就能够实现处理用自定义函数定义的自定义函数。求导因子的处理方法和hw2中处理指数函数的方法一样:将其视为一元运算符加入后缀表达式计算。(dx(x)表示为“x d”)

Differential求导因子;计算层面在PolyCal中加入了用于处理求导运算的方法Dervation();输出方面在Printer中加入了提取公因数的优化方法Extraction()。
假设要在新的迭代中加入三角函数。首先,解析含有三角函数的表达式的过程和解析指数函数基本一样。将sin、cos视为一元运算符,生成的后缀表达式为“x s”或是“x c”。在运算层面,基本项中需要添加两个容器来存储三角函数中的表达式。和指数函数相比,三角函数的求导会更加复杂,因此计算求导的过程需要修改。
第一次作业由于引入的运算种类少,所以没有发现什么正确性层面的bug。不过在测评自己代码的过程中我发现我的程序在运行含有7、8次方的长表达式时会很慢得到结果,这存在很大的TLE风险。经过设置断点调试,我发现了在幂函数指数运算后进行合并同类项的运算速度会很慢,因为要遍历几百万个基本项。于是我在每种运算("+-*/^")后都引用了一次合并同类项的方法,这使得程序的运算速度达到了很大的提升。
第二次作业由于加入了两种新的运算,出现了一些正确性层面的bug。首先在后缀表达式生成层面,我最开始在自定义函数实参替换形参的过程中没有考虑到指数函数的表达式exp中的x也会被替换掉,导致exp指数函数的表达式不能被正常读取,出现报错;在计算后缀表达式层面,由于加入了exp指数函数,基本项的复杂度升高了很多。在hw1中基本项对象只拥有系数和指数两个属性,而随着exp指数函数的加入,我为它添加了一个ArrayList用来储存exp指数函数中的表达式。这样便会产生表达式由基本项组成、基本项中含有exp表达式的层层嵌套的复杂结构,为表达式计算以及合并同类相都带来了不小的麻烦。在表达式加减法所涉及的方法中我原本使用了浅克隆,但由于基本项的复杂度增高,浅克隆会出现意想不到的错误,将浅克隆替换成深克隆就可以修正这些错误。
第三次作业在迭代的过程中没有产生什么bug,但在进行提取公因数的性能优化过程中产生了“同时嵌套多层可提取公因数的指数函数,但内层提取的公因数无法被输出”的bug。经过设置断点的调试发现是递归输出逻辑出现问题而导致的bug。
虽然在第一单元的公测和互测中都没有被测出bug,但在课下编写代码的过程中还是遇到了很多的问题。这些问题集中在程序中面向过程性强、代码冗长的地方,反映出我对java一些底层原理的不熟悉以及面向对象思想的理解不够深入。
在hw1中我选择用测评机生成的大量数据去运行别人的代码,取得了不错的成效。但随着测评机被推广,在hw2和hw3的互测中测评机都没有起到什么作用,所以我尝试自己去构造特殊一点的测试数据,比如和0有关的数据。值得一提的是,在hw2中由于表达式的形式变得复杂,能够优化的地方相比较于hw1也多了很多,通过阅读同组同学的代码我发现他们很多都选择了提取公因数这个优化方法,但是他们在优化时却少考虑了一些情况,导致输出不符合指导书中的格式要求。比如exp(-x)、exp(x)^-2。想要看懂别人写的代码虽然困难,但仔细阅读后也有可能会收获意外之喜。
由于hw2因为没有提取公因数丢了性能分,所以在hw3中加入了指数表达式内提取公因数的操作。实现过程偏面向过程,需要对表达式进行遍历,并利用BigInteger.gcd()方法去寻找最大公因数并将提到exp()外面。当然,在某些情景下提取公因数会导致输出长度不变或甚至是变长,因此我又写了一个ExtLen()方法来检测输出的长度,比较提取公因数前后输出字符串的长度,然后决定是否提取公因数。
#要注意去制定合理的长度比较标准:需要考虑到系数为1或-1时的长度该如何计算、是否需要把符号的长度也计算在内等细节。
public static int ExtLen(Polynomial input) { //测量系数长度
int len = 0;
for (int i = 0; i < input.getPolynomial().size(); i++) {
BigInteger coefficient = input.getPolynomial().get(i).getCoefficient();
BigInteger index = input.getPolynomial().get(i).getIndex();
Polynomial exp = input.getPolynomial().get(i).getExp();
if (coefficient.equals(BigInteger.ONE) ||
coefficient.equals(BigInteger.ONE.negate())) {
if (index.equals(BigInteger.ZERO) &&
exp.getPolynomial().isEmpty()) { //常数为1的系数需要正常输出
len = len + coefficient.toString().length();
} else {
if (coefficient.equals(BigInteger.ONE.negate())) {
len = len + 1;
}
}
} else {
if (index.equals(BigInteger.ZERO) && exp.getPolynomial().isEmpty()) {
len = len + coefficient.toString().length();
} else {
len = len + coefficient.toString().length() + 1; //乘号
}
}
}
return len;
}
注意提取的最大公因数不能是负数,否则会导致输出格式错误,同时要选择合理的方式输出提取出的指数。由于这只是进行简单的优化,代码长度较短,因此并不会对简洁性造成很大影响。
希望能够在性能优化上进行一定方向上的引导,在指导书中说一说在哪些地方可以优化或是给出某些优化的方法。
希望能够将评测中的cost设置的更加合理(由于exp的cost太小,室友被exp嵌套的数据hack了好几次)。