immajm

 

地列路线最短路径——项目实现

主要内容

提供如下格式的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的相关使用,后续会继续完善。

posted on 2020-11-05 12:27  immajm  阅读(244)  评论(3编辑  收藏  举报

导航