OO第一单元作业总结

朱雄伟-22373498 学生 2024-03-21 21:36:13

OO第一单元作业总结

前言

这是本学期OO课程的第一次作业。得益于上学期上过的OOpre,第一单元的工作相对来说较为容易。虽然OOPre喜提一次强测0分

第一单元的总体任务是对表达式进行化简,之后加了对指数函数、多重括号嵌套、自定义函数与求导的迭代开发。

第一次作业

设计思路

本次作业的UML类图如下:

解析类负责将已处理的表达式字符串进行解析得到Expr对象,再由Expr对象通过计算转化为Poly对象,最后调用Poly对象的输出方法进行输出。

表达式可以解析为ExprTermFactor三级,其中Factor可分为幂函数、常数因子、表达式因子三类。Factor的行为抽象层次重合度高,所以我们可以引入Factor接口,并使用NumFactorPowFactorExpr三个类来实现。

Expr类中使用ArrayList来容纳该表达式中含有的TermTerm类中使用ArrayList来容纳该项中含有的Factor

 //Factor.java
 public interface Factor {
     //......
 }
 ​
 //Expr.java
 public class Expr implements Factor {
     //......
 }
 ​
 //NumFactor.java
 public class NumFactor implements Factor {
     //......
 }
 ​
 //PowFactor.java
 public class PowFactor implements Factor {
     //......
 }

整体流程

对输入的表达式进行解析,我能想到的有正则表达式递归下降法两种方法。越读了一些学长学姐的博客,他们都选择了递归下降法,所以我在作业中也采用的是递归下降法。

对于递归下降解析表达式并进行化简的方法,我大概分成了三大板块——字符串处理、表达式解析、计算化简。

字符串处理

在这部分我没有做太多的工作,主要是看到有些博客中提到进行处理时或多或少都碰到一些奇奇怪怪的bug,从而导致强测互测起飞的故事。

对于输入的字符串,我使用Pretreat类进行处理。

 // Pretreat.java
 public class Pretreat {
     private void delWhite() {
         // 删除表达式中的所有空格
     }
 ​
     private void delExpSymbol() {
         // 删除指数可能带有的正号
         // 这是我在中测时发现的bug,图省事就直接把"^+"变为"^"
     }
 }

对于输出的字符串,使用Simplify类。

 // Simplify.java
 public class Simplify {
     private void delAddSub() {
         // 删除或转换连续的正负号
         // 例如,把"+-"替换为"-"
     }
 }

我在本单元的三次作业都采用了以上两个工具类。最初的设想是为了化简表达式,让字符串变短,所以设计了两个类专门处理,但是发现可优化的部分并不多,现在看来可以统一成一个MyIOHandle类似乎就可以了。

表达式解析

结合公众号的相关分享,我使用了LexerParser类对语法单元Token进行解析。为了方便存储语法单元的基本信息,我也为此专门设计了一个Token类。

 // Token.java
 public class Token {
     public enum Type {
         EOF, ADD, SUB, MUL, LPAREN, RPAREN, NUM, VAR, POW
     }
 ​
     private final Type type;
     private final String content;
 ​
     public Token(Type type, String content) {
         this.type = type;
         this.content = content;
     }
 ​
     public Type getType() {
         return this.type;
     }
 ​
     public String getContent() {
         return this.content;
     }
 }
 ​

结合Token类可实现词法分析的Lexer类:

 // Lexer.java
 import java.util.ArrayList;
 ​
 public class Lexer {
     private final ArrayList<Token> tokens = new ArrayList<>();
     private String inputString;
     private int pos = 0;
     private final int length;
     private int tokenspos = 0;
     private final int tokenslength;
 ​
     public Lexer(String inputString) {
         // ……
         while (pos < length) {
             c = inputString.charAt(pos);
             if (c == '(') {
                 tokens.add(new Token(Token.Type.LPAREN, "("));
             } else if (c == ')') {
                 tokens.add(new Token(Token.Type.RPAREN, ")"));
             } else if (c == '+' || c == '-') {
                 // ……
             } else if (c == '*') {
                 // ……
             } else if (c == '^') {
                 // ……
             } else if (Character.isLetter(c)) {
                 // ……
             } else if (Character.isDigit(c)) {
                 // ……
             }
             pos++;
         }
         tokenslength = tokens.size();
     }
 ​
     private boolean notEnd() {
         // Token读完没?
     }
 ​
     public Token now() {
         // 返回当前位置的Token对象
     }
 ​
     public void next() {
         // 指向下一个Token
     }
 ​
 }
 ​

为了实现低耦合的目标,我在Lexer类里构造了专门解析特定单元的方法。以读取数字为例:

 // Lexer.java
 private String getNumber() {
     String numString;
     // 第一步:把所有的数字字符读取出来
     // 第二步:删除所有的前导0
     // 注意检查数字恰好为0是numString是否为空串,若是就补充一个'0'
     return numString;
 }

值得一提的是,很多同学处理连续正负号的时候,都是采用简单的字符串替换的方式。我采用了一种不同的方法。在读取到一个正负号时,我会尝试读取后面的字符判断是否为正负号,并更新整体的正负性

Parser类的设计基本上沿用了实验课和公众号文章的整体思路,总体上分为parseExpr()parseTerm()parseFactor()三类。最后的结构是parseExpr()调用parseTerm()parseTerm()调用 parseFactor()parseFactor()调用parseExpr()

 // Parser.java
 public class Parser {
     public Expr parseExpr() {
         Expr expr = new Expr();
         // 判断整个表达式前是否带负号,并赋给第一个Term
         expr.addTerm(parseTerm(sign));
         while (lexer.now().getType() == Token.Type.ADD
                 || lexer.now().getType() == Token.Type.SUB) {
             // 处理Term的符号
             expr.addTerm(parseTerm(sign));
         }
         return expr;
     }
 ​
     public Term parseTerm(int sign) {
         Term term = new Term(sign);
         // sign表示整个项的正负性
         term.addFactors(parseFactor());
         while (lexer.now().getType() == Token.Type.MUL) {
             // 解析因子
             // 假设出现"3 * -2"的情况,“*”的下一个Token是“-”
             // 可以通过塞入因子“-1”的形式,事实上转换为"3 * -1 * 2"
             // ……
         }
         return term;
     }
 ​
     public Factor parseFactor() {
         if (lexer.now().getType() == Token.Type.LPAREN) {
             // 解析表达式因子
         } else if (lexer.now().getType() == Token.Type.VAR) {
             // 解析PowFactor
         } else {
             // 解析NumFactor
         }
     }
 }

计算化简

观察最底层的Factor类,可以将其数学表示抽象为

$$
PowFactor = a x^b
$$

 

$$
NumFactor = number = number * x^0
$$

 

那么,TermExpr对象实际上都可以化简成

$$
\sum coef * x^{index}
$$

 

的形式。

数学形式相近,我们可以由此建立BasicUnit基本单元类和Poly多项式类。Poly是多个BasicUnit的和。

 // BasicUnit.java
 public class BasicUnit {
     private BigInteger coef;
     private final int exp;
     private final int sign; // 整体的正负号
 ​
     @Override
     public String toString() {
         // 输出为"a*x^b"的形式,可化简优化
     }
 ​
     public BasicUnit negate() {
         // 相反项
     }
 ​
     public void add(BasicUnit other) {
         // 加法运算
     }
 ​
     public BasicUnit mul(BasicUnit other) {
         // 乘法运算
     }
 }

Poly类使用ArrayList来存储BasicUnit。在Poly类实现多项式的加法、乘法与乘方运算,同时重写toString()方法。

import java.util.ArrayList;
import java.util.Collections;

public class Poly {
    private ArrayList<BasicUnit> basicUnits = new ArrayList<>();
    // ……

    public Poly addPoly(Poly other) {
        // 加法运算
        // 在这里可以合并指数相同的项
    }

    public Poly mulPoly(Poly other) {
        // 乘法运算
    }

    public Poly powPoly(int exp) {
        // 乘方运算
    }

    @Override
    public String toString() {
        
    }
}

然后,在Expr等类里均实现一个toPoly()方法。

对于NumFactorPowFactor,只需要返回一个只含有一个BasicUnitPoly对象。Term类把所有Factor.toPoly()Poly对象相乘,Expr类把所有Term.toPoly()类相加。

最后调用PolytoString方法,我们就得到答案了!

优化方法

  • 输出BasicUnit减少不必要的符号输出,如10-x等。

  • 对于Poly.toString()得到的输出,由于每个BasicUnit均由+连接,需要将+-转化为-

  • 最终输出时优先输出正数,如-x+1可以变为1-x

Bug分析

中强测、互测均未出现bug。

调试的时候出现了一些bug,比如:

  • x^0+0*x^3输出为0+0,判断合并上出现失误,最后通过在最后toString之前清除所有的0项解决。

  • ^ +0003解析不到位

第二次作业

这次作业新增了指数函数与自定义函数的需求。

设计思路

本次作业的UML类图如下:

本次迭代新增了IOHandleExpFactorFuncFactor类。IOHandle类专门处理了字符串的输入与输出。

整体流程

过程不再赘述,主要分享一下迭代新增的内容。

调用函数

为了便于自定义函数的定义和解析,本次作业新建了一个工具类FuncDefiner,主要处理自定义函数的定义和调用。该函数的成员和方法都是静态的,意味着不需要实例化对象,直接通过类名即可调用。

// FuncDefiner.java
public class FuncDefiner {
    // 通过函数名称获得函数定义式
    private static HashMap<String, String> funcs = new HashMap<>();
    // 通过函数名称获得形参列表
    private static HashMap<String, ArrayList<String>> args = new HashMap<>();

    public static HashMap<String, ArrayList<String>> getArgs() {
        return args;
    }

    public static HashMap<String, String> getFuncs() {
        return funcs;
    }

    public static void addFunc(String funcDefineString) {
        // 解析函数表达式并更新HashMap
    }

    public static String callFunc(String funcName, ArrayList<Factor> paras) {
        // 函数调用时使用
        // 根据funcName获取形参列表,由形参列表与实参列表的对应关系
        // 将函数定义式中的形参替换成实参的字符串形式
        // 为了防止替换时出现问题,可以多套一些括号
    }
}

表达式解析

Parser类增加parseExpFactor()pareseFuncFactor()两个方法。相对应的修改parseFactor()的方法。

// Parser.java
public Factor parseExpFactor() {
    // 解析Expr
    // 解析指数函数的幂
}

public Factor parseFuncFactor() {
    ArrayList<Factor> paras = new ArrayList<>();
    // 注意替换逗号时出现不必要的替换
    String[] list = tempString.split(",");
    for (String single : list) {
        // 解析逗号分割的实参,转换成Expr对象
    }
    // ……
    return new FuncFactor(name, paras);
}

计算化简

这次迭代,Expr对象可转化成

$$
\sum coef * x^{index} * exp(F(x))
$$

 

的形式。其中F(x)可以是一个Expr转化成的Poly。所以对BasicUnit进行一些适当的修改,然后重写一下计算方法,答案就出来了。

优化方法

本次作业优化方向主要有两个:exp内部括号层数与提取公因数。

括号层数控制

假设exp()括号内的部分是一个长的像因子的Expr,比如1x^12之类,输出的时候可以输出为exp(x^12),而不必输出为exp((x^12))。那么,怎么判断一个Expr是一个长的像因子的Expr呢?

我在BasicUnit类和Poly类都实现了相关的方法:

// BasicUnit.java
public String toOrdExpString() {
    StringBuilder sb = new StringBuilder();
    sb.append("exp(");
    if (poly.isZero()) {
        // "1"
    } else if (poly.isFactor()) {
        // poly是F(x)转化成Poly的结果
        // 只需一层括号
    } else {
        // 需两层括号
    }
    return sb.toString();
}


// Poly.java
public boolean isFactor() {
    if (isZero()) {
        // 必然的
    } else if (basicUnits.size() > 1) {
        // poly的项大于1,显然不可能像Factor
    }
    // 到这,basicUnits.size() == 1
    BasicUnit unit = basicUnits.get(0);
    // 可能符合要求的情况有:
    // 1. unit为0
    // 2. unit为"常数"
    // 3. unit为"x^指数"
    // 4. unit为exp(一堆东西),整体来看是一个exp因子
}

同时,判断是否可以合并需要判断F(x)是否相等,可以通过以下的方式实现:

// BasicUnit.java
public boolean isSame(BasicUnit other) {
    return this.index.compareTo(other.index) == 0 &&
            this.poly.isSame(other.poly);
}

// Poly.java
public boolean isSame(Poly other) {
    // 全非空,为真
    // 基本项的个数不同,为假
    int fitCount = 0;
    for (BasicUnit ba : this.basicUnits) {
        for (BasicUnit bb : other.basicUnits) {
            // 判断两个BasicUnit相等
        }
    }
    // this.basicUnits的项均可以对应到相等的基本项,为真
}

提取公因数

遍历寻找Poly对象中每一个基本项系数的最大公因数。

如果优化效果达到比一般方法更长,则舍弃该优化。这样就可以保证,即使长度一样,也保守地采用原来的化简形式。

// BasicUnit.java
public String toExpString() {
    String sim = toSimExpString(); // 尝试提取公因数
    String ord = toOrdExpString();
    if (//sim提取出了非法公因数) {
        return ord;
    } else if (//sim长度更短) {
        return sim;
    }
    return ord;
}

Bug分析

中测、强测、互测均无出现bug。

hack阶段找到了三个人的问题,问题出在exp((-2 * x))上,他们提取得到了错误的结果exp(x)^-2

当然,强测的性能分没有拿满。我也不知道怎么像优化仙人一样优化了

第三次作业

这次作业新增了求导因子的需求。

设计思路

本次作业的UML类图如下:

本次设计与第二次作业相比,只添加了DxFactor类。

整体流程

深拷贝

为了防止不必要的对象属性更改,我在本次作业中对Poly类和BasicUnit类实现了序列化深拷贝的方法。

// Poly.java
public class Poly implements Serializable {
    private ArrayList<BasicUnit> basicUnits = new ArrayList<>();

    public Poly getSame() {
        Poly newPoly = null;
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bout);
            out.writeObject(this);
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bin);
            newPoly = (Poly) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return newPoly;
    }
}

求导因子

求导因子的形式为dx(表达式),可以以类似于处理ExpFactor的方式来处理。

// Parser.java
public Factor parseDxFactor() {
    lexer.next();
    // 处理表达式
    return new DxFactor(expr);
}

计算化简

我们可以对ExprTermFactor均实现toDiffPoly()方法,以实现表达式求导。

Term对象是多个Factor相乘的结果,所以求导的时候,要采用链式求导的方式。

// Term.java
public Poly toDiffPoly() {
    Poly result = new Poly();
    for (Factor f1 : factors) {
        // 对f1求导
        for (Factor f2 : factors) {
            // f2原式与f1相乘
            // 注意需要判断f1与f2是否相等
        }
        // 把结果加到result里
    }
    // ……
    return result;
}

对于转化得到的PolyBasicUnit对象,我们都需要分别实现求导方法。

我们知道BasicUnit的形式可以表示为:

$$
F(x) = ax^be^{g(x)}
$$

 

那么对BasicUnit求导可以得到一个Poly

$$
F^{'}(x) = a[bx^{b-1}e^{g(x)} + g^{'}(x)x^be^{g(x)}]
$$

 

Poly求导只需要对Poly所存的BasicUnit求导后相加即可。

// BasicUnit.java
public Poly diff() {
    // 按照上面的公式计算
    return result;
}

// Poly.java
public Poly diff() {
    Poly result = new Poly();
    for (BasicUnit unit : basicUnits) {
        Poly aft = unit.diff();
        result = result.addPoly(aft);
    }
    return result;
}

Bug分析

中测、强测、互测均无出现bug,hack阶段又遇到了全0房。

强测的性能分还是没有拿满。我也不知道怎么像优化仙人一样优化了

What if?

假设下一次作业需要迭代,比如说添加多变量因子xyz,那么应该怎么做呢?

首先,整个BasicUnit要改为

$$
F(x) = ax^by^cz^de^{g(x)}
$$

 

的形式。

对于BasicUnit之间加法与乘法的实现没有太大的难度,在求导上实现会稍微麻烦一些。

对于PowFactor,需要捕捉每一项因子的变量名是xyz当中的哪一个。

public class PowFactor implements Factor {
    private char var;
    private BigInteger index;

    public PowFactor(char var, BigInteger index) {
        this.var = var;
        this.index = index;
    }

    @Override
    public Poly toPoly() {
        // 把var和index传入构建BasicUnit和Poly
    }
}

总体来看,需要迭代的部分不是很多。

规模与复杂度分析

最终代码的规模如下:

Source FileTotal LinesSource Code Lines
BasicUnit.java206185
DxFactor.java1613
ExpFactor.java3127
Expr.java3428
Factor.java54
FuncDefiner.java7871
FuncFactor.java2117
IOhandle.java2218
Lexer.java147137
MainClass.java1515
NumFactor.java2117
Parser.java151141
Poly.java200185
PowFactor.java2521
Pretreat.java2319
Simplify.java1916
Term.java5649
Token.java2117
Total:1091980

最终代码的复杂度如下。

classOCavgOCmaxWMC
Token.Type  0.0
DxFactor1.01.03.0
FuncFactor1.01.03.0
IOhandle1.01.03.0
NumFactor1.01.03.0
PowFactor1.01.03.0
Pretreat1.01.04.0
Simplify1.01.03.0
Token1.01.03.0
Expr1.22.06.0
MainClass2.02.02.0
ExpFactor1.66666666666666673.05.0
Term2.05.012.0
FuncDefiner2.86.014.0
Parser3.44444444444444467.031.0
Poly3.42857142857142847.048.0
BasicUnit2.010.040.0
Lexer3.777777777777777712.034.0
Total  217.0
Average2.23711340206185573.647058823529411712.055555555555555

复杂度比较高的方法如下:

methodCogCev(G)iv(G)v(G)
Parser.switchComma(String)19.01.06.010.0
BasicUnit.toString()16.03.09.012.0
Lexer.Lexer(String)14.012.012.015.0
Poly.isSame(Poly)11.04.07.09.0
Parser.parseFactor()9.07.07.07.0
Poly.isFactor()9.06.08.09.0
Lexer.getSign(char)7.02.02.06.0
Parser.parseExpr()7.01.07.07.0
Term.toDiffPoly()7.04.04.05.0
Lexer.getNumber()6.03.03.06.0
Poly.searchPositive()6.04.04.04.0

CogC 是圈复杂度的意思,与下面的v(G)一样。 v(G) 是圈复杂度,表示程序中独立路径的数量,也就是测试程序所需的最少路径条数。圈复杂度越大,说明程序越复杂,越容易出错,越难测试和维护。 iv(G) 是模块设计复杂度,表示程序中结构化程度的高低。模块设计复杂度越大,说明程序越不符合结构化设计原则,越难理解和修改。 ev(G) 是基本复杂度,表示程序中非结构化成分的多少。非结构化成分指的是那些不遵循顺序、选择、循环三种基本控制结构的语句或语句块。基本复杂度越大,说明程序越不规范,越降低了代码质量和可读性。

因为三次迭代都没有进行过重构,所以上面许多方法的基本复杂度还是挺糟糕的,这也说明了我的代码质量与风格确实有比较大的问题。比如说Lexer.Lexer(String)Poly.isSame(Poly)存在许多的特判与递归调用等,可以对此进行一些改进。

互测策略

首先先拿评测机进行无差别攻击压力测试,如果发现错误了,该怎么做呢?

我们可以将评测机生成的用例进行拆分,分别进行测试。

找到错误后开始读源码,在源码定位到错误之后,就可以构造符合要求的hack数据了!

总结与感想

这次作业让我深刻体会到了良好架构的重要性,由于前期设计的比较完善,后面的几次迭代任务都可以很清楚且简单的完成。当然,有些方法的耦合度和复杂度都还比较高,这说明我的面向对象设计的能力还存在一些不足。

在写代码之外,我还明白了一些道理,比如说一定要放手去做,多去实践才能发现到最优解。

...全文
77 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文研究了一种基于遗传算法的新型异构分布式系统任务调度算法,旨在解决异构计算环境中任务分配与调度的复杂优化问题。通过Matlab代码实现该算法,充分利用遗传算法强大的全局搜索能力和鲁棒性,对任务执行时间、资源利用率、系统负载均衡等关键性能指标进行综合优化,有效提升分布式系统的整体运行效率与稳定性。研究详细阐述了算法的整体架构设计、染色体编码策略、适应度函数构造、选择机制以及交叉与变异等遗传操作的实现细节,并通过大量仿真实验验证了所提出算法相较于传统调度方法在收敛速度、解的质量和调度性能方面的显著优越性。; 适合人群:具备一定编程基础和优化算法理论知识,从事分布式计算、高性能计算、云计算资源调度或智能优化算法研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于高性能计算、云计算、边缘计算等异构计算平台中的任务调度优化,提升资源利用效率;②为研究人员提供遗传算法在复杂组合优化问题中应用的完整实现案例,深化对智能优化算法设计原理与仿真实践的理解; 阅读建议:建议读者结合提供的Matlab代码深入研读,重点理解适应度函数的设计逻辑与遗传算子的参数调优策略,并可通过更换不同的任务集和系统模型来测试算法的泛化能力与鲁棒性。
参考李林凤等(2025)一文关于农村劳动力人均受教育年限指标的构建与计算方法,整理了中国31个省份总体、分性别的农村人均受教育年限数据,具体计算方法如下: 农村劳动力人均受教育年限 = (农村未上过学人数 × 1+小学学历人数 × 6+初中学历人数 × 9+高中和中专学历人数 ×12+大专及以上学历人数 × 16)/农村6岁及以上总人口 相关数据:各地区、分性别人均受教育年限数据 一、数据介绍 数据名称:中国各省农村人均受教育年限 数据范围:全国31个省份 时间范围:2006-2024年 样本数量:590条 数据来源:《中国人口和就业统计年鉴》、《中国劳动统计年鉴》 二、数据指标 年份 省份 省份代码 农村人均受教育年限 男性-农村人均受教育年限 女性-农村人均受教育年限 6岁及以上人口 6岁及以上人口_男 6岁及以上人口_女 未上过学人口 未上过学人口_男 未上过学人口_女 小学人口 小学人口_男 小学人口_女 初中人口 初中人口_男 初中人口_女 高中人口 高中人口_男 高中人口_女 大专及以上人口 大专及以上人口_男 大专及以上人口_女 三、参考文献 [1]李林凤,刘杨,杨亦民.种业创新驱动农村产业融合的作用机制与空间分异效应[J].广东财经大学学报,2025,40(6):97-109. [2]徐小阳,李洁,金丽馥.普惠金融对农村教育贫困的纾解效应[J].中国农村经济,2020,(9):41-64.

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧