OO第一单元总结

程序结构

分析

Complexity Metrics(复杂度分析)

在这里插入图片描述

在这里插入图片描述

Dependency Metrics(依赖度分析)

在这里插入图片描述

类图

在这里插入图片描述

  • Pre:将输入的已去掉空白字符的表达式中的所有自定义函数替换
  • Lexer:词法分析,并将+/-(符号)转化成1 * / (-1) *,避免符号和运算符混乱
  • Parser:语法分析,构建表达式树
  • Expr,Term,Ex,Factor(interface):递归下降层次,均含toString()方法
    • Expr:任何符合形式化表述的结构
    • Term:外层不含+/-(运算符)Expr
    • Ex:外层不含*Term
    • Factor:不含^/exp()Ex
      • Unknown:变量
      • Number:常数
      • Derive:求导因子
  • Cal:单项式类
  • Polya:多项式类,含所有计算方法以及toString()方法

优点: 采用递归下降,可扩展性强,实现Factor接口,统一管理,将预处理、解析与计算完全分离,实现高内聚低耦合,遵循单一职责原则
缺点: 单项式的设计上仍然没有实现抽象化层次,违背开闭原则,若要实现sin(),lg()则改动较大。


架构设计

hw_1

在这里插入图片描述

  • 递归下降层次:Expr -> Term -> Power -> Factor(interface)
    • Power处理以^为运算的结构,为二元运算,故多项式类要存在对应“乘方”运算
      public Poly pow(Poly other) {}
      
    • Factor接口只有Unknown(变量),Number(常数)
    • 每个类中均有toString()方法,返回后缀表达式
  • Poly类中有一个HashMapkey为变量的指数,value为系数,即结果均可写成{value*x^key}的形式。定义加、减、乘、乘方方法,实现计算功能
    • 根据后缀表达式,将基项(Unknown,Number)实例化Poly类,再一一计算

      public Poly(String base) {
          this.polys = new HashMap<>();
          if (base.equals("x")) {
              this.polys.put(BigInteger.ONE, BigInteger.ONE);
              this.maxIndex = BigInteger.ONE;
          } else if (base.equals("")) {
              this.maxIndex = new BigInteger("-1");
          } else if (new BigInteger(base).toString().equals("0")) {
              this.maxIndex = new BigInteger("-1");
          } else {
              this.polys.put(BigInteger.ZERO, new BigInteger(base));
              this.maxIndex = BigInteger.ZERO;
          }
      }
      
    • toString()方法,用于将最终计算结果转化为字符串输出

hw_2

在这里插入图片描述

  • 首先引入了自定义函数,我采用了单独建一个预处理Pre
    • 先用=分隔自定义函数表达式,再用正则表达式获取形参,并记录形参的顺序
      String[] postF = fx.split("=");
      if (postF.length == 2) {
          this.fx = postF[1];
      } else {
          this.fx = "";
      }
      String patternTerm = "f\\((.+)\\)";
      Pattern re = Pattern.compile(patternTerm);
      Matcher matcher = re.matcher(postF[0]);
      while (matcher.find()) {
          fp = matcher.group(1).split(",");
      }
      this.numF = fp.length;
      for (int i = 0; i <= this.numF - 1; i++) {
          mapF.put(fp[i], i);
      }
      
    • major()主执行函数遍历表达式,依次拓印字符,若检测到f g h,则截取配对的f/g/h(*)字符串作为参数调用f/g/h()函数,该函数保证传入的f/g/h(*)调用自定义函数形式的字符串会返回完全不含自定义函数的替换字符串
      public String major(String input) {
          StringBuilder str = new StringBuilder();
          int pos = 0;
          while (pos < input.length()) {
              if (input.charAt(pos) == 'f' || input.charAt(pos) == 'g' || input.charAt(pos) == 'h') {
                  pos = pos + 2;
                  int tmp = pos;
                  int left = 0;
                  while (left != -1) {    //配对括号
                      if (input.charAt(pos) == '(') {
                          left++;
                      } else if (input.charAt(pos) == ')') {
                          left--;
                      }
                      pos++;
                  }
                  int end = pos;
                  pos = tmp;
                  str.append("(");
                  if (input.charAt(pos - 2) == 'f') {
                      str.append(f(input.substring(pos - 2, end)));
                  } else if (input.charAt(pos - 2) == 'g') {
                      str.append(g(input.substring(pos - 2, end)));
                  } else {
                      str.append(h(input.substring(pos - 2, end)));
                  }
                  str.append(")");
                  pos = end - 1;
              } else {
                  str.append(input.charAt(pos));
              }
              pos++;
          }
          return str.toString();
      }
      
    • 接下来就是f/g/h()函数:为了保证加传入的字符串完全去除自定义函数,故采用递归调用major()函数的方法,替换时,将实参传入major()的结果去替换,直至实参不再含有自定义函数
      if ((c.equals("x") && (at == 0 || at == fx.length() - 1 ||
              fx.charAt(at - 1) != 'e')) || (c.equals("y")) || (c.equals("z"))) {
          str.append("(");
          str.append(major(paras.get(mapF.get(c))));
          str.append(")");
      }
      
    这样就不需要将x y z替换成特殊字符,避免一些bug
  • Power类替换为Ex类,同时处理^,exp(),因为它们都属于指数范畴,运算的优先级相同,故集中处理
    • 存放两个容器,分别存ExFactor,属性mode标记三种情况,用于确定后缀表达式的格式:
      0 : Unknown / Number / (Expr)  ——  本身
      1 : a ^ b                      ——  a b ^
      2 : exp( c )                   ——  c e
      
      以及一些形式分类:
      1. exp(a)^b
      2. exp(c)
      3. x^d
      4. ()^e
      5. Unknown / Number / (Expr)
      
    • Parser解析Ex时,通过括号配对以及正则表达式确定是哪个分类,并对mode赋值,只要是形式1 2,就反复调用parserEx(),否则调用parserFactor()退出
      注意若是形式1,则注意做标记,下次不再判定为1,避免死循环
  • 修改多项式类,增加单项式类。由于第一次作业较为简单,我没有设置单项式类,本次增设。多项式类Polya、单项式类Cal。注意到结果均可表示成{a * x^b * exp(c)}
    • 先定义单项式(Cal):x^b * exp(c)
      private Polya ex;   //c
      private BigInteger index;   //b
      
    • 再定义多项式(Polya):<Cal,a>
      private final TreeMap<Cal, BigInteger> polys;
      
    • 实现各种运算(在计算过程中逢0即删是个好习惯),新增exp()
      public Polya exp() {
          if (this.polys.isEmpty()) {
              Cal cal = new Cal("3");
              Polya polya = new Polya();
              polya.addCal(cal, new BigInteger("1"));
              return polya;
          }
          Polya polya = new Polya();
          Cal cal = new Cal("3");
          cal.setEx(this);
          cal.getEx().string = cal.getEx().toString();
          polya.polys.put(cal, new BigInteger("1"));
          return polya;
      }
      
    • 为实现Cal作为键值的比较,还要重写compareTo()方法
      public class Cal implements Comparable<Cal> {
          //省略
          @Override
          public int compareTo(Cal o) {
              if (this == null && o == null) {
                  return 0;
              } else if (this == null) {
                  return -1;
              } else if (o == null) {
                  return 1;
              } else {
                  if (this.index.compareTo(o.index) != 0) {
                      return this.index.compareTo(o.index);
                  } else {
                      return this.ex.getString().compareTo(o.ex.getString());
                  }
              }
          }
      }
      
      这里的getString()原本用的toString(),但由于每次都要调用toString(),时间成本很高,故在Polya中添加字符串属性,记录其上一次toString结果,并在任何发生修改的地方更新该属性,用getString()方法获取其值
      而使用TreeMap可以自动排序,保证返回字符串的唯一性
      private String string;
      
    • 深克隆:
      public Polya clone() {
          Polya polya = new Polya();
          for (Cal cal : this.polys.keySet()) {
              polya.polys.put(cal, this.polys.get(cal));
          }
          return polya;
      }
      

hw_3

(类图同最终类图)

  • 自定义函数定义的问题,我只需要先对自定义函数定义表达式按输入顺序依次进行替换处理即可
    ArrayList<String> queue   //记录顺序
    
    if (!queue.isEmpty()) {
        if (queue.get(0).equals("f")) {
            fPre(fx);
        } else if (queue.get(0).equals("g")) {
            //省略
        }
    }
    if (queue.size() >= 2) {
        if (queue.get(1).equals("f")) {
            fPre(fx);
            this.fx = major(this.fx);
        }
        //省略
    }
    if (queue.size() >= 3) {
        if (queue.get(2).equals("f")) {
            fPre(fx);
            this.fx = major(this.fx);
        }
        //省略
    }
    
    由于有序,后面自定义函数调用major()时,前面的自定义函数已经填置完毕
    fPre(),gPre(),hPre()均为hw_2中的初始化内容拆分,没有变化
  • 新增求导因子:
    • 我将其继承到Factor接口,词法解析curToken"d":
      if (lexer.pop().equals("d")) {
          lexer.next();
          Factor derive = new Derive(parseExpr());
          lexer.next();
          return derive;
      }
      
    • Polya新增求导算法:
      public Polya derive() {
          Polya polya = new Polya();
          if (this.polys.isEmpty()) {
              return new Polya();
          } else if (this.polys.size() == 1) {
              for (Cal cal0 : this.polys.keySet()) {
                  Polya polya0 = new Polya();//系数
                  //省略
                  Polya polya1 = new Polya();//对x^n求导项
                  if (cal0.getIndex().compareTo(BigInteger.ONE) >= 0) {
                      //省略
                  }
                  Polya polya2 = new Polya();//对exp(a)求导项
                  if (!cal0.getEx().polys.isEmpty()) {
                      //省略
                  }
                  return polya1.add(polya2).mul(polya0);
              }
          } else {
              for (Cal cal : polys.keySet()) {
                  Polya polya0 = new Polya();
                  polya0.polys.put(cal, this.polys.get(cal));
                  polya0.string = polya0.toString();
                  polya = polya.add(polya0.derive());
              }
              return polya;
          }
          return new Polya();
      }
      

新迭代

变量不再只有x,增加y z

  • Pre完全不需改动,仍可完成自定义函数替换
  • Lexer对解析变量部分稍加改动
    if (c == 'x' || c == 'y' || c == 'z') {
        pos++;
        curToken = String.valueOf(c);
        ifOp = true;
    }
    
  • Unknown原本就有name属性以及构造方法,不用改变
  • Parser所有原来分析x的地方,都或上y z即可,如:
    if (lexer.pop().equals("x")||lexer.pop().equals("y")||lexer.pop().equals("z")) {
        lexer.next();
        return new Unknown(lexer.pop());
    }   //ParseFactor
    
  • 计算类Polya,Cal:
    • Cal新增属性,指数分为x y z的指数:
      private Polya ex;
      private BigInteger xIndex;
      private BigInteger yIndex;
      private BigInteger zIndex;
      
      compareTo随之做适当修改
    • Polya对运算方法进行微调:
      • 加、减、乘、乘方、e指较为简单
      • 求导运算则再多出两个求导项(y z)相加即可

bug分析

hw_2强测&互测

仅有一处bug:对自定义函数表达式未做去空白符处理,公测时是做了处理的,但在改bug时误删了没有发现,样例中也没有这种情况,导致强测才发现。还是测试时捏测试用例不够耐心,没有覆盖很多情况。

hw_3互测

仅有一处bug:词法解析时没有处理前导零

return new BigInteger(str.toString()).toString();

还是跟上面一样,测试不够严密。

hack策略

  • 阅读代码分析时间性能,针对性构造容易导致对方运行时间过长的样例
    • 例如自定义函数替换方法,计算时删零,多层exp()嵌套,过于频繁调用某种方法
  • 分析对方优化策略,观察是否会引发bug(过度化简)
    • 最典型的exp(-1)exp((-x))的区别
  • 看对方代码哪里写的很混乱就着重hack这里
  • 搭建评测机,找到bug后逐渐缩短表达式,找到“病根”

结果优化

  • 最基本的优化:系数为±1且不为常数的项,将1省略;指数为1,将^1省略;正系数项提前;第一项若为正不加+
  • 此外,我只做了exp()里括号的减省:
    • 先取出exp()内部的多项式转化成字符串
      String s = cal.getEx().getString();
      
    • 正则表达式
      String patternTerm1 = "^(-?)(\\d*)\\*x(\\^\\d+)?$"; //matcher1
      String patternTerm2 = "^(-?)(\\d*)\\*(exp\\(.+\\))(\\^\\d+)?$"; //matcher2
      
    • 严判标准
      String patternTerm = "^-\\d+$";//负常数,不用加括号
      通过括号匹配找外层运算符:
      "^-\\d+$"   -> return 0;
      +/-         -> return 1;
      *           -> keep 2;  //上面两个到最后也没有再返回2
      default     -> return 0;
      
      判定顺序:
      1、严判标准若为1,则保留括号
      2、matcher1.find()matcher2.find():系数绝对值取出拿到exp()外面做指数,系数若正,就省略括号,否则保留括号
      3、严判标准若为2,则保留括号
      4、其余情况均省略括号

心得体会

这几次作业我体会到了良好架构的重要性,可扩展性。通过作业以及研讨课对SOLID原则有了更深刻的认知。掌握了递归下降法。掌握了当Map容器键值是类对象时,如何重写compareTo()方法以实现键值比较。
此外,当面临需要重构或没有思路时,一定要敢于发散地去想,敢于试错,不存在完美的架构,只存在适合自己的架构,自己摸索出来的才是自己的,不要因为别人的架构好就盲目修改,更不要还没自己构建无脑效仿别人,这就失去了这门课的意义。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值