个人项目-地铁出行路线规划(Java代码实现)
基于上一篇博客的规划,通过几天的努力,完成了该程序的实现。
源代码传送门
https://github.com/bunnywwwwyj/BeiJingSubway/tree/master/BeiJingSubway
问题回顾
为了实现地铁出行路线的规划,需要设计一个能够计算地铁线路最短路径的程序。
这里以北京城市轨道交通线网图为例来设计:
线路概览
文件输入输出的格式
-
subway.txt 的存储格式
1号线: 苹果园 古城 八角游乐园 ... 2号线: 西直门#4#13 积水潭 鼓楼大街#8 ... ... S1线: 金安桥#6 四道桥 桥户营 ...
线路名后用“:”与其后站点间隔。
站点间用“ ”间隔。
若某站为换乘站,在其后用“#数字”的方式,表示在当前线路上的该站点可换乘到“#”后数字序号所指的线路上,如2号线的西直门可以换乘到4号线以及13号线。
-
station.txt 的输出格式
以查询一号线为例,得到的输出如下:
1号线所经过的站点为: 苹果园 古城 八角游乐园 八宝山 玉泉路 五棵松 万寿路 公主坟(可换乘 10号线)军事博物馆(可换乘 9号线)木樨地 南礼士路 复兴门(可换乘 2号线)西单(可换乘 4号线)天安门西 天安门东 王府井 东单(可换乘 5号线)建国门(可换乘 2号线)永安里 国贸(可换乘 10号线)大望路(可换乘 14号线东)四惠(可换乘 八通线)四惠东(可换乘 八通线)
在每个换乘站后面的"()"中显示可换乘的所有线路。
-
routine.txt 的输出格式
以查询从中关村到西土城为例,得到的输出如下:
共经过5站 中关村 海淀黄庄 10号线 知春里 知春路 西土城
首先输出所经过的站点数,接下来输出所经过的所有站点(包括出发与目的站点),若需要换乘会在该站下提示换乘的线路。
功能实现
-
功能1
-
得到地铁线路图的信息
一个调用应用程序的示例如下:
java Main -map subway.txt
-
-
功能2
-
查询指定地铁线经过的站点
一个调用应用程序的示例如下:
java Main -a 1号线 -map subway.txt -o station.txt
-
-
功能3
-
计算从出发到目的站点之间的最短路线并输出经过的站点的个数和路径
一个调用应用程序的示例如下:
java Main -b 中关村 西土城 -map subway.txt -o routine.txt
-
代码分析
-
SubwayLine.java
- 定义了一个地铁线路的类,包括线路名称以及该线路上的所有站点。
public String name; public List<Station> stations = new ArrayList<>();
- 定义了一个地铁线路的类,包括线路名称以及该线路上的所有站点。
-
Station.java
- 定义了一个站点的类,包括站点名称、所在线路、是否换乘、可换乘的线路以及其相邻站点。
public String name; public String line; public Boolean isTransferStation; public List<String> lines = new ArrayList<>(); public List<Station> adjacentStation = new ArrayList<>();
- 定义了一个站点的类,包括站点名称、所在线路、是否换乘、可换乘的线路以及其相邻站点。
-
Result.java
- 定义了一个结果的类,包括起点站、终点站、两站点之间的最短距离以及该线路上所经过的站点。
private Station startStation; private Station endStation; private int distance; private List<Station> passStations = new ArrayList<>();
- 定义了一个结果的类,包括起点站、终点站、两站点之间的最短距离以及该线路上所经过的站点。
-
Dijkstra.java
- 定义了一个计算路径的类,在计算最短路径的同时并记录所经过的所有站点,这里用到了Dijkstra算法。
// 计算结果 public static Result calculate(Station startStation, Station endStation) { if (!visitedStations.contains(startStation)) { visitedStations.add(startStation); } if (result.isEmpty()) { List<Station> adjacentStation = getLinkStations(startStation); for (Station s : adjacentStation) { Result r = new Result(); r.setStartStation(startStation); r.setEndStation(s); r.setDistance(1); // 默认各站点间距离为1 r.getPassStations().add(s); result.put(s, r); } } Station parent = getNextStation(); if (parent == null) { Result r = new Result(); r.setStartStation(startStation); r.setEndStation(endStation); r.setDistance(0); return result.put(endStation, r); } if (parent.equals(endStation)) { return result.get(parent); } List<Station> childLinkStations = getLinkStations(parent); for (Station s : childLinkStations) { if (visitedStations.contains(s)) continue; int distance = 0; if (parent.getName().equals(s.getName())) distance = 0; int parentDistance = result.get(parent).getDistance(); distance = parentDistance + 1; List<Station> parentPassStations = result.get(parent).getPassStations(); Result childResult = result.get(s); if (childResult != null) { if (childResult.getDistance() > distance) { childResult.setDistance(distance); childResult.getPassStations().clear(); childResult.getPassStations().addAll(parentPassStations); childResult.getPassStations().add(s); } } else { childResult = new Result(); childResult.setDistance(distance); childResult.setStartStation(startStation); childResult.setEndStation(s); childResult.getPassStations().addAll(parentPassStations); childResult.getPassStations().add(s); } result.put(s, childResult); } visitedStations.add(parent); return calculate(startStation, endStation); }
// 获取所有相邻站点 public static List<Station> getLinkStations(Station station) { List<Station> linkedStaions = new ArrayList<>(); for (SubwayLine line : Main.lines) { for (int i = 0; i < line.stations.size(); i++) { if (station.equals(line.stations.get(i))) { if (i == 0) { linkedStaions.add(line.stations.get(i + 1)); } else if (i == (line.stations.size() - 1)) { linkedStaions.add(line.stations.get(i - 1)); } else { linkedStaions.add(line.stations.get(i + 1)); linkedStaions.add(line.stations.get(i - 1)); } } } } return linkedStaions; }
// 计算下一个需要分析的点 public static Station getNextStation() { int min = 1000000; Station station = null; Set<Station> stations = result.keySet(); for (Station s : stations) { if (visitedStations.contains(s)) continue; Result r = result.get(s); if (r.getDistance() < min) { min = r.getDistance(); station = r.getEndStation(); } } return station; }
- 定义了一个计算路径的类,在计算最短路径的同时并记录所经过的所有站点,这里用到了Dijkstra算法。
-
Main.java
- 定义了一个主函数的类,用来运行整个程序。
// 解析 subway.txt 获取地铁线路图信息 public static void subwayMap() { String f = "subway.txt"; try (FileReader r = new FileReader(f); BufferedReader br = new BufferedReader(r) ) { String line; while ((line = br.readLine()) != null) { String[] tokens = line.split(": ", 2); // 分离线路及站点 SubwayLine sl = new SubwayLine(); sl.name = tokens[0]; String[] tokens2 = tokens[1].split(" "); // 分离各个站点 for(String s: tokens2) { Boolean isTransfer = s.contains("#"); Station station = new Station(); if(isTransfer) { station.isTransferStation = true; String[] tokens3 = s.split("#"); // 分离换乘站信息 station.name = tokens3[0]; for(int i = 1; i < tokens3.length; i++) { if(tokens3[i].equals("1")) station.lines.add("1号线"); else if(tokens3[i].equals("2")) station.lines.add("2号线"); else if(tokens3[i].equals("4")) station.lines.add("4号线"); else if(tokens3[i].equals("5")) station.lines.add("5号线"); else if(tokens3[i].equals("6")) station.lines.add("6号线"); else if(tokens3[i].equals("7")) station.lines.add("7号线"); else if(tokens3[i].equals("8北")) station.lines.add("8号线北"); else if(tokens3[i].equals("8南")) station.lines.add("8号线南"); else if(tokens3[i].equals("9")) station.lines.add("9号线"); else if(tokens3[i].equals("10")) station.lines.add("10号线"); else if(tokens3[i].equals("13")) station.lines.add("13号线"); else if(tokens3[i].equals("14西")) station.lines.add("14号线西"); else if(tokens3[i].equals("14东")) station.lines.add("14号线东"); else if(tokens3[i].equals("15")) station.lines.add("15号线"); else if(tokens3[i].equals("16")) station.lines.add("16号线"); else if(tokens3[i].equals("八通")) station.lines.add("八通线"); else if(tokens3[i].equals("房山")) station.lines.add("房山线"); else if(tokens3[i].equals("昌平")) station.lines.add("昌平线"); else if(tokens3[i].equals("亦庄")) station.lines.add("亦庄线"); else if(tokens3[i].equals("燕房")) station.lines.add("燕房线"); else if(tokens3[i].equals("S1")) station.lines.add("S1线"); else if(tokens3[i].equals("西郊")) station.lines.add("西郊线"); else if(tokens3[i].equals("首都机场")) station.lines.add("首都机场线"); } station.line = tokens[0]; stations.add(station); sl.stations.add(station); } else { if(s.contains("!")) continue; station.isTransferStation = false; station.name = s; station.line = tokens[0]; stations.add(station); sl.stations.add(station); } } lines.add(sl); } } catch (IOException e) { e.printStackTrace(); } }
public static String getLineName(Station station) { for (SubwayLine s : lines) { for(Station st : s.stations) { if(st.name.equals(station.name)) return st.line; } } return ""; }
public static void main(String[] args) throws IOException { switch (args[0]) { case "-map": if (args.length == 2) { subwayMap(); System.out.println("数据读取成功 ~"); } else { System.out.println("输入格式有误,请重新输入。"); break; } break; case "-a": subwayMap(); int flag1 = 0; //判断该线路是否在所有线路中 for(SubwayLine sl : lines) { if(sl.name.equals(args[1])) { flag1 = 1; break; } } if(flag1 == 0) { System.out.println("该线路不存在,请重新输入。"); break; } if (args.length == 6 && args[4].equals("-o")) { for(int i = 0; i < lines.size(); i++) { if(args[1].equals(lines.get(i).name)) { String str = lines.get(i).name+"所经过的站点为: "; for(Station s : lines.get(i).stations) { if(s.isTransferStation) { str += s.name+"(可换乘"; for(String st: s.lines) { str += " "+st; } str +=") "; } else { str += s.name+" "; } } byte[] text = str.getBytes(); File file = new File("/Users/bunnywwwwyj/Desktop/BeiJingSubway", "station.txt"); if(file.exists() == true) file.delete(); file.createNewFile(); FileOutputStream out = new FileOutputStream(file); out.write(text); out.close(); break; } } String f2 = "/Users/bunnywwwwyj/Desktop/BeiJingSubway/station.txt"; try (FileReader r2 = new FileReader(f2); BufferedReader br2 = new BufferedReader(r2) ) { String line2; while ((line2 = br2.readLine()) != null) { System.out.println(line2); } } catch (IOException e) { e.printStackTrace(); } } else { System.out.println("输入格式有误,请重新输入。"); break; } break; case "-b": subwayMap(); if(args[1].equals(args[2])) { System.out.println("起点站与终点站不能相同"); break; } int flag2 = 0; //判断起点站是否在所有站点中 int flag3 = 0; //判断终点站是否在所有站点中 for(Station s : stations) { if(s.name.equals(args[1])) flag2 = 1; if(s.name.equals(args[2])) flag3 = 1; if(flag2 == 1 && flag3 == 1) break; } if(flag2 == 0) { System.out.println("该起点站不存在,请重新输入。"); break; } if(flag3 == 0) { System.out.println("该终点站不存在,请重新输入。"); break; } if (args.length == 7 && args[3].equals("-map") && args[5].equals("-o")) { Result r = Dijkstra.calculate(new Station(args[1]), new Station(args[2])); int total = r.getPassStations().size()+1; String str = "共经过"+total+"站\n"; str += r.getStartStation().name+"\n"; String line = getLineName(r.getStartStation()); for (int i = 0; i < r.getPassStations().size(); i++) { if(r.getPassStations().get(i).isTransferStation) { str += r.getPassStations().get(i).name+"\n"; if (i != r.getPassStations().size()-1 && !line.equals(getLineName(r.getPassStations().get(i+1)))) { str += "换乘"+getLineName(r.getPassStations().get(i+1))+"\n"; line = getLineName(r.getPassStations().get(i+1)); } } else str += r.getPassStations().get(i).name+"\n"; } byte[] text = str.getBytes(); File file = new File("/Users/bunnywwwwyj/Desktop/BeiJingSubway", "routine.txt"); if(file.exists() == true) file.delete(); file.createNewFile(); FileOutputStream out = new FileOutputStream(file); out.write(text); out.close(); String f3 = "/Users/bunnywwwwyj/Desktop/BeiJingSubway/routine.txt"; try (FileReader r3 = new FileReader(f3); BufferedReader br3 = new BufferedReader(r3) ) { String line3; while ((line3 = br3.readLine()) != null) { System.out.println(line3); } } catch (IOException e) { e.printStackTrace(); } } else { System.out.println("输入格式有误,请重新输入。"); break; } break; default: System.out.println("输入格式有误,请重新输入。"); }
- 定义了一个主函数的类,用来运行整个程序。
在这部分的实现过程中遇到的很多的问题,比如实现功能3的时候,一开始总是会遇到空指针的问题,因为新创建的站点只有名称,没有其他的任何信息,因此在调用它所在线路的属性时会报错,最后通过新建一个根据站点名称获取它所对应线路的函数getLineName()
解决了这个问题。
在main函数中,通过switch-case的方式根据所代入的参数进行相应的功能实现,并进行了一些简单的输入格式报错处理。
程序运行结果会写到对应的txt文件中,每次运行时都会清空上一次的输出结果,以免产生混乱。为了更直观地观察运行结果,在控制台也进行了相应的输出。
测试样例
-
查询指定地铁线经过的站点
输入1如下:
java Main -a 14号线西 -map subway.txt -o station.txt
输出1如下:
14号线西所经过的站点为: 张郭庄 园博园 大瓦窑 郭庄子 大井 七里庄(可换乘 9号线) 西局(可换乘 10号线)
输入2如下:
java Main -a 14号线东 -map subway.txt -o station.txt
输出2如下:
14号线东所经过的站点为: 北京南站(可换乘 4号线) 永定门外(可换乘 8号线南) 景泰 蒲黄榆(可换乘 5号线) 方庄 十里河(可换乘 10号线) 北工大西门 平乐园 九龙山(可换乘 7号线) 大望路(可换乘 1号线) 金台路(可换乘 6号线) 朝阳公园 枣营 东风北桥 将台 望京南 阜通 望京(可换乘 15号线) 东湖渠 来广营 善各庄
-
无换乘
输入如下:
java Main -b 温阳路 马连洼 -map subway.txt -o routine.txt
输出如下:
共经过7站 温阳路 稻香湖路 屯佃 永丰 永丰南 西北旺 马连洼
-
单次换乘
输入如下:
java Main -b 中关村 西土城 -map subway.txt -o routine.txt
输出如下:
共经过5站 中关村 海淀黄庄 10号线 知春里 知春路 西土城
-
多次换乘
输入如下:
java Main -b 北沙滩 大钟寺 -map subway.txt -o routine.txt
输出如下:
共经过9站 北沙滩 奥林匹克公园 换乘8号线北 奥体中心 北土城 换乘10号线 健德门 牡丹园 西土城 知春路 换乘13号线 大钟寺
-
超长路线
输入如下:
java Main -b 丰台东大街 望京东 -map subway.txt -o routine.txt
输出如下:
共经过22站 丰台东大街 七里庄 六里桥 换乘10号线 莲花桥 公主坟 换乘1号线 军事博物馆 换乘9号线 白堆子 白石桥南 换乘4号线大兴线 国家图书馆 动物园 西直门 换乘2号线 积水潭 鼓楼大街 换乘8号线北 安德里北街 安华桥 北土城 换乘10号线 安贞门 惠新西街南口 芍药居 换乘13号线 望京西 换乘14号线东 望京 换乘15号线 望京东
-
换乘节点-换乘节点
输入如下:
java Main -b 西直门 雍和宫 -map subway.txt -o routine.txt
输出如下:
共经过5站 西直门 积水潭 鼓楼大街 安定门 雍和宫
报错处理
-
起点站与终点站相同
输入如下:
java Main -b 西单 西单 -map subway.txt -o routine.txt
输出如下:
起点站与终点站不能相同。
-
不存在的线路
java Main -a 18号线 -map subway.txt -o station.txt
输出如下:
该线路不存在,请重新输入。
-
不存在的站点
输入1如下:
java Main -b 西湖 西单 -map subway.txt -o routine.txt
输出1如下:
该起点站不存在,请重新输入。
输入2如下:
java Main -b 西单 西湖 -map subway.txt -o routine.txt
输出2如下:
该终点站不存在,请重新输入。
最后
由于对Dijkstra算法的写法不是很熟悉,看了很多的文档,最后找到一篇比较好的blog作为参考。
在最初定义站点类的时候有考虑到统计换乘次数的问题,最后由于时间比较仓促,未能实现。
总的来讲,感觉自己的能力还需继续提升,还有很多的欠缺。