(一)基于度量来分析自己的程序
第一次作业
思路:
通过正则表达式提取系数和指数并求导.
遇到的问题:
第一次作业主要困扰到我的地方在于正则表达式的书写。
之前利用python写爬虫的时候用过正则表达式,有粗浅的了解。但是在第一次作业用到正则时还是遇见以前没有考虑过的问题,第一次作业的表达式较长,而且我用的是大正则进行整个表达式的匹配,在过多贪婪时会出现爆栈的问题。
最初的解决方法
最初的想法较为直观,进行预处理减少正则表达式中的贪婪的项数可以满足题目所述的1000字符限制。
例如,在乘号‘*’与x,x与‘^’之间的空格不会导致 WF,所以可以去掉正则表达式中的部分空格和制表符的贪婪匹配。
\*[ \t]*?x[ \t]*?\^
可以在去掉*与x,x与^间的空格、制表符后简化为
\*x\^
经实验,在去掉这两处空格、制表符之前可以容纳最多458个+x,即916个字符。
去掉之后可以容纳636个+x,即1272个字符,勉强达到要求。
这样修改治标不治本,对于其他类型的易爆栈数据可能仍然存在问题。
进一步的解决方法
参考https://bbs.csdn.net/topics/390955111中的方法
1、如非必要将贪婪匹配修改为非贪婪匹配,比如:+改为+?,*改为*?
2、如非必要不要捕获分组:分组()改为(?:)
减少不必要的贪婪可以使得问题得到较好的解决。
使用百万级别字符串进行压力测试也不会出现爆栈问题,经尝试50万个+x仍然不会爆(但是会超时)。
下面是一个80万字符的测试结果
缺点:
耦合度和模块复杂度方面做的不够好,第一次整体代码还是写得很面向过程。
优点:
初步了解了继承的概念, 并在这次作业中得到了使用.
第二次作业
思路:
第二次作业引入了三角函数,在整体上我沿用了第一次的架构。
为了便于求导,将项表示为coeff*x^a*sin(x)^b*cos(x)^c这种一般形式,对于每一项可以使用统一的求导方式进行求导。
同时使用HashMap存储,便于合并同类项。
遇到的问题:
HashMap是线程不安全的, HashMap在使用iterator遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常
在迭代的同时进行优化势必涉及到项的增减, put,remove操作会抛出Concurrentmodificationexception异常.
最初的解决方法
利用iterator.remove()可以解决当前迭代项的删除问题, 但是在迭代的同时对于多项进行删改时此方法就难以解决了.
进一步的解决方法
进一步的解决方法是利用ConcurrentHashMap替代HashMap, 由于这两者的接口几乎完全一致, 可以直接在初始化map时将原本的HashMap替换为ConcurrentHashMap, 而不用对于代码进行大规模改动.
ConcurrentHashMap允许一边更新、一边遍历,在Iterator对象遍历的时候,ConcurrentHashMap也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化.
经试验, ConcurrentHashMap不会在这次作业对时间和空间的性能产生太大的负面影响, 所以可以基本满足要求了.
缺点:
在PolyPattern的设计中, 个别方法(如取项的方法fetchTerm)的规模过大.
优点:
1. 此次对于面向对象熟悉了一些, 对于继承的使用更为熟练
2. 将不同功能的模块进行了拆分, 进行了较为合理的规划.
第三次作业
思路:
这次作业将因子划分为sin,cos,x^n,常数这四种类型, 因子通过乘法组合为项, 项通过加法组合为表达式. 每一种类型都继承自统一的基础类, 设立了统一的toString()和diff()接口, 用于统一输出和求导, 便于递归.
在处理输入处理方面思考了较长时间, 识别出加减乘这些运算就意味着接下来是因子, 并依照加法(减法也统一为乘-1因子后的加法)和乘法作为项中的下一个因子和新的项分别处理.
再将项通过加法类并列到表达式中存储项的ArrayList里, 项的内部又是通过乘法类构建出并列的存储因子的ArrayList.
Add /...|... 项 .........项 /| ..Mul..../| 因子................因子
这次与第二次作业最大的不同在于第二次的每一项可以统一为四种因子: 系数,x^a,sin(x)^b,cos(x)^c相乘, 但是第三次由于嵌套的存在没有这种统一的形式, 第二次的代码不能够实现重用.
在求导时, 通过Add, Mul以及各种因子统一的求导接口可以进行递归求导, 在因子处结束递归返回结果.
缺点:
1. 这次作业思考耗时过长, 想了一天半在思路没有完全成型的情况下担心时间不够而急于编码, 中间发现递归的思路有点问题于是打翻重来, 整个作业花了四天才彻底完成.
应该在正式敲代码之前应该进行思路的慎重思考, 并且在确定方法的合理性之后再开始码代码.
2. 由于写完就没有什么剩余时间了, 所以没有进行优化, 测试也是很不充分.
优点:
1. 充分利用了继承.
2. 对于各种因子和运算方法进行了抽象.
(二)分析自己程序的bug
第一次作业
测试不充分, 大部分时间都在考虑正则表达式爆栈的问题, 而忽略了格式处理的bug.
没有覆盖到x^0这种情况, 在进行输出时当次数为0就没有输出加号.
第二次作业
指导书稍有歧义导致理解错误
这一句我理解为空号中有且仅有x, 不能有空格, 相应的正则表达式也将括号内含空格的情况视作 WRONG FORMAT!
当然指导书后续部分专门针对空格符有补充说明, 表示sin(x)括号中有空格符又是合法的.
但在写代码时对于此处的第三条没有深思, 仅按照了上面的第一印象认为括号内空白符不合法, 所以在互测出现错误.
ps.个人仍认为指导书上下文有点矛盾, 有空格显然不符合"括号内仅为x"的条件.
第三次作业
WRONG FORMAT!的考虑仍然不充分, 对于括号开头有正负号的情况没有识别出来, 互测出现了问题.
(三)分析自己发现别人程序bug所采用的策略
自动化测试
借鉴讨论区中的方法,用python和shell写了自动评测脚本。参见https://github.com/sherpahu/OO-test
sympy进行正确性判断
from sympy import * from sympy.abc import x from random import choice import sys def main(canshu1,canshu2): expected=canshu1 actual=canshu2 expected = expected.replace("^","**") actual = actual.replace("^","**") out=diff(eval(expected)) #print(out)#视情况输出 ans=eval(actual) print(simplify(simplify(out)-simplify(ans)) == 0 ,end="") print(simplify(out-ans) == 0,end="") print(out.equals(ans)) main(sys.argv[1],sys.argv[2])
这个python脚本吸收了讨论区里的部分思路, 以sympy为基础, 调用三种判断方式对于结果进行判断.
在某些特定情况下(如输入数据过长而又充分优化),某一两种测试方法会判定false.
但是经在互测提交试验后发现这种情况都是空刀,只要有一种输出true就应该是正确结果.
所以统一进行三种类型的判断, 保证判定结果的可靠性.
shell脚本进行自动化统一互测
#!/bin/bash PRE_IFS=$IFS IFS=$' '#read by line rm result.txt > result.txt cat test.txt | while read line do echo "$line" >> result.txt echo "$line" | java -jar Alterego.jar | sed -n '1p' >> result.txt echo "$line" | java -jar Archer.jar | awk 'NR==1{print}' >> result.txt echo "$line" | java -jar Assassin.jar | awk 'NR==1{print}' >> result.txt echo "$line" | java -jar Berserker.jar | sed -n '1p' >> result.txt echo "$line" | java -jar Caster.jar | sed -n '1p' >> result.txt echo "$line" | java -jar Lancer.jar | sed -n '1p' >> result.txt echo "$line" | java -jar Saber.jar | sed -n '1p' >> result.txt done cat result.txt | while read lineIn do read line1 read line2 read line3 read line4 read line5 read line6 read line7 echo "$lineIn" echo "Alterego:" python diff.py $lineIn $line1 echo "Archer:" python diff.py $lineIn $line2 echo "Assassin:" python diff.py $lineIn $line3 echo "Berserker:" python diff.py $lineIn $line4 echo "Caster:" python diff.py $lineIn $line5 echo "Lancer:" python diff.py $lineIn $line6 echo "Saver:" python diff.py $lineIn $line7 done IFS=$PRE_IFS
首先将java代码编译再打包为jar, 放在同一路径下, 在test.txt中分行输入测试样例, 再调用python脚本进行判定.
编译以及jar的获取方法
之前讨论区中对于MANIFEST.MF的说明很模糊, 为了复现生成jar的操作卡了很久.
在使用命令得到jar前需要首先编译,再针对不同的主类名填写MANIFEST.MF文件.
MANIFEST.MF顾名思义是配置文件的意思, 这个文件中必须要进行主类名的说明.
jar在多人互测时比较方便.
当然, 如果只是针对自己的一个project可以在编译后直接用java Main
的方法(Main为主类名)调用主类
用了包:
在包的父目录命令行下输入以下命令完成编译
javac -encoding UTF-8 father/*.java
再在命令行下输入 vim MANIFEST.MF
写入相应的MANIREST.MF文件内容, 此处father为.java代码文件的父路径, Main为主类名(下同)
Manifest-Version: 1.0 Main-Class: father.Main
值得注意有两点
- Main-Class: father.Main的“:”后必须有空格,此行后必须有换行
- 父目录下运行命令打包jar
jar cvfm Dalibao.jar father/MANIFEST.MF father/*.class
MANIFEST.MF在很多博客包括讨论区中被一笔带过, 但是这两种不起眼的错误却会导致jar无法找到主类名
没使用包:
在src下命令行运行 javac -encoding UTF-8 *.java
再在命令行中输入 vim MANIFEST.MF
后写入MANIFEST.MF的内容
Manifest-Version: 1.0 Main-Class: Main
打包jar
jar cfm Dalibao.jar MANIFEST.MF *.class
使用jar
在jar当前目录下输入
java -jar Dalibao.jar
互测可以利用下面的test.sh直接自动化统一测试.
进一步的详细说明可以参考https://github.com/sherpahu/OO-test/blob/master/README.md
充分性测试
完成自动化测试脚本后, 我对于各种项的组合进行了枚举测试,即系数从-10到10,x、sin(x)、cos(x)的指数从-5到5(防止数据过大,在覆盖性测试中没有使用过大的指数)。
在第三次作业中, 我通过对各种因子进行枚举(例如有无系数, 系数的正负零, 有无指数, 指数的正负零), 再利用因子与sin() cos() ()的随机嵌套组合的方法生成测试样例.
(四)Applying Creational Pattern
抽象工厂
第三次作业中我将各类因子, 如x^n, sin(x)^n, cos(x)^n以及系数, 划分为不同模块.
也将加法和乘法操作独立出来.
尽量实现低耦合, 减少某一模块出错影响整个项目的概率.
重构
输入的处理我做的还不够完善, 如果要进一步重构的话, 我会按照递归下降的思路, 对于输入因子以及运算符进行识别, 分别处理.