- 在文章开头给出教学班级和可克隆的 Github 项目地址。(1')
项目 | 内容 |
---|---|
这个作业属于 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求是 | 个人项目作业 |
我的教学班级 | 006 |
本项目的GitHub地址 | IntersectProj |
我对这项作业的目标是 | 提高个人程序开发素质,写出高性能程序 |
PSP
- 在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
- 在你实现完程序之后,在下述 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平台的各种工具和提示,对问题的规模没有正确的判断。
- 这也就导致后面最初构思问题非常大,之后反复改思路,修改程序
- 浪费了很多时间,最终程序的效率和正确性都难以保障。
构思
解题思路
- 解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程。(3')
- 解题思路分为两部分
- 具体计算方法
- 计算优化构思
具体计算方法
- 求直线方程
- 两点求直线,回顾高中数学,参考博文之后,采用直线一般式方程(Ax + By + C = 0),并在(O(1))时间复杂度内计算出直线方程
- 计算交点
- 计算交点也是有公式,可以在(O(1))时间复杂度内得出
- 但计算新增直线和前序直线的交点,是对时间复杂度影响很大的步骤
- 很容易想到的一个暴力解法:
- 每新增直线n,新增交点数是(n-1-N(平行线数)-N(过交点线数)),同时维护平行斜率集合和已有交点集合
- 该算法时间复杂度为(O(n^2))
- 关于交点精确度问题
- 尝试自定义分数类来表示
代码设计
- 设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?(4')
- 基本元素(class)
- 点 Point
- 点构造函数 Point(x,y)
- 点与点关系维护函数(equal)
- 交叉点所属直线维护函数(add, contains, size)
- 线 Line
- 直线两点构造函数 Line(point1, point2)
- 直线间关系判断函数(parallel, intersect, equal)
- 直线集合 lineSet
- 交点集合 interSet
- 分数计算类 Radio
- 加减乘除、分子分母最小化
- 点 Point
- 每次加入新直线的流程图
graph TD
A[构造新直线] --> B[判断交叉,并维护交叉集合]
B --> C[得到交叉线集合delSet]
C --> D[遍历除delSet之外的前序输入的直线集合,并维护新的交叉集合]
性能改进
- 记录在改进程序性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由 VS 2019 的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
-
在编写程序时,我主要面对了两大问题,这两大问题也严重影响了整体性能
- 不熟悉C++代码
- 构思时对问题没有深入准确的理解
-
尤其是第二个问题,导致了第一次运行时效能严重低下,最终需要多次代码重构,
- 最初设计Radio分数类,其中一些求取最大公倍数的函数导致程序性能异常低下,以至于1min也只能跑500+条直线
- 其中分数类Radio占用了接近50%的CPU使用率,而求取最大公约数函数则是其中的大头
- 之后参考位运算——高效的最大公约数算法来优化,但是最后效果平平,放弃使用Radio类来提高精度而采用float
- 程序最初设计采用遍历,加之除去平行线、交点剪枝,以提高程序性能,但是最终实现时候,最初的设计delSet设计导致CPU占用率极高
- 遍历交叉点,而且在交叉点的直线集合中查找,导致整个程序效能也只能达到1min600+条直线,所以删去这两种优化
- 反思:并不是这两种方法不能提高性能,而是我在构思和编写中没有采用更好的数据结构和算法
- 最终的性能分析图
- 程序中消耗最大的函数是main函数中的向交叉点集合中插入交叉点的插入操作,这让我想我试用unordered_set来进一步优化,只可惜DDL迫在眉睫;消耗次之的函数是main函数中的前序直线组遍历操作,这是我们在最初的分析中遇见到的
代码综述
- 代码说明。展示出项目关键代码,并解释思路与注释说明。(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==
重载,都是比较重要的地方
- 其中用于set排序点
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
- 覆盖性压力测试
- 借鉴了同学随机生成点阵的代码,生成了大量的几千数据量的黑盒测试集来辅助测试
- 但是黑盒测试问题在于数据量过大,一旦对拍出现不一致也很难找到问题。
编程反思
-
这次作业我差不多断断续续写了两天,由于C++和平台不熟悉,以及寒假项目也没有太多软件开发任务,所以这一次作业从完成结果和过程上看,都不尽人意。后面的软件工程开发还有很重的担子。
-
下面记录一些问题和需要改进的地方
-
问题
- 设计文档需要细化到哪个细粒度?哪些工作是需要代码完成的?
- 写代码时改设计文档是一件可以避免的事吗?如何避免
-
改进
- 由于编程能力的问题,我之后需要在学习各种方法的同时,留更多的时间给程序编写,以保证作业质量,比如这次作业,假如能提前一天时间开始写,应该会大有长进
- 需要更熟悉C++这门语言,以及VS IDE,保证之后多人开发的效率