北航软工个人项目作业

个人项目作业-求图形交点个数

项目 内容
本作业属于北航 2020 年春软件工程 博客园班级连接
本作业是本课程个人项目作业 作业要求
我在这个课程的目标是 收获团队项目开发经验,提高自己的软件开发水平
这个作业在哪个具体方面帮助我实现目标 体验软件开发的 Workflow
项目代码 Github 仓库

解题思路

根据需求描述,我们不难得到所需软件的运行流程,总体而言分为三步:

  • 解析命令行参数,获取输入文件与输出文件的路径
  • 从输入文件中获取输入,并对图形参数进行解析,存储于相应的数据结构中
  • 求解图形之间的交点个数,并输出

第一步是命令行交互软件的基本操作,不再赘述。

第二步的关键在于设计存储图形所需的数据结构。考虑图形有两种:直线与圆,且直线的输入数据是两点式,圆的输入数据是圆心-半径式,同时点需要两个参数来标识,半径需要一个参数来标识,因此每个点用数对来存储,每条直线用两个点来表示,每个圆用一个点和一个半径来表示。

第三步是最复杂的。图形类型有两种,因此有\(C^2_3\)种情况需要纳入考虑范围内。根据数学知识,继续划分如下:

  • 直线与直线
    • 平行:交点个数 0
    • 同一条直线:交点个数无限
    • 相交:交点个数 1
  • 直线与圆
    • 相离:交点个数 0
    • 相切:交点个数 1
    • 相交:交点个数 2
  • 圆与圆
    • 相离:交点个数 0
    • 相切:交点个数 1
    • 相交:交点个数 2
    • 内含:交点个数 0

每一种情况都有完整的初等解法,参见 Paul Bourke 先生的文章

设计

数据结构

同上述提到的数据结构设计。CPP 代码如下

using Point  = std::pair<int32_t, int32_t>;
using Line   = std::pair<Point, Point>;
using Circle = std::pair<Point, float>;

对于交点的存储,如果只有直线,那么可以用两个整数来表示有理数的方法实现,不存在精度的问题。但是除了直线以外还有圆,引入了无理数,因此浮点数是有必要的。CPP 代码如下。

struct IntersectPoint
{
    float first;
    float second;

    IntersectPoint(float first, float second) : first(first), second(second) {}

    bool operator < (const IntersectPoint& rhs) const
    {
        return first - rhs.first < -eps || (!(rhs.first - first < -eps) && second - rhs.second < -eps);
    }
    friend bool operator < (IntersectPoint& left, IntersectPoint& right)
    {
        return left.operator<(right);
    }
};

可以发现,我没有选择使用 STL 内建数据结构,而是使用自定义类,原因在于精度。由于浮点数的不准确性,两个数学上相等的值在计算机中可能会因为运算的原因产生误差,此时需要使其在一定程度上容许误差的存在,因此不能容许误差的 STL 内建数据结构不是一个好的选择。

复杂度分析

对于需求中的第三步操作,我的策略如下:每个图形都与其他图形计算一次交点,存储交点并去重。不难发现,时间复杂度与空间复杂度均为\(\Theta(n^2)\)

实际上,这个复杂度是难以接受的,尤其是对于 50W 的输入数据量而言。在测试时,空间占用更是一度达到了 32GB。由于时间关系,我暂且还未找到更佳的处理策略。

代码实现

代码组织

共有 5 个主要文件,其中 main.cpp 与 option.hpp 主要内容是用户交互与 benchmark,intersect.cpp 与 intersect.hpp 主要内容是主要功能的实现,intersect_test.cpp 的内容则是测试相关。

IntersectProject
└── src
    ├── main.cpp
    ├── intersect.cpp
└── include
    ├── intersect.hpp
    ├── option.hpp
└── test
    └── intersect_test.cpp

在 intersect.cpp 中,函数的关系大致如下

calculate_intersect_points - 计算图形之间的交点个数
├── calculate_line_circle_intersect_points - 计算一条直线和一个圆之间的交点个数
├── calculate_line_intersect_points - 计算两条直线之间的交点个数
└── calculate_circle_intersect_points - 计算两个圆之间的交点个数

可以看出,只要功能函数的组织和需求分析是相吻合的。

不同情况下的返回值处理

正如上面提到的,两个图形在不同的位置有着不同的相对关系,从而影响着交点的个数。

但总的来说,分为两种,0 个和 2 个(相切时,交点相同)。这种情况下,我选择 C++17 引入的 std::optional,这是一个 Sum Type,可以优雅地处理不同返回值问题,不需要额外引入 flag 变量以标志返回值内容。

测试

对于两个图形位置的每个可能情况,都有相应的一个测试点。代码覆盖率如下:

(使用 OpenCPPCoverage 生成,由于测试问题,与用户交互的 main.cpp 和 option.hpp 基本没有覆盖,只覆盖了功能实现的 intersect.cpp)

代码质量分析

如下

实际上,我的 Visual Studio 是配以 Jetbrains 的 Resharper C++ 使用的,Resharper C++ 可以帮助我调用 clang-tidy 及时地对代码风格与质量进行提醒。

性能改进

初始时,我选择 std::set 作为交点的存储容器。经过 profile 之后,我发现热点在于 set::insert,它的调用次数非常多,而且每次调用的花销非常高。因此,我选择了 std::vector 作为容器,在处理完毕后再进行排序去重操作。对于 1000 级别的数据量而言,性能提高了约三倍。但是,目前的性能热点仍然在于排序去重操作,这是我目前采取的处理策略所不能避免的。

事后总结

由于本次任务的需求是确定的,没有太多需要揣测的地方,因此需求分析花费的时间比较少。

而本次任务花费时间最多的阶段是设计阶段,主要在于寻找一个不需要复杂运算且具有足够精度的方法。在数学上,可以使用反三角函数来轻松解决这个问题,但是在计算机中,反三角函数是一个复杂的运算过程,且精度一般。我尝试使用 Mathmatica 辅助求解,但给出的计算公式十分复杂,没能充分利用图形本身的特征进行简化。最后,我找到了 Paul Bourke 先生的文章,方法很简洁,计算过程也适合于计算机。

PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 5 5
Development 开发
· Analysis · 需求分析 (包括学习新技术) 15 15
· Design Spec · 生成设计文档 10 10
· Design Review · 设计复审 (和同事审核设计文档) 0 0
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 0 0
· Design · 具体设计 30 90
· Coding · 具体编码 60 60
· Code Review · 代码复审 10 10
· Test · 测试(自我测试,修改代码,提交修改) 60 60
Reporting 报告
· Test Report · 测试报告 10 10
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
合计 240 300
posted @ 2020-03-10 10:28  btapple  阅读(234)  评论(1编辑  收藏  举报