面向对象设计入门一:表达式求导

一.多项式求导

(一)对象和方法设计

  根据定义,多项式是由若干符号和项交替组成。和求导规则决定多项式求导等价对每个项求导。因此考虑一个项类,包含两个Biginteger类型数据次幂和系数,对应幂指数求导方法,toString()方法。为了化简,提供一个判断是否系数是负数的布尔值方法来判断表达式这一项前是否需要‘+’。表达式本质上转化为项的集合,为化简则选用不可重复集合。

  在主类中,按照工作流程分别设置静态方法输入分析,表达式求导和静态输出。保证输入正确性降低了鲁棒性要求,处理合法空白字符后直接使用正则表达式读取符号和表达式项组合,进而获得一个项类。表达式求导和静态求导就是遍历。

(二)对比作业程序和结果

  在对象上,并没有考虑项类,而是直接考虑了Hashmap的不可重复Biginteger组,优点是省事、简单粗暴。缺点是丧失面向对象的结构特征,使得所有代码都在主类中且都是静态方法。像极了面向函数编程。

HashMap<BigInteger,BigInteger> polyContent = new HashMap<>();

  输入处理上,首先是采用了大正则的形式,关键是忘记去空格预处理,用捕获组提取项。

Pattern p = Pattern.compile("\\s*([+-]\\s*)?((([+-]?\\d+\\s*\\*\\s*|[+-]\\s*)?x(\\s*\\*\\*\\s*([+-]?\\d+))?)|[+-]?\\d+)\\s*");
Matcher m = p.matcher(poly_line);
 while (m.find()){
  Pattern c = Pattern.compile("([+-]?)(\\d+)");
  Matcher m_c = c.matcher(m.group(2));
  if(m_c.matches())term.setboth(dealC(m.group(2)).multiply(operator) , BigInteger.ZERO);
  else term.setboth(dealcoe(m.group(4)).multiply(operator) , dealmi(m.group(5)));
}

  (三)分析度量

UML类图

 

 实际上另一个类的使用次数不多,本质上第一次作业代码没有解耦。

Statistics分析

 复杂度分析

 

 其中三个处理项字符串的方法和主类中求导方法基本复杂度较高,代表不够结构化。

(四)分析bug

    输入:1.带符号整数的读取:可前导零,长度超过20。2.空白符读取:指数符号**前后的空白符。3.项的读取:项的省略形式、常数形式、标准形式的组合。

   求导:1.常数项求导和非常数项求导。

   输出:1.常数项0的输出

(五)debug设计方法

  基本类型数据

1.对每一类项提供一个实例,比如-030*x**+0098, -x**+089, +x**-0156,x**-0012,-999*x,+67*x,-x,+x,x,-02436,+78,7。

2.对每种项组合,比如全变量项 符号 全变量项,获得“-030*x**+0098+-15*x**-3”。

3.任意两种项组合,比如全变量项 符号 指数省略项 符号 全变量项,获得“x**-0012++78*x-x**+02”

4.将上述表达式能添上空白符号添上空符号得到带空白符号的表达式,如:”    -030  * x ** +0098 +  -15 * x ** -3 + -75 * x ** 2345  “。

  特殊类型数据

  长数据表达式,比如:--03423525223423423252534240*x**+0232343242357657675675675653454098

  其他方法:设计对拍器

二.简单表达式求导

(一)对象和方法设计

  引入了乘号*和因子的概念,因子包括常数因子、幂函数因子、sin因子与cos因子,每个项都由四部分组成。因此建立四种因子类,属性均只包含一个Biginteger数据,提供一个返回项的求导方法。项类有四个属性,分别是四种因子;为了收集读入的每个因子提供multi_constant、multi_sinFunc等四种void方法来改变属性;提供求导方法,返回一个项集合;提供一个输出项的void方法;为了化简,还提供判断是否是同一项的布尔值方法isSameTerm(Term)与是否需要输出‘+’的needPLUS方法。

  主类大结构同上,由于加入鲁棒性的要求需要对表达式输入进行判断,因此需要预处理。加入一个Exception类输出WF。输入处理采用了预处理和代换的方法,先清除所有合法的空白字符,然后把(符号? 项)+结构中每一个符号修改为空格+[1\\*|-1\\*]的结构再使用Scanner读取每一项,再用相同的方式读取项中的每个因子。

(二)对比作业程序和结果

  因子类内部,提供求导方法。通过类内部定义为主类和父类中使用降低了代码量。

public class sin_factor {
    private BigInteger mi;
    public sin_factor(){}
    public sin_factor(BigInteger mi){
        this.mi = mi;
    }
    public BigInteger getMi(){
        return mi;
    }
    public void setMi(BigInteger mi){
        this.mi = mi;
    }
    public Term derivSin(){
        if(this.getMi().equals(BigInteger.ZERO))return new Term(new constant_factor(BigInteger.ZERO),new sin_factor(BigInteger.ZERO),new cos_factor(BigInteger.ZERO),new power_factor(BigInteger.ZERO));
        else {
            constant_factor C_dSin =new constant_factor(this.getMi());
            sin_factor sin_dSin = new sin_factor(this.getMi().subtract(BigInteger.ONE));
            cos_factor cos_dSin = new cos_factor(BigInteger.ONE);
            power_factor power_dSin = new power_factor(BigInteger.ZERO);
            return new Term(C_dSin,sin_dSin,cos_dSin,power_dSin);
        }
    }
}

 

  项内部,虽然没有层次关系,但因子为项提供了项一些接口。整体代码的利用率还算高。

public class Term {
    private constant_factor coe;
    private sin_factor sinFunc;
    private cos_factor cosFunc;
    private power_factor powerFunc;
    public Term (){}
    public Term (constant_factor coe , sin_factor sinFunc , cos_factor cosFunc , power_factor powerFunc){
        this.coe = coe;
        this.cosFunc = cosFunc;
        this.sinFunc = sinFunc;
        this.powerFunc = powerFunc;
    }
    public void multi_Cfact(constant_factor c){
        this.coe.setNumber(this.coe.getNumber().multiply(c.getNumber()));
    }
    public void multi_Sinfact(sin_factor sin){
        this.sinFunc.setMi(this.sinFunc.getMi().add(sin.getMi()));
    }
    public void multi_Cosfact(cos_factor cos){
        this.cosFunc.setMi(this.cosFunc.getMi().add(cos.getMi()));
    }
    public void multi_Powfact(power_factor power){
        this.powerFunc.setMi(this.powerFunc.getMi().add(power.getMi()));
    }
    //get
    public constant_factor getCoe(){
        return this.coe;
    }
    public sin_factor getSinFunc(){
        return this.sinFunc;
    }
    public cos_factor getCosFunc(){
        return this.cosFunc;
    }
    public power_factor getPowerFunc(){
        return this.powerFunc;
    }
    //deriv
    public ArrayList<Term> derivTerm(){
        ArrayList<Term> DTerms = new ArrayList<>();
        //constant terms
        if(this.getCoe().getNumber().equals(BigInteger.ZERO)){
            DTerms.add(new Term(new constant_factor(BigInteger.ZERO),new sin_factor(BigInteger.ZERO),new cos_factor(BigInteger.ZERO),new power_factor(BigInteger.ZERO)));
            return DTerms;
        }
        if(this.getSinFunc().getMi().equals(BigInteger.ZERO) && this.getCosFunc().getMi().equals(BigInteger.ZERO) && this.getPowerFunc().getMi().equals(BigInteger.ZERO)){
            DTerms.add(new Term(new constant_factor(BigInteger.ZERO),new sin_factor(BigInteger.ZERO),new cos_factor(BigInteger.ZERO),new power_factor(BigInteger.ZERO)));
            return DTerms;
        }
        //derive others
        if(!this.getSinFunc().getMi().equals(BigInteger.ZERO)){
            Term term_dsin = this.getSinFunc().derivSin();
            term_dsin.multi_Cfact(this.getCoe());
            term_dsin.multi_Cosfact(this.getCosFunc());
            term_dsin.multi_Powfact(this.getPowerFunc());
            DTerms.add(term_dsin);
        }
        if(!this.getCosFunc().getMi().equals(BigInteger.ZERO)){
            Term term_dcos = this.getCosFunc().derivCos();
            term_dcos.multi_Cfact(this.getCoe());
            term_dcos.multi_Sinfact(this.getSinFunc());
            term_dcos.multi_Powfact(this.getPowerFunc());
            DTerms.add(term_dcos);
        }
        if(!this.getPowerFunc().getMi().equals(BigInteger.ZERO)){
            Term term_dpower = this.getPowerFunc().derivPower();
            term_dpower.multi_Cfact(this.getCoe());
            term_dpower.multi_Sinfact(this.getSinFunc());
            term_dpower.multi_Cosfact(this.getCosFunc());
            DTerms.add(term_dpower);
        }
        return DTerms;
    }
    //same type term
    public boolean IsSametype(Term t){
        if(this.getSinFunc().getMi().equals(t.getSinFunc().getMi()) && this.getCosFunc().getMi().equals(t.getCosFunc().getMi()) && this.getPowerFunc().getMi().equals(t.getPowerFunc().getMi())){
            return true;
        }
        else return false;
    }
    public void addCoe(Term t){
        this.getCoe().setNumber(this.getCoe().getNumber().add(t.getCoe().getNumber()));
    }
    //output
    public String displayTerm(){
        String term_word = "";
        boolean needmul = false;
        //constant
        if(this.getSinFunc().getMi().equals(BigInteger.ZERO) && this.getCosFunc().getMi().equals(BigInteger.ZERO) && this.getPowerFunc().getMi().equals(BigInteger.ZERO)) {
            if(!this.getCoe().getNumber().equals(BigInteger.ZERO))return term_word.concat(String.valueOf(this.getCoe().getNumber()));
        }
        //cons factor
        if(this.getCoe().getNumber().equals(BigInteger.ZERO))return term_word;
        else if(this.getCoe().getNumber().equals(new BigInteger("-1")))term_word=term_word.concat("-");
        else if(this.getCoe().getNumber().equals(BigInteger.ONE))term_word="";
        else {
            term_word=term_word.concat(String.valueOf(this.getCoe().getNumber()));
            needmul=true;
        }
        //sin factor
        if(!this.getSinFunc().getMi().equals(BigInteger.ZERO)){
            if(needmul)term_word=term_word.concat("*");
            else needmul=true;
            term_word=term_word.concat("sin(x)");
            if(!this.getSinFunc().getMi().equals(BigInteger.ONE))term_word=term_word.concat("**"+String.valueOf(this.getSinFunc().getMi()));
        }
        //cos factor
        if(!this.getCosFunc().getMi().equals(BigInteger.ZERO)){
            if(needmul)term_word=term_word.concat("*");
            else needmul=true;
            term_word=term_word.concat("cos(x)");
            if(!this.getCosFunc().getMi().equals(BigInteger.ONE))term_word=term_word.concat("**"+String.valueOf(this.getCosFunc().getMi()));
        }
        //power factor
        if(!this.getPowerFunc().getMi().equals(BigInteger.ZERO)){
            if(needmul)term_word=term_word.concat("*");
            term_word=term_word.concat("x");
            if(!this.getPowerFunc().getMi().equals(BigInteger.ONE))term_word=term_word.concat("**"+String.valueOf(this.getPowerFunc().getMi()));
        }
        return term_word;
    }
    public boolean needplus(){
        if(this.getCoe().getNumber().compareTo(BigInteger.ZERO) > 0)return true;
        else return false;
    }
}

  对正负号的预处理,关键在于replaceall的特殊字符的替换,注意不要引入新的字符错误。比如特殊字符本身存在替换后无法察觉,就需要我们提前处理特殊字符;注意除了三个符号时考虑最后的符号是带符号整数的符号不予处理外,其余两个符号中‘+’替换为“1*”而非“  ”的原因是在于如果正负号出现在表达式的结尾空格将忽略这一错误。一定要注意替换前后是否会忽略某些错误。而且替换为常数因子乘积也为后面读取因子作预处理,省去了项头负常因子省略的处理。

String Eline = line.replaceAll(" |\\t","");
        Eline=Eline.replaceAll("#|%","@");
        Eline=Eline.replaceAll("\\*-","*#");
        Eline=Eline.replaceAll("\\*\\+","*%");
        Eline=Eline.replaceAll("\\+\\+\\+|--\\+|-\\+-|\\+--"," %");
        Eline=Eline.replaceAll("\\+\\+-|---|\\+-\\+|-\\+\\+"," #");
        Eline=Eline.replaceAll("\\+\\+|--"," 1*");
        Eline=Eline.replaceAll("\\+-|-\\+"," #1*");
        Eline=Eline.replaceAll("\\+"," 1*");
        Eline=Eline.replaceAll("-"," -1*");
        Eline=Eline.replaceAll("#","-");
        Eline=Eline.replaceAll("%","+");

 Exception类和抛出异常的标准写法。

class formException extends Exception{
    private String message;
    public formException(String message){
        this.message = message;
    }
    public String getMessage(){
        return message;
    }
}
public class MainClass {
    public static ArrayList<Term> getExpreline(String line) throws formException{
...
if(Eline.equals(""))throw new formException("WRONG FORMAT!"); } }

(三)分析度量

UML类图

 

 没有使用清晰的层次关系,但是不同类之间有接口的使用,使得程序本身效果还不错。

Statistic分析

 

 行数适中,主类中,注释行由于调试因此有些多。

复杂度分析

 

 

 

   关键在于主类中的处理方法和展示方法。常理Iv(G)应当远小于v(G),但这里几乎相同。圈复杂度高代表出现各种情况的可能性高,而Iv(G)高则说明模块之间耦合度高。

(四)分析bug

 WFbug

1.幂函数指数绝对值小于10000 2.除“ ”“\t”外空白字符非法 除此之外的空白项都会被读入,导致匹配错误。 3.由于替换,注意不合法的[+-]出现。比如:项内对的位置错误数量”x**-+5“;项内错误位置“x[+-]*[+-]*5[+-]*[+-]cos(x)[+-]*5” 4.注意不合法的‘*’,尤其是项末尾和开头。

求导

所有常数项的求导,如sin因子、cos因子和幂函数因子系数全为0,或常数项为0。

(五)对代码debug方法

  分析数据要求,输入表达式长度要在100 以内,幂函数指数不超过10000,不能输出结果为WF的数据。由于之前测试了多项式求导,因此空格与带符号整数的前导零可以不作为考察的重点。最简单获取样例输入的方法按照正则表达式利用python的Xeger包生成。

[+-]?([+-]?(x(\*\*[+-]?([1-9]\d*))?|(sin|cos)\(x\)(\*\*[+-]?([1-9]\d*))?|[+-]?([1-9]\d*))(\*(x(\*\*[+-]?([1-9]\d*))?|(sin|cos)\(x\)(\*\*[+-]?([1-9]\d*))?|[+-]?([1-9]\d*)))*)([+-]([+-]?(x(\*\*[+-]?([1-9]\d*))?|(sin|cos)\(x\)(\*\*[+-]?([1-9]\d*))?|[+-]?([1-9]\d*))(\*(x(\*\*[+-]?([1-9]\d*))?|(sin|cos)\(x\)(\*\*[+-]?([1-9]\d*))?|[+-]?([1-9]\d*)))*))*
#想要多0表达式可以将([1-9]\d*)替换为([1-9]\d*|0)可产生50%概率为0的数据。

  通过sympy对程序结果和求导结果进行对比,再利用bat脚本可以完成一个简单的对拍器。可以测试一般性数据的输出结果。

三.复杂表达式求导

(一)对象和方法设计

  在因子中,引入了可以在变量处嵌套因子的三角函数因子和括号加表达式组成的表达式因子。建立一个抽象类项,提供抽象方法求导和输出,提供返回项的乘和加方法,最基本的项如上四种同时继承抽象类项。为了实现对表达式项的解析,引入继承项的加法项和乘法项,包括两个项类型的属性分别指符号左侧和右侧的项。为了实现三角函数因子嵌套因子的解析,在三角函数类项内添加属性Mterm表示嵌套的内容。通过相同父类确定相同的节点,子类包含自身同类引用,可以形成类似表达式树的结构。

  输入表达式嵌套,对输入解析分为两个相互迭代的过程,解析表达式过程和解析因子过程。底端是解析因子到幂函数因子或常数因子。由于每一项复写求导和输出方法,对类似表达式树的项求导时类似按深度递归,输出时按照中序遍历可以得到表达式。

(二)对比作业程序和结果

  抽象类Term,为了控制括号输出还使用静态布尔变量。

abstract class Term {
    protected static boolean mulNbrack=false;
    protected static boolean triNbrack=false;
    abstract Term derive();
    abstract void showTerm();
    public Term multi(Term t){
        if(this.getClass()==Cterm.class && t.getClass()==Cterm.class) {
            return new Cterm(((Cterm) this).getNumber().multiply(((Cterm) t).getNumber()));
        }
        else return new multerm(this,t);
    }
    public Term add(Term t){
        return new addterm(this,t);
    }
}

    addterm项的类似按深度递归的求导方法和输出方法。

@Override
    public Term derive() {
        return new addterm(this.Mterm.derive(),this.term.derive());
    }
    public void showTerm(){
        //keep two booleans
        boolean mulN=mulNbrack;
        boolean triN=triNbrack;
        //deal
        if(Mterm.getClass() == Cterm.class){
            Cterm number = (Cterm)Mterm;
            if(number.getNumber().equals(BigInteger.ZERO)){
                if(mulNbrack && term.getClass()==addterm.class)mulNbrack=true;
                else mulNbrack=false;
                if(triNbrack && (term.getClass()==multerm.class || term.getClass()==addterm.class))triNbrack=true;
                else triNbrack=false;
                term.showTerm();
                mulNbrack=mulN;
                triNbrack=triN;
                return;
            }
        }
        if(term.getClass() == Cterm.class){
            Cterm number = (Cterm)term;
            if(number.getNumber().equals(BigInteger.ZERO)){
                if(mulNbrack && Mterm.getClass()==addterm.class)mulNbrack=true;
                else mulNbrack=false;
                if(triNbrack && (Mterm.getClass()==multerm.class || Mterm.getClass()==addterm.class))triNbrack=true;
                else triNbrack=false;
                Mterm.showTerm();
                mulNbrack=mulN;
                triNbrack=triN;
                return;
            }
        }

        if(mulNbrack || triNbrack)System.out.print('(');
        mulNbrack=false;
        triNbrack=false;
        this.Mterm.showTerm();
        System.out.print('+');
        this.term.showTerm();
        mulNbrack=mulN;
        triNbrack=triN;
        if(mulNbrack || triNbrack)System.out.print(')');
    }

在解析表达式前需要保存括号内部的内容不受影响,解析因子前需要匹配因子的格式,因此需要对最外层括号识别、替换,从而保存和用特殊符号替代括号内的内容。将最外层括号“()”替换为“pq”。

public static String outBrack(String exp) throws formException{
        Stack<Character> sk = new Stack<>();
        ArrayList<Integer> right = new ArrayList<>();
        ArrayList<Integer> left = new ArrayList<>();
        int i;
        for(i=0 ; i< exp.length() ; i++){
            if(exp.charAt(i)=='('){
                if(sk.isEmpty())left.add(i);
                sk.push('(');
            }
            if(exp.charAt(i)==')'){
                if(!sk.isEmpty() && sk.pop()=='('){
                    if(sk.isEmpty())right.add(i);
                    continue;
                }
                else throw new formException("WRONG FORMAT!");
            }
        }
        if(!sk.isEmpty())throw new formException("WRONG FORMAT!");
        //
        StringBuilder expbuilder = new StringBuilder(exp);
        Iterator i_right = right.iterator();
        while(i_right.hasNext()){
            int place =(int) i_right.next();
            expbuilder.replace(place,place+1,"q");
        }
        Iterator i_left = left.iterator();
        while(i_left.hasNext()){
            int place =(int) i_left.next();
            expbuilder.replace(place,place+1,"p");
        }
        return expbuilder.toString();
    }

(三)分析度量

UML类图

 

 出于使用考虑只用2层的结构,且由于没有多个类属选择了抽象类而非接口。

statistic分析

 

 复杂度分析

 

 

三复杂度都比较高的是方法中代码量较多的内容。 这告诉我们,对每一种方法和他的输出来说,当代码量上升的时候可以开始考虑解耦的问题。高复杂度可能会产生未知错误。

(四)分析bug

   1.按正则表达式匹配项时不同位置的空格。由于对表达式内容进行替换需要多次匹配,匹配失误会导致错误地WF。

    2.表达式化简过程,比如对求导结果的化简、对输出方式的化简导致产生未知错误。本次作业按照中序遍历输出添加括号会导致输出的内容过于复杂无法辨认,在对乘积项、和项以及对应括号化简时会引出新的问题。比如在使用静态变量达到括号化简的时候由于对含1乘积项输出时只输出非1项本身,导致含1乘积项的子项仍然是带1乘积项且子项为和的时候会不输出括号。

  3.特殊项。如sin(0)**0定义为1.

(五)对他程序debug方法 

  通过如上建立抽象类项和子项的层次关系,利用迭代、随机数、静态变量控制到达底端的方式生成表达式。输出方法基本与上述相同,但是注意在所有该添加空白符的地方添加空格,且此时输入数据长度只受迭代层次的影响。利用Sympy解析和用简单的bat脚本可以对代码进行对拍,得到bug样例后解析产生bug的原因将输入缩短至50以内,且保证指数严格大于0。

 总结

  三次作业重构三次,本身是因为没有对后续的内容探究,当然也是不想浪费时间。更重要的收获是尝试自己写评测机,虽然错误不少但是检验出了自己和同学的错误。最后一次作业给我的感受最强烈,之前一直没有考虑面向对象也能递归,这提醒我在复杂的类继承关系下可以达到更复杂的实现层次而非只是简单的按照规则继承。这一单元还是吃老本,下一单元可能面对更复杂的问题需要抽出时间学习。

 

posted @ 2020-03-18 21:59  ZhaoYunQz  阅读(387)  评论(1编辑  收藏  举报