开放模式的软件工程教学实践
在个人项目开始之前,我之前的编程几乎从来没有在开始前预估过时间,所以也就是摸着石头过河,大概根据之前的经验估计了一下时间。
从上面两个表格可以看出,最终我完成这个个人项目所用的时间几乎达到了计划值的两倍之多。对于设计-实现-测试这个流程来说,主要比计划多出来的时间都花在了设计与实现上,我认为之所以多花了将近一倍的时间,我想这与我从最开始设计程序开始,就在不停的思考和推翻一个个设计,并不断改进追求程序性能的最大化有关。
如果说在做这个个人项目之前,我把编程的重点放在把需求尽可能完整的而又没有错误的实现上的话,这次贯穿整个项目流程的主线则是不断的优化算法,以寻求程序的性能在保证没有错误的基础上最大化。
下面讲一下我整个优化的流程。
第一阶段 设计阶段
在设计之初,我就在一直想一个问题:如何将地铁图抽象成为数据结构,用什么样的数据结构能够最简单,最直接,最利于算法的发挥,并且空间的开销最小。最开始,也是最自然想法就是将所有的地铁站按照线路来进行存储,每条线路中存储着所有的车站,然后再对于换乘站进行特殊处理。相信这是大多数人最开始的思路,于是我也按照这样的思路开始了设计,对于这样的数据结构,很自然的就想到了利用广搜来进行最短路径的搜索,对于完成按照换乘最少和站数最少的搜索,广搜似乎都能很好的得到结果。
但是这时,我又转念一想,广搜在最坏情况下的时间复杂度达到了N^2,虽然这次图的节点数并不多,按照所有的地铁站来算,也就是不到300个节点,而且每个节点并不是与所有节点都有连接,而是大部分只与两个节点(非换乘站),最多与六个(三线换乘站)节点之间有连接,这又会大大降低广搜的搜索空间。这样的算法对于这样的数据规模还是比较合适的。
但是,对着地铁图琢磨了一会以后,我又有了一个新的想法。
问题就在于这地铁图的特殊性,一个站貌似与两个节点之间有连接,但实际上在地铁线路中旅行时,一个最短线路绝对不会走回头路,也就是说实际上在单独的一条线路上,最短路径只能是单向行走,直到出现了换乘站,不同的路径之间才会在这里产生分歧,走向不同的方向。所以,地铁线路中看似有非常非常多的地铁站点,但实际上能够对最短路径产生影响的,只有那些多线交叉的换乘站,而换乘站与换乘站之间的非换乘站,只要把它们抽象成一个带权值的边就足够了。在数据结构中,真正需要的,就是那些换乘站组成的换乘节点。
按照这样的思路,整个地图就能够被简化为将近五十个换乘节点,和换乘节点之间连接的带权边,在这样的图中进行广搜,搜索效率应该能够有所提升。所以,在真正开始实现之前,我就明确了注重换乘站而尽量简化换乘站之间的非换乘站这样的整体设计。
第二阶段 正式编码阶段
于是在编码阶段,虽然为了计算起始点与终点不是换乘站的最短路径,我的程序的数据结构中仍然需要存储所有的站点以及所有的线路,但是在实际的算法实现中,并没有用到这些数据,仅仅是利用了一张代表所有换乘站之间的距离的二维数组,并找到距离起始点和终点最近的两个换乘站,进行最短路径的查找。这样是对于广搜算法的一次优化,提升了一部分程序性能。
接下来,我又对广搜这个算法提出了疑问,对于这样的数据结构,能否有更高效的最短路径搜索算法呢?于是我又想到了迪杰斯特拉算法,这种算法在最坏情况下的时间复杂度要比广搜小了一个数量级,并且之前我就选定好的数据结构非常适合迪杰斯特拉算法算法的应用,因此我觉得要用迪杰斯特拉算法来提升整个程序的性能。
然后就出现了一个小问题,因为所有按照换乘站来存储的数据结构实际上把所有的线路都割裂成了换乘站之间的小段线路的集合,也就是说在经过这样的抽象以后,很难再在寻找最短路径时区分一条边和另一条边是不是同一条路线,至少,对于迪杰斯特拉算法算法来说很难,这也就使得迪杰斯特拉算法算法无法完成对于换乘最少的最短路径的搜索。而此时广搜就可以发挥它的用武之地,在搜索时,可以配合线路图,做“线路优先”的搜索,也就是说将最少换乘最短路线问题,转换成在最少的节点的连接下,由起点线路换乘到终点线路。这是相对容易实现的。
所以在最后的程序当中,对于不同的搜索模式,我分别用了两种不同的搜索算法。在搜索两站之间换乘最少的最短路径时,程序使用广度优先搜索,在搜索站数最少的最短路径时,程序使用迪杰斯特拉算法进行搜索。
程序性能分析图如下,在分析性能时,我分别使用广搜和迪杰斯特拉算法对十对不同的起点终点信息进行了测试,测试结果如下
其中BFSPath是广搜算法,DjistraPath是迪杰斯特拉算法。可以看到,在相同的测试数据下,迪杰斯特拉算法所用的时间大概是广搜的25%左右,而在更详细的分类下,广搜算法所有时间都用在了广搜的递归调用子函数上,而迪杰斯特拉算法中真正计算路径的算法部分只占用了2%左右的时间,而其他时间则用在了处理地图数据和建立路径上。可见,如果不考虑算法的复数函数的时间,两种算法在计算路径的时间上几乎有十倍左右的差距。
而这一切的一切,还是建立在两种算法都对地铁站本身的数据结构进行了优化存储,仅保留换乘的节点的基础之上得到的数据。可想而知,如果直接使用我最开始设想的数据结构,那么用的时间还会延长。
第三阶段 性能优化阶段
在整体编码忘了之后,我跟根据性能分析工具提供的结果检查了一遍所有函数,找到了几个耗时间比较长的函数,发现他们的共性就是都存在调用函数时遍历数组的情况。我对此进行了分析,发现有一个函数为了返回当前线路中的总站数而遍历了整个线路集合。这是一个可以优化的点,因为集合的总站数可以在每次集合中添加或者删除元素时记录在对象中的一个整型变量中而没有必要每次都遍历整个数组。因此我对此进行了修改,并且将几个函数中关于数组遍历的部分都检查了一遍,并修改了所有类似的部分。
在修改后,这几个函数都不在存在于耗时较多的函数列表中了。
十组测试用例
1. MetroSystem.exe -b 2号航站楼 3号航站楼
2. MetroSystem.exe -b 健德门 火器营
3. MetroSystem.exe -b 南邵 角门西
4. MetroSystem.exe -b 朱辛 马泉营
5. MetroSystem.exe -b 西直门 国贸
6. MetroSystem.exe -c 3号航站楼 西局
7. MetroSystem.exe -c 北苑路北 安立路
8. MetroSystem.exe -c 广渠门外 国贸
9. MetroSystem.exe -c 大屯路东5 大屯路东15
10.MetroSystem.exe -c 什刹海 什刹海