地列路线最短路径——项目实现
主要内容
提供如下格式的SubwayInfo.txt,包含北京地铁的线路及站点,实现以下功能:
线路1 站名1 站名2 ... 站名n
线路2 站名1 站名2 ... 站名n
线路n 站名1 站名2 ... 站名n
1.实现任意两站最短乘坐路线的查询,能够显示换乘路线和站点,计算总距离
2.支持查询全部路线、单条路线所包含的各个站点
3.能够具有较好的交互性,给出有效的操作提示,用户输入错误时应该有相应的提示信息
代码及文件见此链接:https://github.com/immajm/ShortestSubwayRoute.git
实现语言
Java
实现算法
-
存储结构:邻接表。由于地铁站点有上百个,而每个站点周围的相关站点只有1-5个不等,如果用邻接矩阵存储图的内容,将会是稀疏矩阵,空间复杂度是O(N²),并不理想。因此选用邻接表存储图的内容,空间复杂度是O(N+E),在处理的效率上有极大的优化。
-
算法:
本算法旨在解决两点之间最短路径的问题,通常可以使用Dijkstra,Floyd等。Floyd算法可以解决任意两点之间的最短路径但时间复杂度为O(N3),在本情况中是大材小用了;Dijkstra算法可以根据路径成本的不同,有效解决单源最短路径的问题,然而在本例中,为简化问题,我们将最短路径问题转化为最少地铁站数问题,也就意味着所有站点之间的距离是相等的,Dijkstra算法退化为BFS算法。故最后采用BFS进行路径查询。
类职责划分(相关类的功能描述)
1.线路类,包括线名、子站名,用来存储线路信息
public class BeanLine
-
private String LineName
//线名 -
private ArrayList<BeanStation> SubStation=new ArrayList<BeanStation>()
//子站名
2.站点类,记录站点信息,包括周围节点,从属线路,访问状态和上一节点
public class BeanStation
-
private String StationName
//站名 -
private ArrayList<String> BelongsToLine= new ArrayList<String>()
//所属线名,用于判断是否需要地铁换乘 -
private ArrayList<BeanStation> NeighborStation =new ArrayList<BeanStation>()
//邻接站点,相当于创建邻接表 -
private int isVisited=0
//是否已被访问,防止搜索产生死循环 -
private String parent=null
//上一站点,方便回溯找到起点站
3.文件读取类,方便数据存储和结构
public class ReadFile
URL path
//保存类文件的系统存储路径
考虑到工程项目的移值,SubwayInfo.txt
在不同pc的位置会不同,如果还是在FileReader
中用绝对路径就会出错,所以这里不同寻常地使用 java.net.URL
中的.class.getClassLoader().getResource("")
方法,实现并输出它的相对路径,能保证类文件夹下有相关文件即可操作,增加了可移植性。
4.主运行类,在此类中包含了多个重要部分
public class SubwayTest
private Map<String,BeanStation> StationSet=new HashMap<String, BeanStation>()
//建立站点名和战点类的映射关系private ArrayList<BeanLine> LineSet=new ArrayList<BeanLine>()
//存储线路类的信息public void initTest()
//初始化,包括读取文件,并按所需格式进行存储public void startPlaying()
//交互部分,开始查询public void startPlaying()
//开始交互void LineSearch(String LineName)
//按名字搜索该线路下的站点void showAllLines()
//显示所有站点int Check(String start,String end)
//查看起点终点是否存在或者重合void SearchRoute(Map<String,BeanStation> StationSet,String start,String end)
//搜索算法private void showRoute(String end)
//显示路径void print(ArrayList<String> path)
//按照要求的格式输出public String findSameLine(String station1,String station2)
//找到重合的线路名,确定本站位置public boolean isChange(String station1,String station2)
//判断是否有换乘
核心代码(所有类的代码标注)
1.写入文件的类
public class ReadFile {
public ArrayList<String> getFlieData(){
//考虑到工程项目的移值,SubwayInfo.txt在不同pc的位置会不同
//如果还是在FileReader中用绝对路径就会出错
//所以这里选用它的相对路径
URL path = SubwayTest.class.getClassLoader().getResource("");
String proFilePath = path.toString().substring(6);//取第6个字符开始的字符串
String[] str=proFilePath.split("/");
StringBuilder realPath=new StringBuilder();
for(int i=0;i<str.length-3;i++){
String a=str[i];
realPath.append(a+'/');
}
ArrayList<String> res=new ArrayList<String>();
proFilePath = realPath.toString() + "src/SubwayInfo.txt";
try {
Reader reader= new FileReader(proFilePath);
BufferedReader br = new BufferedReader(reader);
String tem = "";
while ((tem = br.readLine()) != null)
{
res.add(tem);
}
}catch (IOException e){
e.printStackTrace();
}
return res;//返回地址为在类文件中txt文件的位置
}
}
2.站点结构的定义
public class BeanStation {
private String StationName;//站名
private ArrayList<String> BelongsToLine= new ArrayList<String>();//所属线名
private ArrayList<BeanStation> NeighborStation =new ArrayList<BeanStation>();//邻接站点,相当于创建邻接表
private int isVisited=0;//是否被访问
private String parent=null;//上一站点
public String getStationName() {
return StationName;
}
public void setStationName(String stationName) {
StationName = stationName;
}
public ArrayList<String> getBelongsToLine() {
return BelongsToLine;
}
public void addBelongsToLine(String lineName) {
BelongsToLine.add(lineName);
}
public void setBelongsToLine(ArrayList<String> belongsToLine) {
BelongsToLine = belongsToLine;
}
public ArrayList<BeanStation> getNeighborStation() {
return NeighborStation;
}
public void setNeighborStation(ArrayList<BeanStation> neighborStation) {
NeighborStation = neighborStation;
}
public int getIsVisited() {
return isVisited;
}
public void setIsVisited(int isVisited) {
this.isVisited = isVisited;
}
public String getParent() {
return parent;
}
public void setParent(String parent) {
this.parent = parent;
}
}
3.线路结构的定义
public class BeanLine{
private String LineName;//线名
private ArrayList<BeanStation> SubStation=new ArrayList<BeanStation>();//子站名
public BeanLine(String tem){//存储线路信息
String[] str=tem.split(" ");
LineName=str[0];
for(int i=1;i<str.length;i++){
BeanStation station=new BeanStation();
station.setStationName(str[i]);
SubStation.add(station);
}
}
public String getLineName() {
return LineName;
}
public void setLineName(String lineName) {
LineName = lineName;
}
public ArrayList<BeanStation> getSubStation() {
return SubStation;
}
public void setSubStation(ArrayList<BeanStation> subStation) {
SubStation = subStation;
}
}
4.在主函数中,导入数据作相应处理,开始交互过程
public class SubwayTest {
private Map<String,BeanStation> StationSet=new HashMap<String, BeanStation>();
private ArrayList<BeanLine> LineSet=new ArrayList<BeanLine>();
public static void main(String[] args) throws IOException {
SubwayTest test=new SubwayTest();
test.initTest(); //读入并按需求存储信息
test.startPlaying(); //开始交互
}
}
5.初始化过程读入并按需求存储信息,建成线路的List和具有映射关系和邻接表的站点表
public void initTest() throws IOException {//读入并按需求存储信息
ReadFile file=new ReadFile();//读入文件内信息
ArrayList<String> fileTxt=file.getFlieData();
for(String temp:fileTxt){
//利用Line类的构造函数建Line类,并加到LineSet里
BeanLine line=new BeanLine(temp);
LineSet.add(line);
}
//LineSet已经存好 现在读入StationSet
//两重循环,第一重遍历所有的线路,第二重循环遍历每一条线路上的站点
for(BeanLine line:LineSet) {
for (int i = 0; i < line.getSubStation().size(); i++) {
//检查是否已经存在,将该站点存入(Map)StationSet中
if(!StationSet.containsKey(line.getSubStation().get(i).getStationName()))
StationSet.put(line.getSubStation().get(i).getStationName(),line.getSubStation().get(i));
//更新信息:将该站点前后没有放入NeighborStation的站点加入
//直接修改(map)StationSet,或者使用getOrDefault()从map中取出后更新,本次使用直接修改
//加入前一站点
if (i > 0) {
BeanStation front_neighbor = new BeanStation();
front_neighbor=line.getSubStation().get(i-1);
if(!StationSet.get(line.getSubStation().get(i).getStationName()).getNeighborStation().contains(front_neighbor)){
StationSet.get(line.getSubStation().get(i).getStationName()).getNeighborStation().add(front_neighbor);
}
}
//加入后一站点
if (i < line.getSubStation().size() - 1) {
BeanStation next_neighbor = new BeanStation();
next_neighbor=line.getSubStation().get(i+1);
if(!StationSet.get(line.getSubStation().get(i).getStationName()).getNeighborStation().contains(next_neighbor)){
StationSet.get(line.getSubStation().get(i).getStationName()).getNeighborStation().add(next_neighbor);
}
}
//记录所属线路
String lineName = line.getLineName();
StationSet.get(line.getSubStation().get(i).getStationName()).getBelongsToLine().add(lineName);
}
}
}
6.循环式的交互过程,能够给出足够的操作提示和反馈
public void startPlaying(){//开始交互
System.out.println("************************************************************");
System.out.println(" {欢迎使用SuMa识途} ");
System.out.println("************************************************************");
System.out.println(" ");
System.out.println("1 :查询地铁线路(-a显示全部) ");
System.out.println("2 :查询最短路径 ");
System.out.println("0 :退出查询 ");
System.out.println();
Scanner sc =new Scanner(System.in);
while(true){
String choice=sc.next();
if(choice.equals("1")){
System.out.println("请输入线路名称:");
LineSearch(sc.next());
}
else if(choice.equals("1-a")){
showAllLines();
}
else if(choice.equals("2")){
System.out.println("请输入#起点站#和#终点站#");
String start=sc.next();
String end=sc.next();
if(Check(start,end)==1) {
SearchRoute(StationSet,start,end);//开始寻路
}
}
else if(choice.equals("0")){
System.out.println("退出查询");
break;
}
else{
System.out.println("只有俩功能,配合点!!!(´థ౪థ)σ");
System.out.println(" ");
}
System.out.println("请重新选择");
System.out.println("1 :查询地铁线路 ");
System.out.println("2 :查询最短路径 ");
System.out.println("0 :退出查询 ");
}
System.out.println("************************************************************");
System.out.println(" 查询结束! ");
System.out.println("************************************************************");
}
其中包含的查找全部线路、单条线路、检验起点终点的部分如下
void LineSearch(String LineName){
int isExist=0;
for(BeanLine line:LineSet){
if(LineName.equals(line.getLineName())){
System.out.println("您好,"+line.getLineName()+"包含以下站点:");
for(int i=0;i<line.getSubStation().size();i++){
System.out.print(line.getSubStation().get(i).getStationName()+" ");
}
System.out.println(" ");
isExist=1;
break;
}
}
if(isExist==0){
System.out.println("该线路不存在");
System.out.println(" ");
}
}
void showAllLines(){
for(BeanLine line:LineSet){
System.out.println(line.getLineName());
for(int i=0;i<line.getSubStation().size();i++){
System.out.print(line.getSubStation().get(i).getStationName()+" ");
}
System.out.println(" ");
}
}
int Check(String start,String end){
int isCorrect=1;
if(!StationSet.containsKey(start)){
isCorrect=0;
System.out.println("起点不存在");
}
if(!StationSet.containsKey(end)){
isCorrect=0;
System.out.println("终点不存在");
}
if(start.equals(end)){
System.out.println("您已到达终点");
isCorrect=0;
}
return isCorrect;
}
7.寻求路径算法部分,并按照规定格式输出,并对站点信息做出选择判断
void SearchRoute(Map<String,BeanStation> StationSet,String start,String end) {
Queue<BeanStation> queue= new LinkedList<>();
String next_start=null;
int neighbor_size=0;
String temp=null;
int isfind=0;
//等距图中,单源最短距离计算,Dijkstra退化为BFS
queue.add(StationSet.get(start));//将起点放入队列
StationSet.get(start).setIsVisited(1);//起点已访问
while(!queue.isEmpty()){
next_start=queue.peek().getStationName();
neighbor_size=StationSet.get(next_start).getNeighborStation().size();
for(int i=0;i<neighbor_size;i++){
temp=StationSet.get(next_start).getNeighborStation().get(i).getStationName();
//找到终点
if(temp.equals(end)){
StationSet.get(temp).setParent(next_start);
isfind=1;
break;
}
else if(StationSet.get(temp).getIsVisited()==0){//若未被访问过
StationSet.get(temp).setParent(next_start);//设父亲节点
StationSet.get(temp).setIsVisited(1);//该点被访问
queue.add(StationSet.get(temp));//加入队列
//必须先设父节点、改边isVisited,再放入queue,否则节点未更新,无限循环
//应该找到map对应键值更新父节点和visit,而不是在map对应键值的邻居节点的父节点和visit进行更新,故增加temp
}
}
if(isfind==1) break;//设置判断标志,否则继续while循环
queue.poll();
}
showRoute(end);
}
算法中的显示路径经过、距离,调整格式显示换乘,判断前后线路情况的部分如下
private void showRoute(String end) {
int count=0;
//需要判断相隔一个站点的两个站是否在一条线路上,用list比stack输出方便
ArrayList<String> path= new ArrayList<>();
BeanStation station=new BeanStation();
station=StationSet.get(end);
//回溯父节点
while(station.getParent()!=null){
path.add(station.getStationName());
station=StationSet.get(station.getParent());
count++;
}
path.add(station.getStationName());
System.out.println("至少需要乘坐"+count+"站哦");
//将站点按固定格式输出,此时path是反向存放的
print(path);
}
void print(ArrayList<String> path){
System.out.println(" 一开始位于:"+findSameLine(path.get(path.size()-1),path.get(path.size()-2)));
System.out.print(" "+path.get(path.size()-1)+" ");
int i;
for(i=path.size()-1;i>2;i--){
System.out.print("-> "+path.get(i-1)+" ");
//判断两站有无换乘
if(isChange(path.get(i),path.get(i-2))){
//如果换乘了 输出换乘信息 并重启一行输出
System.out.println();
System.out.println(" "+findSameLine(path.get(i),path.get(i-1))+" --> "+findSameLine(path.get(i-1),path.get(i-2)));
System.out.print(" "+path.get(i-1)+" ");
}
}
System.out.println("-> "+path.get(i-2)+" ");
System.out.println(" 最终位于:"+findSameLine(path.get(i),path.get(i-1)));
}
public String findSameLine(String station1,String station2){
for(String name:StationSet.get(station1).getBelongsToLine()){
if(StationSet.get(station2).getBelongsToLine().contains(name))
return name;
}
return "奇怪,它们肯定线路相同呀";
}
public boolean isChange(String station1,String station2){
for(String name:StationSet.get(station1).getBelongsToLine()){
if(StationSet.get(station2).getBelongsToLine().contains(name))
return false;
}
return true;
}
测试用例 (输入输出结果截图)
1.当查询线路不存在时
2.查询单线
3.查询所有
4.当起点和终点有错误时,分别做出判断
5.当起与终点重合
6.需要查询节点正确时,写清楚换乘过程和每一条线的所经站点
7.输入非规定查询命令时
8.结束查询
总结
1.此次实践,让我对与软件开发的过程有了新的认识和体验,首先要知道项目需求有哪些,然后对整体结构做出规划,如数据结构存储方式、算法实现方面,同时也要不断查询测试代码模块的正确性,修改错误,在此基础上最好能保证代码的强健性。
2.不足之处还是有很多。在结构和接口的设计上有一些出入,需要反复删改;在算法设计上,给出了结果较优的一条路线,并显示了总距离和换乘过程,而不是全部最短路径,但实际的地图软件往往会给出多条以供选择。鉴于整个项目如果能够美观的可视化搜索过程会增加使用的流畅性,我构想能够在规划路径的基础上,添加如高德地图那样的多条路线选择,并且能够在地图上将路径显示,同时采集更多数据,如将站点之间的距离、乘坐时间、车票花费等内容加上。
3.学会了通过写博客来记录并完善整个过程,也学习了git的相关使用,后续会继续完善。