[结对编程作业--天津地铁线路]
一、任务:
实现一个帮助进行地铁出行路线规划的命令行程序。
二、设计信息
- 开发语言:JAVA
- 算法:Dijkstra
- 功能设计框架
三、需求分析及实现
需求1
在程序启动时,自动获取到地图信息
需要实现一个支持自动加载subway.txt 文件的程序,程序启动时需要通过读取 -map 参数来获得对应的自定义地铁文件(命名为 subway.txt),从而得到地铁线路图的信息。一个调用应用程序的示例如下:
java subway -map subway.txt
需求2
-
查询指定地铁线经过的站点
在应用程序上,需要支持一个新的命令行参数 -a ,指定用户希望查询的地铁线路。
在给定地铁线路时,程序需要从线路的起始站点开始,依次输出该地铁线经过的所有站点,直到终点站。输出的文件使用
-o
参数来指定。一个调用应用程序的示例如下:java subway -a 1号线 -map subway.txt -o station.txt
下为实际输出的station.txt 文件的内容
地铁:1号线 刘园 西横堤 果酒厂 本溪路 勤俭道 洪湖里 西站 西北角 西南角 二纬路 海光寺 鞍山道 营口道 小白楼 下瓦房 南楼 土城 陈塘庄 复兴门 华山里 财经大学 双林 李楼
需求3
计算从出发到目的站点之间的最短路线并输出经过的站点的个数和路径
如果用户希望坐地铁,他希望能通过最少的站数从出发点到达目的地,这样就可以在命令行中以 -b 参数加两个地铁站点名称分别作为出发与目的,比如用户希望知道 洪湖里 到复兴路 之间的最短路线是怎样的,他就可以使用如下命令让程序将结果写入 routine.txt 中。
subway.exe -b 洪湖里 复兴路 -map subway.txt -o routine.txt
程序将计算从出发到目的站点之间的最短(经过的站点数最少)路线,并输出经过的站点的个数和路径(包括出发与目的站点)。如果需要换乘,会在换乘站的下一行输出换乘的线路。输出的文件使用-o
参数来指定。
一个调用应用程序的示例如下:
java subway -b 华苑 乐园道 -map subway.txt -o routine.txt
下为实际输出的routine.txt 文件的内容
起始站:华苑
3号线
王顶堤
红旗南路
6号线
迎风道
南翠屏
水上公园东路
肿瘤医院
天津宾馆
文化中心
乐园道
共经过 10 站
四、逻辑实现
数据预处理
- 地铁线路数据在subway.txt文件中的记录以如下格式呈现:
1:1号线:刘园,西横堤,果酒厂,本溪路,勤俭道,洪湖里,西站,西北角,西南角,二纬路,海光寺,鞍山道,营口道,小白楼,下瓦房,南楼,土城,陈塘庄,复兴门,华山里,财经大学,双林,李楼
2:2号线:曹庄,卞兴,芥园西道,咸阳路,长虹公园,广开四马路,西南角,鼓楼,东南角,建国道,天津站,远洋国际中心,顺驰桥,靖江路,翠阜新村,屿东城,登州路,国山路,空港经济区,滨海国际机场
3:3号线:小淀,丰产河,华北集团,天士力,宜兴埠,张兴庄,铁东路,北站,中山路,金狮桥,天津站,津湾广场,和平路,营口道,西康路,吴家窑,天塔,周邓纪念馆,红旗南路,王顶堤,华苑,大学城,高新区,学府工业区,杨伍庄,南站
4:5号线:北辰科技园北,丹河北道,北辰道,职业大学,淮河道,辽河北道,宜兴埠北,张兴庄,志成路,思源道,建昌道,金钟河大桥,月牙河,幸福公园,靖江路,成林道,津塘路,直沽,下瓦房,西南楼,文化中心,天津宾馆,肿瘤医院,体育中心,凌宾路,昌凌路,中医一附院,李七庄南
5:6号线:南孙庄,南何庄,大毕庄,金钟街,徐庄子,金钟河大桥,民权门,北宁公园,北站,新开河,外院附中,天泰路,北运河,北竹林,西站,复兴路,人民医院,长虹公园,宜宾道,鞍山西道,天拖,一中心医院,红旗南路,迎风道,南翠屏,水上公园东路,肿瘤医院,天津宾馆,文化中心,乐园道,尖山路,黑牛城道,梅江道,左江道,梅江公园,梅江会展中心,解放南路,洞庭路,梅林路
6:9号线:天津站,大王庄,十一经路,直沽,东兴路,中山门,一号桥,二号桥,张贵庄,新立,东丽开发区,小东庄,军粮城,钢管公司,胡家园,塘沽,泰达,市民广场,太湖路,会展中心,东海路
- 在读取了文件中的数据后,首先以 ”: “ 作为分隔,将每一行的数据分为三部分,第一部分为每条地铁的标号,第二部分为每条地铁的名称,第三部分为每条地铁的站点信息。
- 程序将识别所有的转乘站点,作为一个结点,这样可以避免在计算最短路径过程中,将所有站点的距离都计算一遍。并且,由于北京地铁的特殊性,有些地铁线路为环状线路,我们需要把成环的线路检测出来,并且以map的形式记录其编号和线路总长度在这里。
为了方便后续的路径计算,我们以距离为模拟将每个站点计算出其编号,公式为(地铁线路标号*1000 + 当条地铁线第几个站点 +1)。若该地铁为环状地铁线路,则将 其编号乘以(-1),以便在计算最短路径时候可以知道这是一条环线,并采用不同的最短计算方式。
数据加载预处理的方法函数如下:
public void loadLineFile()
// 加载地铁线路数据
public void loadLineFile(String strSubwayFileName) {
File fSubway = new File(strSubwayFileName);
BufferedReader reader = null;
try {
InputStreamReader isr = new InputStreamReader(new FileInputStream(strSubwayFileName), "UTF-8");
reader = new BufferedReader(isr);
String tempString = null;
while ((tempString = reader.readLine()) != null) {
if (tempString.startsWith("\uFEFF")) {
tempString = tempString.substring(1, tempString.length());
}
listSubwayInfo.addElement(tempString);
}
System.out.println("成功加载地铁线路文件!\n");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
parseSubwayStationsData();
}
void parseSubwayStationsData()
void parseSubwayLineData()
// 地铁数据处理
void parseSubwayStationsData() {
for (String strSubwayLine: listSubwayInfo) {
parseSubwayLineData(strSubwayLine);
}
}
void parseSubwayLineData(String strSubwayLine) {
String[] arrLineAndStations = strSubwayLine.split(":"); // 划分地铁线路和该线路所有站点
if (arrLineAndStations.length != 3) {
System.out.println("地铁数据错误" + strSubwayLine);
return;
}
int nLine = getLineNumber(arrLineAndStations[0]);
stationInfo.put(nLine,arrLineAndStations[1]);
NtostationInfo.put(arrLineAndStations[1],nLine);
if (nLine == -1) {
System.out.println("地铁线路号数据错误" + strSubwayLine);
}
String[] arrStrStationNames = arrLineAndStations[2].split(",");
if(arrStrStationNames[0].equals(arrStrStationNames[arrStrStationNames.length-1]))
{
for (int i=0; i < arrStrStationNames.length-1; i++) {
String strStationName = arrStrStationNames[i];
int nStationId = -(nLine*1000 + i+1);
circleInfo.put(nLine,arrStrStationNames.length-1 );
Station station = new Station();
station.stationName = strStationName;
station.setStationId.add(nStationId);
mapStationIdtoStation.put(nStationId, station);
if (!mapNametoStation.containsKey(strStationName)) {
mapNametoStation.put(strStationName, station);
} else {
// 如果站点名字存在,证明是中转站
Station stationExistedTransferStation = mapNametoStation.get(strStationName);
stationExistedTransferStation.setStationId.add(nStationId);
mapTransferStationNametoDistance.put(stationExistedTransferStation.stationName, nMaxDistance);
}
}
}
for (int i=0; i < arrStrStationNames.length; i++) {
String strStationName = arrStrStationNames[i];
int nStationId = nLine*1000 + i+1;
Station station = new Station();
station.stationName = strStationName;
station.setStationId.add(nStationId);
mapStationIdtoStation.put(nStationId, station);
if (!mapNametoStation.containsKey(strStationName)) {
mapNametoStation.put(strStationName, station);
} else {
// 如果站点名字存在,证明是中转站
Station stationExistedTransferStation = mapNametoStation.get(strStationName);
stationExistedTransferStation.setStationId.add(nStationId);
mapTransferStationNametoDistance.put(stationExistedTransferStation.stationName, nMaxDistance);
}
}
}
迪杰斯特拉算法求最短路径
操作步骤
- 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]。
- 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k。
- 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
- 重复步骤(2)和(3),直到遍历完所有顶点。
- 同时,在计算最短路径的同时,要保存每个结点最后被更新前的上一个结点,这样可以方便我们后续输出其完整路径。
以下为迪杰斯特拉算法计算最短路径的方法函数:
Path Dijkstra()
// 最优路径规划
Path Dijkstra(String strStartStationName, String strEndStationName) {
// todo: 进行一些合法性检查
Station stationStart = mapNametoStation.get(strStartStationName);
Station stationEnd = mapNametoStation.get(strEndStationName);
if(stationStart==null || stationEnd==null)
{
System.out.println("起始站或终点输入不正确,请检查输入数据!");
return null;
}
mapTransferStationNametoDistance.put(strEndStationName, nMaxDistance);
mapTransferStationNametoDistance.put(strStartStationName, nMaxDistance);
Path pathStart = new Path();
pathStart.nFDistance = 0;
pathStart.stationLastStationInPath = stationStart;
Path Dijkstra = new Path();
Dijkstra.nFDistance = nMaxDistance;
Stack<Path> stackAllPaths = new Stack<>();
stackAllPaths.push(pathStart);
Set<String> TStationNameSet = new TreeSet<>();
for(String strname: mapTransferStationNametoDistance.keySet()) {
TStationNameSet.add(strname);
}
for(String strname: mapTransferStationNametoDistance.keySet()) {
finalMap.put(strname,"null");
}
while (!stackAllPaths.empty()) {
Path pathCurrent = stackAllPaths.pop();
if (pathCurrent.nFDistance > Dijkstra.nFDistance) {
continue;
}
int nBDistance = getStationsDistance(pathCurrent.stationLastStationInPath, stationEnd);
if (nBDistance == 0) { // 到达终止节点
if (pathCurrent.nFDistance < Dijkstra.nFDistance) {
Dijkstra = pathCurrent;
}
continue;
}
int minDistance = 1000000;
String nextStation = null;
TStationNameSet.remove(pathCurrent.stationLastStationInPath.stationName);
for (String strTStationName: mapTransferStationNametoDistance.keySet()) {
Station stationTransfer = mapNametoStation.get(strTStationName);
int nDistanceDelta = getStationsDistance(pathCurrent.stationLastStationInPath, stationTransfer);
int nTStationDistance = pathCurrent.nFDistance + nDistanceDelta;
if (nTStationDistance >= mapTransferStationNametoDistance.get(strTStationName)) {
continue;
}
finalMap.put(strTStationName,pathCurrent.stationLastStationInPath.stationName);
mapTransferStationNametoDistance.put(strTStationName, nTStationDistance);
}
for(String strTStationName: mapTransferStationNametoDistance.keySet()) {
int Distance = mapTransferStationNametoDistance.get(strTStationName);
if(Distance<minDistance&& TStationNameSet.contains(strTStationName)) {
minDistance = Distance;
nextStation = strTStationName;
}
}
Station stationTransfer = mapNametoStation.get(nextStation);
Path pathNew = new Path();
pathNew.nFDistance = minDistance;
pathNew.stationLastStationInPath = stationTransfer;
stackAllPaths.push(pathNew);
}
System.out.println(finalMap);
return Dijkstra;
}
打印路径 &打印指定地铁线路
在计算最短路径的时候,已经保存了每个站点最后被更新时的上一个结点,这个时候,只要我们循环从最后一个结点往回找,就可以得到最终”倒序“的路径了,这里有一个小技巧,为了避免再写一个linkedhashmap 的倒序,这里我们可以直接在输入起点和终点的时候,程序将起点和终点交换,这样就可以得到(倒序(倒序)=正序)的效果了。
同时在打印两个中间转站点之间的站点时,当两个站点的id差值为正的时候,就将step设置为正1,每次加1,并通过得到的编号输出站点名;同样,当两个站点的id 差值为负的时候,就将step设置为-1即可,最后,通过计算出的地铁线路编号和地铁名称的map,输出地铁名称以及地铁线路的信息,最后将其输入到txt文件中去。
stationsInLine()
Vector<String> stationsInLine(Station stationStart, Station stationEnd) {
Vector<String> listStations = new Vector<String>();
int nLineNumber = getLineNumber(stationStart, stationEnd);
int nStartId = 0;
int nEndId = 0;
for (int nId: stationStart.setStationId) {
if (Math.abs(nId-(nLineNumber*1000))<1000) {
nStartId = nId;
}
}
for (int nId: stationEnd.setStationId) {
if (Math.abs(nId-(nLineNumber*1000))<1000) {
nEndId = nId;
}
}
if (nStartId == nEndId) {
return listStations;
}
int nStep = 1;
if (nEndId < nStartId) {
nStep = -1;
}
int nIndexId = nStartId + nStep;
while (nIndexId != nEndId) {
String strSName = mapStationIdtoStation.get(nIndexId).stationName;
listStations.addElement(strSName);
nIndexId += nStep;
}
String strName = mapStationIdtoStation.get(nEndId).stationName;
listStations.addElement(strName);
return listStations;
}
void printPath()
void printPath(String start,String end, String strOutFileName) {
String strFormatedPath = formatPath(start,end);
toFile(strFormatedPath, strOutFileName);
}
formatPath()
String formatPath(String start,String end) {
StringBuffer strRst = new StringBuffer();
String StartStationName=end;
int i=1;
int nCurrentLine = -1;
System.out.print(finalMap);
strRst.append(String.format("\n起始站:%s\r\n", end));
while(end != finalMap.get(end)) {
Station stationStart = mapNametoStation.get(end);
Station stationEnd = mapNametoStation.get(finalMap.get(end));
end = finalMap.get(end);
int nLineNum = Math.abs(getLineNumber(stationStart, stationEnd));
if (nLineNum != nCurrentLine) {
nCurrentLine = nLineNum;
strRst.append(String.format("%s\r\n", stationInfo.get(nLineNum)));
}
for (String strStationName: stationsInLine(stationStart, stationEnd)) {
i++;
strRst.append(String.format("%s\r\n", strStationName));
}
System.out.println(i);
}
strRst.append(String.format("\n共经过 %d 站\r\n", i));
return strRst.toString();
}
void toFile()
void toFile(String strContent, String strOutFile) {
try {
File file = new File(strOutFile);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file.getName(), false);
fileWriter.write(strContent.toString());
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
};
printLineInfo()
void printLineInfo(String LineName, String strOutFile) {
StringBuffer strRst = new StringBuffer();
strRst.append(String.format("地铁:%s\r\n", LineName));
if(NtostationInfo.get(LineName)==null)
{
System.out.println("不存在该条地铁线路,请检查输入!\n");
return;
}
int nLineNum = NtostationInfo.get(LineName);
for (int i = 1; i < 90; i++) {
int nStationId = nLineNum * 1000 + i;
if (mapStationIdtoStation.containsKey(nStationId)) {
strRst.append(mapStationIdtoStation.get(nStationId).stationName + "\r\n");
} else {
break;
}
}
toFile(strRst.toString(), strOutFile);
}
五、测试
- 1.map 后没有参数信息
java subway -b 洪湖里 复兴路 -map
- 2.缺少输出文件信息
java subway -b 洪湖里 复兴路 -map subway.txt -o
- 3.缺少起点或终点信息
java subway -b 华苑 -map subway.txt -o routine
- 4.查询的地铁线路不存在
java subway -a 天府广场 -map subway.txt -o station.txt
- 5.查询的地铁起点或终点站不存在
java subway -b 天府广场 复兴路 -map subway.txt -o routine.txt
- 6.查询指定地铁线路并输出到station.txt
java subway -a 3号线 -map subway.txt -o station.txt
输出的station.txt文件内容:
地铁:3号线
小淀
丰产河
华北集团
天士力
宜兴埠
张兴庄
铁东路
北站
中山路
金狮桥
天津站
津湾广场
和平路
营口道
西康路
吴家窑
天塔
周邓纪念馆
红旗南路
王顶堤
华苑
大学城
高新区
学府工业区
杨伍庄
南站
- 7.查询的起点终点在同一条地铁线上
java subway -b 刘园 西北角 -map subway.txt -o routine.txt
输出的routine.txt文件内容:
起始站:刘园
1号线
西横堤
果酒厂
本溪路
勤俭道
洪湖里
西站
西北角
共经过 8 站
- 8.查询的起点终点不在同一条地铁线上
java subway -b 果酒厂 咸阳路 -map subway.txt -o routine.txt
输出的routine.txt文件内容:
起始站:果酒厂
1号线
本溪路
勤俭道
洪湖里
西站
6号线
复兴路
人民医院
长虹公园
2号线
咸阳路
共经过 9 站
- 9.查询不存在的地铁线路
java subway -a 4号线 -map subway.txt -o station.txt
- 10.查询多换乘路线
java subway -b 天士力 土城 -map subway.txt -o routine.txt
输出的routine.txt文件内容:
起始站:天士力
3号线
宜兴埠
张兴庄
铁东路
北站
中山路
金狮桥
天津站
9号线
大王庄
十一经路
直沽
5号线
下瓦房
1号线
南楼
土城
共经过 14 站
六、项目完成时间统计
PSP 2.1 | Personal Software Process Stages | Time |
---|---|---|
Planning | 计划 | |
· Estimate | · 估计这个任务需要多少时间 | 3weeks |
Development | 开发 | |
· Analysis | · 需求分析 (包括学习新技术) | 2days |
· Design Spec | · 生成设计文档 | 2days |
· Design Review | · 设计复审 (和同事审核设计文档) | 1day |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 1day |
· Design | · 具体设计 | 2days |
· Coding | · 具体编码 | 4days |
· Code Review | · 代码复审 | 2days |
· Test | · 测试(自我测试,修改代码,提交修改) | 2day |
Reporting | 报告 | |
· Test Report | · 测试报告 | 1day |
· Size Measurement | · 计算工作量 | 1day |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 2days |
合计 | 20days |
七.总结
这是我第一用java编写数据结构的算法,也是我第一次用数据结构做一个有实际意义的小项目。通过本次项目,我复习回顾了java,并且对其应用有了更深的理解。同时,熟悉了idea的使用,熟悉了结对编程方式,感受到了结对编程模式的优势。
在本次天津地铁路径的规划中,只将换乘站和起点终点作为需要计算的结点,从而免去了计算所有站点所带来的时间负杂度。但是,在开发过程中,也遇到了不少麻烦,比如说结对编程过程中如何高效沟通,提高我们小队的开发效率,比如在测试过程中如何保证测试用例足够全面,以确保我们的程序达到了预期需求等等。但是后来通过我们小组的不断沟通与上网查询、学习,这些问题都比较理想的解决了。
总之,通过本次项目实验,让我体会到了作为一个程序员,理论和实践相结合的重要性,同时还要学会不断学习,细心观察,强化程序的健壮性和实用性。
github代码地址:https://github.com/Wanghongyu156/ck