• 软件工程个人项目作业——平面交点个数


    1. 在文章开头给出教学班级和可克隆的 Github 项目地址。(1')
    项目 内容
    这个作业属于 2020春季计算机学院软件工程(罗杰 任健)
    这个作业的要求是 个人项目作业
    我的教学班级 006
    本项目的GitHub地址 IntersectProj
    我对这项作业的目标是 提高个人程序开发素质,写出高性能程序

    PSP

    1. 在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
    2. 在你实现完程序之后,在下述 PSP 表格记录下你在程序的各个模块上实际花费的时间。(0.5')
    • PSP
    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划
    - Estimate - 估计这个任务需要多少时间 10 10
    Development 开发
    - Analysis - 需求分析 (包括学习新技术) 75 120
    - Design Spec - 生成设计文档 10 15
    - Design Review - 设计复审 (和同事审核设计文档) 5 10
    - Coding Standard - 代码规范 (为目前的开发制定合适的规范) 5 2
    - Design - 具体设计 30 30
    - Coding - 具体编码 60 180
    - Code Review - 代码复审 15 60
    - Test - 测试(自我测试,修改代码,提交修改) 45 240
    Reporting 报告
    - Test Report - 测试报告 10 10
    - Size Measurement - 计算工作量 5 5
    - Postmortem & Process Improvement Plan - 事后总结, 并提出过程改进计划 10 15
    合计 280 697
    • 反思
      • 实际编码时间是计划的3倍左右
      • 主要问题在于第一次用C++写面向对象的程序,不了解VS平台的各种工具和提示,对问题的规模没有正确的判断。
      • 这也就导致后面最初构思问题非常大,之后反复改思路,修改程序
      • 浪费了很多时间,最终程序的效率和正确性都难以保障。

    构思

    解题思路

    1. 解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程。(3')
    • 解题思路分为两部分
      1. 具体计算方法
      2. 计算优化构思

    具体计算方法

    1. 求直线方程
    • 两点求直线,回顾高中数学,参考博文之后,采用直线一般式方程(Ax + By + C = 0),并在(O(1))时间复杂度内计算出直线方程
    1. 计算交点
    • 计算交点也是有公式,可以在(O(1))时间复杂度内得出
    • 但计算新增直线和前序直线的交点,是对时间复杂度影响很大的步骤
    • 很容易想到的一个暴力解法:
      • 每新增直线n,新增交点数是(n-1-N(平行线数)-N(过交点线数)),同时维护平行斜率集合和已有交点集合
      • 该算法时间复杂度为(O(n^2))
    1. 关于交点精确度问题
    • 尝试自定义分数类来表示

    代码设计

    1. 设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?(4')
    • 基本元素(class)
      • 点 Point
        • 点构造函数 Point(x,y)
        • 点与点关系维护函数(equal)
        • 交叉点所属直线维护函数(add, contains, size)
      • 线 Line
        • 直线两点构造函数 Line(point1, point2)
        • 直线间关系判断函数(parallel, intersect, equal)
      • 直线集合 lineSet
      • 交点集合 interSet
      • 分数计算类 Radio
        • 加减乘除、分子分母最小化
    • 每次加入新直线的流程图
    graph TD A[构造新直线] --> B[判断交叉,并维护交叉集合] B --> C[得到交叉线集合delSet] C --> D[遍历除delSet之外的前序输入的直线集合,并维护新的交叉集合]

    性能改进

    1. 记录在改进程序性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由 VS 2019 的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
    • 在编写程序时,我主要面对了两大问题,这两大问题也严重影响了整体性能

      • 不熟悉C++代码
      • 构思时对问题没有深入准确的理解
    • 尤其是第二个问题,导致了第一次运行时效能严重低下,最终需要多次代码重构,

    1. 最初设计Radio分数类,其中一些求取最大公倍数的函数导致程序性能异常低下,以至于1min也只能跑500+条直线
    • 其中分数类Radio占用了接近50%的CPU使用率,而求取最大公约数函数则是其中的大头
    • 之后参考位运算——高效的最大公约数算法来优化,但是最后效果平平,放弃使用Radio类来提高精度而采用float

    2Radio效能分析

    1. 程序最初设计采用遍历,加之除去平行线、交点剪枝,以提高程序性能,但是最终实现时候,最初的设计delSet设计导致CPU占用率极高
    • 遍历交叉点,而且在交叉点的直线集合中查找,导致整个程序效能也只能达到1min600+条直线,所以删去这两种优化
    • 反思:并不是这两种方法不能提高性能,而是我在构思和编写中没有采用更好的数据结构和算法
    1. 最终的性能分析图
    • 程序中消耗最大的函数是main函数中的向交叉点集合中插入交叉点的插入操作,这让我想我试用unordered_set来进一步优化,只可惜DDL迫在眉睫;消耗次之的函数是main函数中的前序直线组遍历操作,这是我们在最初的分析中遇见到的

    2最终效能分析

    代码综述

    1. 代码说明。展示出项目关键代码,并解释思路与注释说明。(3')

    代码说明

    • 采用Top-down的思路来阐述我的代码
    graph TD A[直线与交点集合操作] --> B[直线,交点类] B --> C[浮点数确保精度] B --> D[直线相交平行计算]
    • main.cpp中的直线与交点集合操作
      • 前序直线集合顺序遍历和尾部添加,可以采用List这种链表型数据结构
      • 交点集合需要避免重复,而且需要较高的查找速度,故采用以红黑树为数据结构的set(也可以尝试以hash为索引结构的unordered_set)
    // 直线和交点集合
    vector<Line> lineList;
    set<Point> interSet;
    
    // 遍历
    for (i = 0; i < n; i++) {
    		input >> c;
    		if (c == 'L') {
    			input >> x1 >> y1 >> x2 >> y2;
    			Line l(x1, y1, x2, y2);
    			
    			for (auto iter = lineList.begin(); iter != lineList.end(); iter++) {
    				Line lit = (Line)* iter;
    				if (lit.isParallel(l)) {	
    					continue;
    				}
    				// 交点计算和新增
    				Point pInter = l.getIntersect(lit);
    				interSet.insert(pInter);
    			}
    			lineList.push_back(l);
    		}
    	}
    
    • Graph.h & cpp中的点和线类
      • 其中用于set排序点operator< & operator==重载,都是比较重要的地方
    class Point
    {
    public:
    	float x;
    	float y;
    
    	Point(float xNew, float yNew);
    	bool equal(Point p);
    	float getX();
    	float getY();
    	// 重载
    	bool operator<(const Point& p) const {
    		if (!EQFLOAT(x, p.x))
    			return x < p.x;
    		else
    			return y < p.y;
    	}
    	bool operator==(const Point& p) const {
    		return EQFLOAT(x, p.x) && EQFLOAT(y, p.y);
    	}
    	
    private:
    };
    
    • 直线的参数、斜率、是否含有某点、取得交点,都很重要
    class Line
    {
    public:
    
    	Line(int x1, int y1, int x2, int y2);
    	float getA();
    	float getB();
    	float getC();
    	float getslope();
    	bool isParallel(Line l);
    	bool containsPoint(Point p);
    	Point getIntersect(Line l);	
    	bool equal(Line l);
    
    private:
    	// line: Ax + By + C = 0;
    	float A;
      float B;
    	float C;
    	float slope;
    };
    
    // 计算直线参数极其斜率
    Line::Line(int x1, int y1, int x2, int y2) {
    	A = (float) y2 - y1;
    	B = (float) x1 - x2;
    	C = (float) x2 * y1 - x1 * y2;
    	if (x1 - x2 == 0) {
    		slope = FLT_MAX;
    	}
    	else {
    		slope = (float)(y1 - y2) / (x1 - x2);
    	}
    }
    
    • 在判断两交点是否重合的问题上,需要引入浮点数的精度计算
    #define EQS (1e-8)
    #define EQFLOAT(a,b) (fabs((a) - (b)) < EQS)
    
    bool Point::operator==(const Point& p) const {
    		return EQFLOAT(x, p.x) && EQFLOAT(y, p.y);
    	}
    
    • 最后取得直线的交点,判断直线是否平行,也是关键
    Point Line::getIntersect(Line l) {
    	float a2 = l.getA();
    	float b2 = l.getB();
    	float c2 = l.getC();
    	float x = (B * c2 - C * b2) / (A * b2 - B * a2);
    	float y = (C * a2 - A * c2) / (A * b2 - B * a2);
    	Point p(x, y);
    	return p;
    }
    
    bool Line::containsPoint(Point p) {
    	float res = A * p.getX() + B * p.getY() + C;
    	return EQFLOAT(res, 0);
    }
    

    测试与调试

    • 单元测试
      • 覆盖性的测试了Point和Line两个类的成员函数的功能
    		TEST_METHOD(TestMethodPoint1) {
    			Point p(0.5, 3);
    			Assert::AreEqual(p.getX()== 0.5, true);
    			Assert::AreEqual(p.getY()==3, true);
    			Point m(0.5, -3);
    			Assert::AreNotEqual(p.equal(m), true);
    		}
    
    		TEST_METHOD(TestMethodLine1) {
    			Line l1(0, 0, 1, 1);
    			Line l2(0, 2, 1, 0);
    			Line l3(0, -45, 45, 0);
    			Line lr(1, 0, 5, 0);
    			Line bt(1, 1, 1, 10);
    			Assert::AreEqual(l2.getA()== -2, true);
    			Assert::AreEqual(l2.getB()== -1, true);
    			Assert::AreEqual(l2.getC()==2, true);
    			Assert::AreEqual(l2.getslope()==-2, true);
    			// parallel
    			Assert::AreEqual(l1.isParallel(l3), true);
    			Assert::AreEqual(lr.isParallel(bt), false);
    			// containsPoint
    			Point e(0.5,1);
    			Point base(0,0);
    			Assert::AreEqual(l2.containsPoint(e), true);
    			Assert::AreNotEqual(l1.containsPoint(e), true);
    			Assert::AreEqual(l1.containsPoint(base), true);
    			// get intersect
    			Point inter12((float)2/3,(float)2/3);
    			Assert::AreEqual(l1.getIntersect(l2).equal(inter12), true);
    			Point inter3lr(45, 0);
    			Point inter3tb(1, -44);
    			Assert::AreEqual(l3.getIntersect(lr).equal(inter3lr), true);
    			// equal
    			Assert::AreEqual(l1.equal(l2), false);
    		}
    
    • 调试辅助工具
      • GeoGebra是一项可视化界面精良的轻量级数学绘图工具,使用它也辅助我发现了程序的一个bug

    2图形界面debug

    • 覆盖性压力测试
      • 借鉴了同学随机生成点阵的代码,生成了大量的几千数据量的黑盒测试集来辅助测试
      • 但是黑盒测试问题在于数据量过大,一旦对拍出现不一致也很难找到问题。

    编程反思

    • 这次作业我差不多断断续续写了两天,由于C++和平台不熟悉,以及寒假项目也没有太多软件开发任务,所以这一次作业从完成结果和过程上看,都不尽人意。后面的软件工程开发还有很重的担子。

    • 下面记录一些问题和需要改进的地方

    • 问题

      • 设计文档需要细化到哪个细粒度?哪些工作是需要代码完成的?
      • 写代码时改设计文档是一件可以避免的事吗?如何避免
    • 改进

      • 由于编程能力的问题,我之后需要在学习各种方法的同时,留更多的时间给程序编写,以保证作业质量,比如这次作业,假如能提前一天时间开始写,应该会大有长进
      • 需要更熟悉C++这门语言,以及VS IDE,保证之后多人开发的效率
  • 相关阅读:
    Django--模型层进阶
    Django--模板层
    对自己的博客园主题稍作修改
    集群中Session共享解决方案分析
    【检测工具】keepalived安装及配置
    跨域问题简单分析
    Linux设置静态IP后出现的几种问题
    Linux上安装ElasticSearch及遇到的问题
    Linux上安装JDK1.8,tomcat9,以及mysql8的步骤
    归并排序分析
  • 原文地址:https://www.cnblogs.com/yzy11235/p/12457866.html
Copyright © 2020-2023  润新知