地列路線最短路徑——專案實現
主要內容
提供如下格式的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的相關使用,後續會繼續完善。