1. 程式人生 > 實用技巧 >地列路線最短路徑——專案實現

地列路線最短路徑——專案實現

主要內容

提供如下格式的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的相關使用,後續會繼續完善。