设定
程序要求:
对带符号整数 ,幂函数,表达式,三角函数,表达式因子组合而成的多项式进行求导运算。求导规则符合数学规则,链式求导,复合求导以及乘法求导。
在本次作业中,空白字符包含且仅包含<space>和 。
此外,值得注意的几点是:
带符号整数内不允许包含空白字符。
幂函数、项、表达式,在不与上一条矛盾的前提下,可以在任意位置包含任意数量的空白字符。
如果某表达式存在不同的解释方式,则只要有任意一条解释中是合法的,该表达式即为合法。
描述与判定
关于输出,首先程序需要对输入数据的合法性进行判定。这道题在输入处理上是一个关键,只有处理好输入数据,才能进行下一步的求导工作。处理输入在这次作业中运用到了正则表达式的匹配,以及递归法和正则表达式的混合使用。
如果是一组合法的输入数据(即符合上述的表达式基本规则),则应当输出一行,表示求出的导函数。求导函数则是对字符串进行加操作,在本次作业中运用了表达式树求导,简化了工作,相当于只对x和常数进行求导(其实常数没有操作,直接求导为零)。
解题思路:
1.从C语言到Java语言的思想转化
学习面向对象思想,面向对象的三个基本特征:继承、封装、多态。
用C语言写程序的时候,我们常常把程序写成流程化形式,分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了(即面向过程编程);而面向对象编程是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
举个例子:(中国象棋游戏)
(1)面向过程步骤就是:1、开始游戏,2、红方先下,3、判断是否吃棋并绘制画面,4、判断输赢,5、换黑方下棋,6、与步骤3、4一样,8、返回步骤2,9、输出最后获胜结果。把上面每个步骤按照顺序依次用不同的函数来实现。
(2)面向对象则是:1、棋子对象(红黑两方),这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,判断是否吃棋以及输赢。面向对象是以功能来划分问题,而不是步骤。
2.对象与类的定义
对象:是对客观事物的抽象。
类:对对象的抽象。
或许对这两个概念还是很迷糊,(其实还会把自己给说绕了哈哈哈)。不过说个例子就能明白了。
例如:“女生”是一个类(class),“男生”也是一个类,但“你”和“我”是一个对象(emm我算撩你吗?没有对象怎么面向对象编程)。
百度百科解释:它们的关系是,对象是类的实例,类是对象的模板。对象是通过new className产生的,用来调用类的方法;类的构造方法 。
对象还具有属性、方法(C语言的函数)之类的……
类类型的声明:
1 class 类名 2 { 3 public: 4 //公用的属性和成员方法 5 protected: 6 //保护的属性和成员方法 7 private: 8 //私有的属性和成员方法 9 }
同一个包中公共类是可以调用的,但私有类是不可以调用的。本题我用到的类有主函数、表达式匹配、表达式树、树节点、求导类。但我并没有用到继承和接口,是我自己还没有学会使用,也觉得没有必要用,所以继承、多态这个特性没有使用到。
3.Main方法的定义
public static void main(String[] args){ …… } public:代表该函数的访问是最大的。 static:代表着主函数随着类的加载就已经存在了。 void:主函数没有具体的返回值。 main:不是关键字,是一个特殊的单从,可以被jvm识别。 (String[] args):函数的参数,参数类型是一个数组,该数组中的元素是字符串,字符串类型的数组。
4.正则表达式的匹配
此题的关键就是利用正则表达式对输入格式进行匹配,只有输入的多项式符合格式,才能进行计算。
具体的正则表达式大家可以百度或者博客搜索,有很多详细的描述和讲解。
正则表达式的匹配需要用到Java里的 1 import java.util.regex.Matcher; 2 import java.util.regex.Pattern; 这两个包。运用自带的maches,find,lookAt等方法可以达到很好的效果。对于最后一次嵌套表达式我是利用递归匹配的思想,有同学利用了有限状态机,这也是一个状态转换的过程。特别是第三次作业,我想了好几个小时没有思路,差点退却了,但几经思考,我尝试递归,尝试暴力,硬着头皮写出来了。(熬了两天夜,debug到凌晨4点没有效果也是正常的事),但值得一提的是,很多时候每个的思考角度不同,所以debug的时候互相讨论是非常有必要的,互相询问应该存在的问题,这是一个很有效的方式。
"([ ]*[-+]?[ ]*[-+]?((\d+([ ]*\*[ ]*x([ ]*\^" + "[ ]*[-+]?\d+)?)?)|([ ]*x([ ]*\^[ ]*[-+]?\d+)?))" + "[ ]*)([ ]*[-+][ ]*[-+]?((\d+([ ]*\*[ ]*x([ ]*" + "\^[ ]*[-+]?\d+)?)?)|([ ]*x([ ]*\^[ ]*[-+]?\d+)?" + "))[ ]*)*+";
5.求导以及合并同类项
求导在这个部分首先以为会是最难的部分,但写到后面的代码时,发现求导并不是最难的,而递归才是问题,于是第三次求导作业我选择了表达式树,通过建树来进行求导,大大简化了求导的代码,只保留了对x的求导,不过需要自己定义一个Node类,还需要用到栈的思想,这是数据结构与面向对象一起使用的效果。由于考虑到性能问题(就是求导结果的字符串长短问题),我们需要对求导结果的字符串进行合并同类项,需要用到动态数组之类的方法,例如Arraylist与hashmap。
下面是Node类的定义:
1 public class Node { 2 private String data; 3 private Node lchild; 4 private Node rchild; 5 //private String ans; 6 7 public Node() { 8 } 9 10 public Node(String data) { 11 this.data = data; 12 this.lchild = null; 13 this.rchild = null; 14 } 15 16 public Node(String data, Node lchild, Node rchild) { 17 super(); 18 this.data = data; 19 this.lchild = lchild; 20 this.rchild = rchild; 21 } 22 23 public String getData() { 24 return data; 25 } 26 27 public Node getLchild() { 28 return lchild; 29 } 30 31 public Node getRchild() { 32 return rchild; 33 } 34 35 }
随后就开始通过正确的表达式进行建树,在建树之前需要对表达式处理一下,就是保证每个式子都是两个数与一个符号相连接,这样才能形成表达式树进行求解,这是求导真正部分的代码:
1 public String Polyterm(String group) { 2 if (group.equals("x")) { 3 return "1"; 4 } else { 5 return "0"; 6 } 7 }
建树:
1 public class Tree { 2 private String string = ""; 3 private Node root; //根节点 4 5 public String create(String str) { // create three 6 Stack<Node> poly = new Stack<Node>(); // tree stack 7 Stack<String> op = new Stack<String>(); // op stack 8 node(str, poly, op); 9 root = poly.peek(); // root node 10 //求导 11 return resovle(root); 12 } 13 }
于是就开始遍历整个树节点,根据符号进行相应的求导:
1 public String resovle(Node tree) { 2 Polyterm factor = new Polyterm(); 3 if (tree.getLchild() == null && tree.getRchild() == null) { 4 return factor.Polyterm(tree.getData()); 5 } 6 if (tree.getData().equals("+")) { 7 return "(" + resovle(tree.getLchild()) + "+" + 8 resovle(tree.getRchild()) + ")"; 9 } else if (tree.getData().equals("-")) { 10 return "(" + resovle(tree.getLchild()) + "-" + 11 resovle(tree.getRchild()) + ")"; 12 } else if (tree.getData().equals("*")) { 13 return "(" + resovle(tree.getLchild()) + "*(" + getTree( 14 tree.getRchild()) 15 + ")+" + resovle(tree.getRchild()) + "*(" 16 + getTree(tree.getLchild()) + "))"; 17 } else if (tree.getData().equals("^")) { 18 String a = new BigInteger(tree.getRchild().getData()). 19 subtract(BigInteger.ONE).toString(); 20 String ans = "((" + getTree(tree.getRchild()) + ")*" + getTree( 21 tree.getLchild()) + "^" + a + "*"; 22 return ans + resovle(tree.getLchild()) + ")"; 23 } else if (tree.getData().equals("s")) { 24 return "cos(" + getTree(tree.getRchild()) + ")*" 25 + resovle(tree.getRchild()); 26 } else { 27 return "(-sin(" + getTree(tree.getRchild()) + ")*" 28 + resovle(tree.getRchild()) + ")"; 29 } 30 }
6.自己遇到的问题及bug
1.空白字符比较坑,有可能你的程序运行报错,可能是匹配出现了问题;
2.由于Matcher包的方法所限制,当“+”或“*”过多时,自测500个“+x”字符串就会导致栈溢出,此时可采用单步匹配,可以解决问题,更建议使用独占模式(可以搜索贪婪、懒惰、独占模式);
3.注意正则表达式的正确性,即合法格式;
4.输出结果可以为最简式,0次方不出现,1次方可省略等。
7.互测所遇到的bug
在互测的时候,我并没有采取诸如评测机、对拍器等这种自动化简单的方式,都是自己构造数据,然后人工验证。自动化构造数据和检验数据确实是一个很高效并且不用花费力气的方式,以后我还会改进,但其实自己构造数据更能达到debug的目的,能掌握对方bug所存在的点,然后进行hack。
由于每个人的代码风格不同,在debug的时候会发现代码很难读下去,看不懂是很常有的事,而且对于自己的代码写得太少(没有优化),别人的代码非常长,就没有兴趣看代码了。
首先考虑的是对方正则表达式是否匹配正确,格式有没有考虑全,空白字符以及栈溢出的报错信息(这个可以运用我前面博客写到的try...catch来捕获);其次就是构造0次方,1次方,以及乘0,乘1,乘有前导0的表达式,但每个人都用了大整数类型,对常数因子的处理还是比较规范;在一个就是求导是否正确,是否多符号,少符号;最后就是合并是否正确,我个人是合并很少部分,有的地方没有合并,所以表达式结果很长,但正确性可以保证,有的同学往往在合并的时候出了错,这是不值的,宁愿不要性能分,也要保证结果正确啊。
8.度量分析
类图:
代码长度:
复杂度: