• BUAA-OO 前三次作业“表达式求导" 总结与思考


    写在前面

      OO作业已经做了三周,有种“一周蜕一层皮”的感觉,还是挺酸爽的。之前虎头蛇尾的博客经历告诉我,及时发博文总结反思是必要的,很开心能在博客园建一个专属的技术博客。希望这些拙劣的碎碎念可以给自己也给大家提供帮助和启迪,也期待和大家共同交流!

      PS. 讲一个悲伤的故事,这是这个博文的第三稿...前两稿都被博客园和typora无情吞掉了,于是在默写第三遍之后有些话就省去了...希望以后的博客作业能被电脑温柔对待;(

    一、需求分析

    用JAVA实现多项式的合法性判断和求导运算。

    1)多项式是带常数的幂函数之和

    2)多项式是带常数的幂函数和基本三角函数的乘积项之和

    3)在(2)的基础上,三角函数的自变量可以进行嵌套

    二、思路分析

    1、基于度量的程序结构分析

    类关系图(利用UML Support插件)

    第一次

    第二次

    第三次

    代码行数统计(利用Statistic插件)

    第一次

    第二次

     

    第三次

    代码设计复杂度(利用MetricsReloaded插件)

    ev(G)基本复杂度,用来衡量程序非结构化程度
    iv(G)模块设计复杂度,用来衡量模块判定结构
    v(G)独立路径条数

    第一次

    第二次

    第三次

    2、BUG分析

    第一次:

    强测中得分 100

    互测:由于被 hack 扣分 1 分。这其中包括了 13 个错误,同时hack 他人成功 8 次,得分 18.6 分。

    自己错误:1)未判断其他空白字符的不合法性 2)处理第一项前面的符号时,未考虑空白字符 的情况

    他人错误:正则表达式疏漏,特别是对符号和数字间的空格的特殊处理。例如 -   -   5

    第二次:

    在强测中得分 93.8423

    未被hack。hack 他人成功 7 次,得分 0.625 分。

    他人错误:正则表达式疏漏,特别是三角函数特殊位置空白字符的判断。例如cos ( x ),      -+x

    第三次:

    在强测中得分 84.7059

    由于被 hack 扣分 2 分。这其中包括了 3 个错误。同时, hack 他人成功 25 次,得分 16.75 分

    自己错误:1)某一处合并同类项写挂了,导致求导结果不对 2)乘积项输出时,若为空串应直接return,否则会执行下一条charAt(0)语句导致访问越界。例如:+-sin ((x-x) ) ^ +07

    他人错误:同前两次。随机数据都能一串三,也警醒我们自测的重要性。

     

    三、知识技能总结

    1、正则表达式

    (1)正则表达式的组命名:

    //正则表达式的组命名
            String sin = "(sin\((?<quad>.*)\)(\^" + num + ")?)";
            String cos = "(cos\((?<quad>.*)\)(\^" + num + ")?)";
    //调用
     r = Pattern.compile(sin);
     m = r.matcher(str);
     if (m.matches()) return isFactor(m.group("quad"));     

    (2)正则表达式的回溯

    java的正则表达式,相当于把我们在C语言中用栈处理表达式运算符的过程封装起来,所以在匹配过程中要考虑到爆栈问题。正则表达式量词匹配模式区别就在于回溯(弹栈)方式的不同。具体如下:

     1     private String matchProduct(String str) {
     2         String num = "[+-]?\d+";
     3         String pow = "x(\^" + num + ")?";
     4         String sin = "sin\(x\)(\^" + num + ")?";
     5         String cos = "cos\(x\)(\^" + num + ")?";
     6         String factor =
     7                 "(" + num + ")|(" + pow + ")|(" + sin + ")|(" + cos + ")";
     8         String product =
     9                 "^[+-]{1,2}(\d+\*)?((" + factor + ")\*)*+(" + factor + ")";
    10         Pattern r = Pattern.compile(product);
    11         Matcher m = r.matcher(str);
    12         if (m.find()) {
    13             return m.group();
    14         } else {
    15             return "";
    16         }
    17     }
    我觉得比较好的正则匹配写法,用到了占有型匹配

    2、Hashmap

     • 第一次作业,直接用指数作为Key即可

     • 第二次作业,需要使用(a,b,c)三元对作为自定义Key

     1 import java.util.Arrays;
     2 public class Tuple {
     3     private final int x;
     4     private final int y;
     5     private final int z;
     6     public Tuple(int x, int y, int z) {
     7         this.x = x;
     8         this.y = y;
     9         this.z = z;
    10     }
    11     public int getX() {
    12         return x;
    13     }
    14     public int getY() {
    15         return y;
    16     }
    17     public int getZ() {
    18         return z;
    19     }
    20 
    21     @Override
    22     public boolean equals(Object obj) {
    23         if (obj == this) {
    24             return true;
    25         } else if (obj instanceof Tuple) {
    26             return (((Tuple) obj).x == this.x)
    27                 && (((Tuple) obj).y == this.y)
    28                 && (((Tuple) obj).z == this.z);
    29         } else {
    30             return false;
    31         }
    32     }
    33 
    34     @Override 
    35     public int hashCode() {
    36         return Arrays.hashCode(new int[]{x, y, z});
    37     }
    38     @Override 
    39     public String toString() {
    40         return String.format("(%s, %s, %s)", x, y, x);
    41     }
    42 }
    43 /******************************************************/
    44 import java.util.HashMap;
    45 public class Main {
    46     private static final HashMap<Tuple, Integer> hashMap = new HashMap<>();
    47     private static void showHashMapValue(Tuple tuple) {
    48         if (hashMap.containsKey(tuple)) {
    49             System.out.println(String.format(
    50                 "Value of %s is : %s", tuple.toString(), hashMap.get(tuple)));
    51         } else {
    52             System.out.println(String.format(
    53                 "Value of %s not found!", tuple.toString()));
    54         }
    55     }
    56 
    57     public static void main(String[] args) {
    58         hashMap.put(new Tuple(1, 2, 3), 1);
    59         hashMap.put(new Tuple(1, 3, 4), 2);
    60         hashMap.put(new Tuple(2, 3, 5), 3);
    61         showHashMapValue(new Tuple(1, 2, 3));
    62         showHashMapValue(new Tuple(1, 3, 4));
    63         showHashMapValue(new Tuple(2, 3, 5));
    64         showHashMapValue(new Tuple(2, 4, 0));
    65     }
    66 }

    3、考虑合并同类项的设计思路

    4、对拍程序

    已上传至GitHub

    5、继承与接口

    首先明确一点:能用接口就不用继承。

    静态绑定与动态绑定

    静态绑定:在程序执行前已经被绑定,也就是说在编译过程中就已经明确一个对象类应该调用哪个方法,此时由编译器获取其他连接程序实现。

    动态绑定(多态):在程序运行根据具体对象的类型进行绑定,调用对应的方法。

    6、递归下降分析

     1 /*******************分解表达式************************/
     2 import java.util.ArrayList;
     3 public class ExpressionParser {
     4     private enum Status { // 表达式解析状态
     5         BEGIN, // 开始状态
     6         SIGN, // 符号状态
     7         TERM, // 项状态
     8         END, // 结束状态
     9     }
    10     
    11     private Status status; // 当前状态信息
    12     private ArrayList<ExpressionItem> items;
    13     
    14     private final String string;
    15     private int index;
    16     
    17     public ExpressionParser(String string, int index) {
    18         this.string = string;
    19         this.index = index;
    20         this.status = Status.BEGIN;
    21     }
    22     
    23     public Expression parse() {
    24     // 起初为开始状态
    25     // 如果读到了+/-,则进入符号状态,否则记录符号为+后进入项状态
    26 
    27     // 加减状态
    28     // 读取+/-,并记录,之后进入项状态
    29 
    30     // 项状态
    31     // 实例化一个新的TermParser,传入字符串以及当前位置
    32     // 用此TermParser来生成解析出一个Term,进行存储
    33     // 如果接下来还有+/-,则进入加减状态,否则进入结束状态
    34 
    35     // 结束状态
    36     // 整理获取到的值,并返回
    37     }
    38 }
    39 /**************分解乘积项***********************/
    40 import java.util.ArrayList;
    41 public class TermParser {
    42     private enum Status {
    43         BEGIN, // 开始状态
    44         SIGN, // 符号状态
    45         FACTOR, // 因子状态
    46         END, // 结束状态
    47     }
    48     private Status status;
    49     
    50     private ArrayList<Factor> factors;
    51     private final String string;
    52     private int index;
    53     
    54     public TermParser(String string, int index) {
    55         this.string = string;
    56         this.index = index;
    57         this.status = Status.BEGIN;
    58     }
    59     public Term parse() {
    60     // 起初为开始状态
    61     // 直接进入因子状态
    62 
    63     // 符号状态
    64     // 读取*后,进入因子状态
    65 
    66     // 因子状态
    67     // 因子状态下,实例化一个FactorParser,传入字符串和位置信息
    68     // 使用该FactorParser来解析一个因子,并进行存储
    69     // 如果下一个字符为*,则进入符号状态,否则进入结束状态
    70 
    71     // 结束状态
    72     // 整理获取到的值,并返回
    73     }
    74 }
    75 /***************分解因子*******************/
    76 public class FactorParser {
    77     private final String string;
    78     private int index;
    79     public FactorParser(String string, int index) {
    80         this.string = string;
    81         this.index = index;
    82     }
    83     public Factor parse() {
    84     // 解析一个因子
    85     
    86     // 如果开头是数字,则连续读取一串数字,实例化Constant对象,返回
    87     
    88     // 如果开头是x,则读取x,实例化XFunction对象,返回
    89     
    90     // 如果开头是s,则读取sin(x),实例化SinFunction对象,返回
    91     
    92     // 如果开头是c,则读取cos(x),实例化CosFunction对象,返回
    93     }
    94 }

    7、设计模式

    1)工厂模式

    2)单例模式

    3)装饰者模式

    (%cyx)

    以第三次作业为例:

    我们可以用Factor(因子)作为抽象父类

    以具体的ExprFactor、Sin、Cos等作为被装饰者

    用可以嵌套的Sin、Cos作为装饰者(注意到它们既是被装饰者也是装饰者)

    调用公有的方法——求导,实现对复合函数的求导

    四、自己的一点思考

    1、学习方法

    1)即用即学

      上学期报了java一般专业课,最后摸了一份大作业出来。但这学期正式开始用java编程时,却发现自己之前真正学到的只是偏GUI的一些语法,连正则表达式都还不会用。于是开学初想像学C那样通读语言书,却发现大多数java编程入门书晦涩生硬,也缺少有用的例子。现在再回过头来再看语法,便能理解得更加透彻了,因为在编程中摸爬滚打尝试过一些东西后,语言书变成了使用正确性的印证和内部机制的解释,而不是必须系统学习后才能开始编程的必要前提。因为不同环节侧重的理论知识不同,“即用即学”远比“在没真正使用一个东西的时候通过阅读来提前学习”效率要高。

    2)从算法竞赛看工程作业

      算法竞赛追求巧妙的思维、代码短小精悍易编写,遇到特殊的边界条件直接特判来莽,总之快速AC是王道。这种写程序的策略有点像在“贪心”。

      而工程思维可能更像是“动态规划”,追求“全局最优”,而非“当前最优”。“不行的话我就在这添一添,那里删一删”的想法在这时变得很危险,乱搞和特判的补丁只会让程序看起来极其恶心。设计上的全面考虑和可扩展性在此时显得更加有智慧。

      但是,这两者有共同的要求:

    1)同样注重代码实现能力,即在规定时间内完成特定功能的“硬实力”。写大作业就像长跑,配速低并不意味着我们可以在长距离长时限下允许思维怠惰。

    2)对bug“零”容忍。虽说“世界上不存在没有bug的程序”,但“只要出错就爆零”的紧迫感,如果迁移到工程上或许会敦促我们把作业完成得更好。

    2、调试方法

    1)边写边调试

    从HW1到HW3,我的强测成绩逐渐变形(笑容逐渐消失),其实用来构思的时间逐渐变长,写代码时间逐渐变长,也感到压力逐渐增大。

    这些年自己写过的代码量应该有5w+,却面对上千行、面向对象的代码debug时常感到崩溃。我认为做出按照层次来编写程序会有效缓解这一问题:

    输入处理(合法性检查)——>求导运算——>输出处理——>合并同类项——>输出优化(指数、系数、符号)——>进阶优化(三角函数)

    每个模块编程完成后,顺手debug,既能减少工作量,又能减轻心理压力。

    2)构造测试数据

    在编码前就尽可能充分考虑不同数据的处理方式,而不是在周二写完代码开始debug的时候才开始造数据。

    在编写代码前能够考虑得约细致,编写代码的过程就越顺利,debug的时间也会减少。

    但是,对于我们初学者来说,很多实现细节在缺乏足够的经验时确实很难提前考虑到,踩坑和磨练大概是必经之路。

    3)小黄鸭调试法

    大概这就是“肉眼debug”的精髓。在完成程序后先前前后后捋一遍,就可以大幅度减少在输出调试和单步调试中的许多脑残、手残错误。

    其实在思路卡壳不知道该怎么做的时候也可以尝试这个方法,跟身边的人讲一讲思路,就算他们无法给出可行的解决方案,自己也会对问题更加明晰。

    所以欢迎大家来和我唠嗑!说不定可以互相启迪> < ~

    毕竟哪个程序员都不想每天只对着二次元纸片人和小黄鸭自言自语:))

  • 相关阅读:
    YAML 语法小结
    小程序之脚本语言
    小程序WXML 使用小结
    微信小程序 js逻辑
    小程序开发1
    联想Y7000安装Ubuntu16.04/Win10双系统,wifi问题,显卡驱动和CUDA10安装
    VS2015中配置Eigen
    联想Y700安装显卡驱动和CUDA8.0
    php微信生成微信公众号二维码扫描进入公众号带参数
    Y7000 (1)安装ubuntu1604遇到的问题
  • 原文地址:https://www.cnblogs.com/Ryan0v0/p/10601429.html
Copyright © 2020-2023  润新知