ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

BUAA_OO2022_第一单元总结

2022-03-26 15:31:06  阅读:134  来源: 互联网

标签:化简 函数 递归 BUAA 因子 表达式 OO2022 方法 单元


作业概述

Homework 1 :通过对表达式结构进行建模,完成单变量多项式的括号展开,初步体会层次化设计的思想。

Homework 2 :完成多项式的括号展开与三角函数、自定义函数、求和函数调用、化简,进一步体会层次化设计的思想。

Homework 3 :完成多层嵌套表达式和函数调用的括号展开与化简,进一步体会层次化设计的思想。

架构设计

  1. 类图

     

  2. 设计思路

    预处理:去除空字符;“**”替换为"^";利用SumFunc类、SelfDefFuncs类对表达式中求和函数与自定义函数进行替换;
    解析:Lexer类与Parser类将表达式进行因子层面的递归解析。
    化简:Polynomial类与BodyMap类将各种因子统一存储、合并化简。

    • SelfDefFunc类
      (1)保存一个自定义函数的函数名、定义参数表、函数表达式;
      (2)call()方法读入调用参数表,顺序遍历一次函数表达式并将其中的参数替换成调用参数。要点:勤加括号防止替换后语义出错;替换造成表达式长度变化时,指针位置要相应改变。

    • SelfDefFuncs类
           (1)解析自定义函数定义,保存在SelfDefFunc类对象数组中;
           (2)displaceSelfDefFuncs()方法解析函数调用中的函数名和参数,递归地替换表达式中的自定义函数:如 f(1, g(x), x) 的情况,从最内层的函数开始替换。

    • SumFunc类
          同样是对字符串进行操作,将求和表达式中的"i"替换为数字,并构建求和式。要点:求和下限大于求和上限、或求和表达式中不含迭代变量时不需要进行替换;勤加括号。

    • Lexer类
          提取字符串中的语义要素,通过peek()交给Parser类,将对字符串的解析从单个字符级别抽象到语义要素级别,语义要素包括:(1)整数; (2)运算符 +、-、*、^; (3)字母; (4)三角函数函数体。三角函数函数体虽然也可以拆分为前三种要素,但笔者为了方便三角函数的化简将其独立为一种要素,暂且按下不表。

    • Parser类
       采用递归解析的思路,Expression是多个Term的求和,Term是多个Factor的连乘,Factor包括幂函数、带符号整数、表达式、三角函数。
          关于正负号:parseExpr()方法和parseTerm()方法不处理正负号,parseFactor时,位于Factor开头的+号将被忽略,位于Factor开头的 - 号会引起一次parseFactor()的递归调用,返回一个取反后的Factor。
          关于指数:表达式后大于1的指数会被展开,得到一个 (表达式) * (表达式) * (表达式) * ... 的Term,非表达式因子后的指数被对应因子的类对象保存。
          关于三角函数:获得它的函数体并视为不需要预处理的待化简表达式,传递给Lexer类开始递归化简,得到一个最简的函数体字符串交给TriangleFunc类对象。例如 1+cos(x+x+x)+1 ,先解析、化简 x+x+x ,得到3*x,再解析化简1+cos((3*x))+1。

    • Factor接口
          有toPolynomial()方法,将因子统一转化为便于计算合并的多项式。
     
    • Expr类
          使用HashSet存储多个Term,toPolynomial()调用每个Term的toPolynomial()方法,并将结果进行多项式相加。

    • Term类
           使用HashSet存储多个Factor,toPolynomial()调用每个Factor的toPolynomial()方法,并将结果进行多项式相乘。

    • Number类
          带符号整数。除了三角函数的函数体,所有因子中的数字均用BigInteger类储存。

    • PowerFunc类
          储存x的指数index。toPolynomial()方法中,需要判断 index == 0 的情况。

    • TriangleFunc类
          储存一个三角函数的类型(sin或cos),函数体,指数。在构造方法中,会对递归化简后的函数体字符串作进一步处理:若函数体为非表达式因子,它的正负号将被提出;若函数体为表达式因子,将被额外加一层括号;sin(0)与cos(0)在这一步也会被化简为0与1。isExpr()方法通过正则表达式匹配判断函数体是否为表达式因子。

    • BodyMap类
          用HashMap<非表达式因子,指数>储存多项式中的一个项的非系数部分,定义了项与项的乘运算,乘法中对因子相同的键值对进行合并。

    • Polynomial类
          用HashMap<BodyMap,系数>储存多项式(需要重写BodyMap的equals(),hashCode()),定义了多项式之间的加运算,加法中对非系数部分相同的项进行合并。toString()方法将多项式转化为字符串,进行以下输出长度优化:
          (1)指数为0时该因子为1;
          (2)省略指数1;
          (3)系数为0时省略该项,除非输出为空串;
          (4)项存在非系数部分时省略系数1;
          (5)用x*x替换x**2,单独作为三角函数函数体时除外;
          (6)若存在符号为 + 的项,保证输出的开头符号被省略。

  3. 架构设计体验

           得益于助教提供的第一单元训练代码,我在homework1中就使用了lexer、parser双层解析,表达式、项、因子递归解析的架构,并以此为基础加入了预处理模块和化简模块,有了一个好的开始,因此在后续的homework中也没有进行重构。
    从hw1到hw2的迭代:
          (1)项中不再只有一个因子,(系数*幂函数)模型不再能描述所有的项,出现了3*x*cos(x**2)之类的项,于是增加BodyMap类存储项,修改Polynomial类,使用双层嵌套HashMap存储多项式;
          (2)自定义函数、求和函数:增加SelfDefFunc类、SelfDefFuncs类、SumFunc类对表达式进行预处理,使表达式符合homework1的输入标准;
          (3)增加TriangleFunc类,与最终架构相比差别很小。
    从hw2到hw3的迭代:
          (1)由于出现自定义函数的嵌套调用,需要调整SelfDefFuncs.displaceSelfDefFuncs(),使其实现递归;
          (2)三角函数的函数体先递归化简再储存,修改Parser类将函数体传递给Lexer类,修改TriangleFunc类的构造函数,对函数体化简结果做进一步处理。

  4. 设计优劣

          (1)分设SelfDefFunc类和SelfDefFuncs类多此一举。最终实现自定义函数替换的方法在SelfDefFuncs类,但该方法所需要的信息(函数名、参数表、函数表达式)却存储在SelfDefFunc类中,造成了类之间不必要的信息传递,可将SelfDefFunc类改为内部类。
       (2)三角函数的函数体没有用结构化的方法储存,而是用字符串储存,若要进行平方和化简和二倍角化简,需要对字符串作分类讨论。
          (3)对负号的处理中,使用了递归解析,因此在parseExpr()、parseTerm()、parseFactor()中增加了参数boolean neg表示当前因子是否需要取反,增加了函数参数个数和空间开销。改进方法:每类因子写一个将自身取反的方法。
          (4)优点是模块间的耦合度比较低、程序拓展比较方便,并且递归调用较多,提高了代码的复用率。

代码分析

       方法复杂度

    

 

 

         

  圈复杂度较高和设计复杂度较高的方法均是Polynomial.toString()、SumFunc.displaceSumFuncs()、Parser.parseFactor(),优化方案如下:

  • Polynomial.toString() 中需要对输出长度进行控制,使用了非常多的条件语句,其中有不少可以合并,也可以用更加简洁的正则表达式完成,笔者在做第三次作业时才学会了正则表达式,但没有对前两次作业的代码进行优化。同时对象间调用很频繁,耦合度高,这可能是因为使用了BodyMap和Polynomial两个类嵌套存储多项式,获取因子的类型和指数时需要连续访问两个类,可以将BodyMap设为Polynomial的内部类。
  • SumFunc.displaceSumFuncs()中对字符串进行了多次遍历,第一次是寻找位于求和表达式两端的“( ”、“ )”,从而定位求和表达式;第二次是遍历求和表达式,判断是否存在迭代变量;第三次是替换求和表达式中的迭代变量,这三次遍历操作是低耦合的,应该将大方法拆分为三个小方法,降低复杂度的同时也降低代码理解难度。
  • Parser.parseFactor()的问题也是处理的任务太多,正负号处理应该由各因子类的取反方法完成而不需要递归调用parseFactor();获取各类因子的指数的代码在方法中出现了多次,应该封装为函数。

 

 

        方法的非注释代码行数(NCLOC)
    
      

 

 

         

 

测试

  1. 互测策略

    出于节省时间的考虑,笔者没有针对互测者的代码构造数据,而是根据笔者认为的难点、易错点、临界点构造数据。第一次互测比较失败,直接在测评机提交数据,没有进行本地测试,战绩8刀0中;后两次互测改进了测试思路,先本地对所有互测者进行充分测试,此方法相当有效,战绩分别为4刀1中,3刀5中。

  2. BUG分析

    • 第一次作业无BUG
    • 第二次作业3个BUG
      (1)x**10 >> x0 ,判断指数为1的情况出错,所属方法Polynomial.toString()
      (2)sin(-1)**2 >> -sin(1)**2 ,提取 - 号时没有考虑三角函数的幂次,所属方法TriangleFunc.toPolynomial()
      (3)sin(0)**0 >> 0 ,当时以为这是未定义的情况,过于相信自己的经验,所属方法TriangleFunc.toPolynomial()
    • 第三次作业1个BUG
      (1)sum(i,1,2,i**2) >> 1 ,混淆了字符串变量,对错误的变量进行操作 ,所属方法SumFunc.displaceSumFuncs()
    • 复杂度较高的三个方法中,有两个产生了BUG,说明笔者在编写较长、较复杂的方法时 很容易疲劳,进而忽略一些细节


心得体会

  
作业用时

   • hw1: 设计+编写——3~4天,每天约10小时,调试——半天;

    • hw2: 设计+编写——小于10小时,调试——约3小时;     

      • hw3: 设计+编写——小于5小时,调试——约2小时;


    关于迭代

     
hw2与hw3所花时间大大减少,原因除了hw1是从无到有的开发、当时对java语言不熟悉外,还有对代码可扩展性的重视:
    标识符不仅限于题目要求的 f、g、h、i;解析表达式时采取递归下降策略;数值类型采用BigInteger而非Int等等……在设计时留有余地,就能在迭代时带来方便。


      关于调试

     第一单元中由于对Idea还不太熟悉,主要利用断点和System.out.println()输出语句进行debug,接下来的学习中要结合对变量的跟踪监视来debug。

 

    其他收获

     学会了正则表达式的常见写法,如断言、零宽匹配、捕获组等,处理字符串时可以和冗长的if else语句说再见了。


    

标签:化简,函数,递归,BUAA,因子,表达式,OO2022,方法,单元
来源: https://www.cnblogs.com/soraomezasu/p/16055817.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有