資料結構-基於鄰接表實現圖的遍歷視覺化及使用Floyd、Dijkstra演算法求解最短路徑(JavaScript實現)
阿新 • • 發佈:2019-01-05
使用 JavaScript 基於鄰接表實現了圖的深度、廣度遍歷,以及 Floyd、Dijkstra 演算法求解最短路徑。
另外使用 SVG 實現圖的遍歷視覺化。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>圖論</title> <style type="text/css"> .container{ width: 600px; height: 800px; margin: 0 auto; } .node{ width: 100px; min-height: 100px; } .circle{ margin: 0 auto; width: 50px; height: 50px; } </style> </head> <body> <div class="container"> <div class="control"> 選擇開始遍歷的結點: <select id="node"> <option>v0</option> </select> 選擇遍歷方式: <select id="select"> <option>深度優先遍歷</option> <option>廣度優先遍歷</option> </select> <input type="button" name="click" value="開始遍歷" id="button"> </div> <svg id="svg" width="600" height="600"></svg> </div> <script type="text/javascript"> (function(){ //存放圖結點的陣列 var graphVertex = ["v0","v1","v2","v3","v4","v5","v6"]; //存放圖邊的陣列 var graphAdge = [ { start:"v0", end: "v2", weight: 2 }, { start:"v0", end: "v5", weight: 8 }, { start:"v3", end: "v2", weight: 1 }, { start:"v3", end: "v5", weight: 3 }, { start:"v4", end: "v1", weight: 6 }, { start:"v5", end: "v4", weight: 8 }, { start:"v1", end: "v2", weight: 8 }, { start:"v1", end: "v6", weight: 8 } ]; //圖的建構函式,n:結點數量 , e: 邊的數量 var GraphList = function(graphVertex,graphAdge){ var vertex = [];//存放圖中頂點的陣列 var adjList = [];//存放圖中邊的鄰接矩陣 var n = graphVertex.length, e = graphAdge.length; //初始化鄰接表 var initArr = function(){ //深拷貝,不改變輸入陣列 vertex = JSON.parse(JSON.stringify(graphVertex)); //頂點和邊的數量 var n = graphVertex.length, e = graphAdge.length; //初始化arc陣列 for(let i=0;i<n;i++){ adjList[i] = { "vertex": vertex[i], "firstEdge": null }; } //console.log(adjList); //給arc鄰接矩陣填充邊的權值 for(let i=0;i<e;i++){ var start = graphVertex.indexOf(graphAdge[i].start); var end = graphVertex.indexOf(graphAdge[i].end); if(start===-1||end===-1){ throw Error("邊陣列中有結點不在結點陣列中"); return; } var s = { "node": end, "weight": graphAdge[i].weight, "next": adjList[start].firstEdge } adjList[start].firstEdge = s; //無向圖需要下面這兩個,有向圖不需要 var t = { "node": start, "weight": graphAdge[i].weight, "next": adjList[end].firstEdge } adjList[end].firstEdge = t; } //console.log(adjList); }; //立即執行函式,初始化 (function(){ initArr(); })(); //圖的深度優先遍歷 var dfsTraverse = function(v){ //輸入檢測 if(vertex.indexOf(v)===-1){ throw Error("廣度優先遍歷輸入結點不在結點陣列中"); return; } var result = [],visited = []; var stack = []; stack.push(v); result.push(v); visited[vertex.indexOf(v)] = true; while(stack.length!==0){ let top = stack.pop(); if(visited[vertex.indexOf(top)] !== true){ result.push(top); visited[vertex.indexOf(top)] = true; } let row = vertex.indexOf(top); var p = adjList[row].firstEdge; while(p!==null){ let j = p.node; if(visited[j] !== true){ stack.push(vertex[j]); //visited[j] = true; } p = p.next; } if(result.length>100){ alert("死迴圈"); } } //console.log(result); return result; } //圖的廣度優先遍歷 var bfsTraverse = function(v){ //輸入檢測 if(vertex.indexOf(v)===-1){ throw Error("廣度優先遍歷輸入結點不在結點陣列中"); return; } //廣度優先遍歷 var queue = [],result = [],visited = []; var pre = 0, tail = 1; queue.push(v); visited[vertex.indexOf(v)] = 1; while(pre !== tail){ let v = queue[pre++]; let row = vertex.indexOf(v); //console.log(row,"----"); if(row===-1){ throw Error("廣度優先遍歷結點不在結點陣列中"); } var p = adjList[row].firstEdge; while(p!==null){ let j = p.node; if(visited[j] === undefined){ queue.push(vertex[p.node]); visited[j] = 1; } p = p.next; } tail = queue.length; if(pre>100){ alert("死迴圈"); } } //console.log(queue); return queue; } //Floyd最短路徑 var floyd = function(){ var dist = [], path = []; //初始化 for(let i=0;i<n;i++){ dist[i] = new Array(n); path[i] = new Array(n); for(let j=0;j<n;j++){ dist[i][j] = Infinity; path[i][j] = []; } let p = adjList[i].firstEdge; let count=0; while(p!=null){ dist[i][p.node] = p.weight; path[i][p.node] = [ vertex[i]]; p = p.next; if(count++>100){ alert("死迴圈"); } } } //console.log(dist,path); //核心程式碼 for(let k=0;k<n;k++){ for(let i=0;i<n;i++){ for(let j=0;j<n;j++){ if(dist[i][k] + dist[k][j] < dist[i][j]){ dist[i][j] = dist[i][k] + dist[k][j]; path[i][j] = path[i][k].concat(path[k][j]); } if(i===j){ dist[i][j] = 0; } } } } //加上終點結點名 for(let i=0;i<n;i++){ for(let j=0;j<n;j++){ if(dist[i][j]!==Infinity&&dist[i][j]!=0){ path[i][j].push(vertex[j]); } } } //console.log(dist,path); var out = { "dist":dist, "path":path } return out; } //Dijkstra var dijkstra = function(v){ var getDist = function(k,i){ var dist = Infinity; var p = adjList[k].firstEdge; let count=0; while(p!=null){ if(i===p.node){ dist = p.weight; } p = p.next; if(count++>100){ alert("死迴圈"); } } return dist; } var index = vertex.indexOf(v); var dist = [], path = []; for(let i=0;i<n;i++){ dist[i] = Infinity; path[i] = []; } let p = adjList[index].firstEdge; let count=0; while(p!=null){ dist[p.node] = p.weight; path[p.node] = [v,vertex[p.node]]; p = p.next; if(count++>100){ alert("死迴圈"); } } //console.log("dist",dist,path); var vis = []; vis[vertex.indexOf(v)] = true; dist[vertex.indexOf(v)] = 0; var num = 1; while(num<n){ let k = 1; for(let i=0;i<n;i++){ if(vis[i]!==true && dist[k] > dist[i]){ k = i; } } vis[k] = true; for(let i=0;i<n;i++){ if(dist[i] > dist[k]+getDist(k,i)){ dist[i] = dist[k]+getDist(k,i); path[i] = path[k].concat([vertex[i]]); } } num++; } //console.log(dist,path); var out = { "dist":dist, "path":path } return out; } return { dfsTraverse: dfsTraverse, bfsTraverse: bfsTraverse, floyd: floyd, dijkstra: dijkstra } } //顯示圖 var showGraph = function(){ var svg = document.getElementById("svg"); var circleStr = "",lineStr = "",textStr = "",arrowStr="";//圓、線和文字的HTML字串 var adge = JSON.parse(JSON.stringify(graphAdge));//層序遍歷生成陣列 var vertex = JSON.parse(JSON.stringify(graphVertex));//層序遍歷生成陣列 var vertexObj = {};//存放所有頂點的物件,鍵為頂點名,值為含有屬性的物件 var width = Number(svg.getAttribute("width"))-50;//畫布寬度 var r = width/2;//半徑 //畫圓和頂點名 for(let i=0;i<vertex.length;i++){ let vertexName = vertex[i], len = vertexName.length; let angle = (i*2*Math.PI/vertex.length); let cx = 0, cy = 0;//當前結點的定位畫素座標 cx = r*(1 + Math.sin(angle) )+25; cy = r*(1 - Math.cos(angle) )+25; let obj = { "cx":cx, "cy":cy }; vertexObj[vertexName] = obj; circleStr += '<circle cx="'+cx+'" cy="'+cy+'" r="20" fill="#9F79EE"/></circle>'; //調整文字縮排 let textcx = len>1?(cx-10):(cx-5); let textcy = cy+6; textStr += '<text x="'+textcx+'" y="'+textcy+'" fill="black">'+vertexName+'</text>'; } //畫線和數字 for(let i =0;i<adge.length;i++){ //如果依然處於當前層,則累加佔用寬度,否則將佔用寬度置零,更新層數 let startcx = 0, startcy = 0,endcx = 0,endcy = 0;//當前結點的定位畫素座標 if(!vertexObj[adge[i].start]||!vertexObj[adge[i].end]){ throw Error("邊陣列中有結點不在結點陣列中"); return; } startcx = vertexObj[adge[i].start].cx; startcy = vertexObj[adge[i].start].cy; endcx = vertexObj[adge[i].end].cx; endcy = vertexObj[adge[i].end].cy; lineStr += '<line x1="'+startcx+'" y1="'+startcy+'" x2="'+endcx+ '" y2="'+endcy+'" style="stroke:#999;stroke-width:2" />'; //計算三角形箭頭的座標和旋轉角 var getTriangle = function(obj){ var x = endcx,y=endcy+20; startcx = obj.startcx; startcy= obj.startcy; endcx= obj.endcx; endcy= obj.endcy; var x = endcx, y = endcy+20; var points = [x-7,y+15,x+7,y+15,x,y].join(",");//一個三角形三點的座標 var angle = 0; //注意這裡的座標系和通常情況下的座標系y軸是相反的,因此 endcy-startcy 要變為startcy-endcy angle =180*Math.acos((startcy-endcy)/Math.sqrt((endcx-startcx)*(endcx-startcx)+ (startcy-endcy)*(startcy-endcy)))/Math.PI; if(endcx-startcx < 0){ angle = 360 - angle; } var out = { "points":points, "angle":angle } return out; } //如果是無向圖隱藏下面計算與畫三角形箭頭的程式碼即可 var obj = { "startcx":startcx, "startcy":startcy, "endcx":endcx, "endcy":endcy } var data = getTriangle(obj); var angle = [data.angle,endcx,endcy].join(","); //畫三角形箭頭 arrowStr += '<polygon points="'+data.points+'" fill:"#171717" transform="rotate('+angle+')"/>'; obj = { "startcx":endcx, "startcy":endcy, "endcx":startcx, "endcy":startcy } data = getTriangle(obj); angle = [data.angle,endcx,endcy].join(","); //畫三角形箭頭 arrowStr += '<polygon points="'+data.points+'" fill:"#171717" transform="rotate('+angle+')"/>'; //調整文字縮排 var textcx = (startcx + endcx)/2; var textcy = (startcy + endcy)/2; textStr += '<text x="'+textcx+'" y="'+textcy+'" fill="#171717">'+adge[i].weight+'</text>'; } svg.innerHTML = lineStr+circleStr+textStr+arrowStr; } showGraph(); var graph = new GraphList(graphVertex,graphAdge); console.log("基於鄰接表實現:"); console.log("深度優先遍歷",graph.dfsTraverse("v0")); console.log("廣度優先遍歷",graph.bfsTraverse("v0")); console.log("Floyd演算法求最短路徑",graph.floyd()); console.log("Dijkstra演算法求最短路徑",graph.dijkstra("v0")); var showGraphTraverse = function(id){ var svg = document.getElementById("svg"); svg.innerHTML = ""; var circleStr = "",lineStr = "",textStr = "",arrowStr="";//圓、線和文字的HTML字串 var adge = JSON.parse(JSON.stringify(graphAdge));//層序遍歷生成陣列 var vertex = JSON.parse(JSON.stringify(graphVertex));//層序遍歷生成陣列 var vertexObj = {};//存放所有頂點的物件,鍵為頂點名,值為含有屬性的物件 var width = Number(svg.getAttribute("width"))-50;//畫布寬度 var r = width/2;//半徑 //畫圓和頂點名 for(let i=0;i<vertex.length;i++){ let vertexName = vertex[i], len = vertexName.length; let angle = (i*2*Math.PI/vertex.length); let cx = 0, cy = 0;//當前結點的定位畫素座標 cx = r*(1 + Math.sin(angle) )+25; cy = r*(1 - Math.cos(angle) )+25; let obj = { "cx":cx, "cy":cy }; vertexObj[vertexName] = obj; var color = "#9F79EE"; if(id===vertex[i]){ color = "green"; } circleStr += '<circle cx="'+cx+'" cy="'+cy+'" r="20" fill="'+color+'"/></circle>'; //調整文字縮排 let textcx = len>1?(cx-10):(cx-5); let textcy = cy+6; textStr += '<text x="'+textcx+'" y="'+textcy+'" fill="black">'+vertexName+'</text>'; } //畫線和數字 for(let i =0;i<adge.length;i++){ //如果依然處於當前層,則累加佔用寬度,否則將佔用寬度置零,更新層數 let startcx = 0, startcy = 0,endcx = 0,endcy = 0;//當前結點的定位畫素座標 if(!vertexObj[adge[i].start]||!vertexObj[adge[i].end]){ throw Error("邊陣列中有結點不在結點陣列中"); return; } startcx = vertexObj[adge[i].start].cx; startcy = vertexObj[adge[i].start].cy; endcx = vertexObj[adge[i].end].cx; endcy = vertexObj[adge[i].end].cy; lineStr += '<line x1="'+startcx+'" y1="'+startcy+'" x2="'+endcx+ '" y2="'+endcy+'" style="stroke:#999;stroke-width:2" />'; //計算三角形箭頭的座標和旋轉角 var getTriangle = function(obj){ var x = endcx,y=endcy+20; startcx = obj.startcx; startcy= obj.startcy; endcx= obj.endcx; endcy= obj.endcy; var x = endcx, y = endcy+20; var points = [x-7,y+15,x+7,y+15,x,y].join(",");//一個三角形三點的座標 var angle = 0; //注意這裡的座標系和通常情況下的座標系y軸是相反的,因此 endcy-startcy 要變為startcy-endcy angle =180*Math.acos((startcy-endcy)/Math.sqrt((endcx-startcx)*(endcx-startcx)+ (startcy-endcy)*(startcy-endcy)))/Math.PI; if(endcx-startcx < 0){ angle = 360 - angle; } var out = { "points":points, "angle":angle } return out; } //如果是無向圖隱藏下面計算與畫三角形箭頭的程式碼即可 var obj = { "startcx":startcx, "startcy":startcy, "endcx":endcx, "endcy":endcy } var data = getTriangle(obj); var angle = [data.angle,endcx,endcy].join(","); //畫三角形箭頭 arrowStr += '<polygon points="'+data.points+'" fill:"#171717" transform="rotate('+angle+')"/>'; obj = { "startcx":endcx, "startcy":endcy, "endcx":startcx, "endcy":startcy } data = getTriangle(obj); angle = [data.angle,endcx,endcy].join(","); //畫三角形箭頭 arrowStr += '<polygon points="'+data.points+'" fill:"#171717" transform="rotate('+angle+')"/>'; //調整文字縮排 var textcx = (startcx + endcx)/2; var textcy = (startcy + endcy)/2; textStr += '<text x="'+textcx+'" y="'+textcy+'" fill="#171717">'+adge[i].weight+'</text>'; } svg.innerHTML = lineStr+circleStr+textStr+arrowStr; } var select = document.getElementById("select"); var button = document.getElementById("button"); var node = document.getElementById("node"); var optionStr = ''; for(let i =0;i<graphVertex.length;i++){ optionStr += '<option>'+graphVertex[i]+'</option>'; } node.innerHTML = optionStr; button.addEventListener("click",function(){ if(button.click===false){ return; }else{ button.click = false; } var nodeIndex = node.selectedIndex ; var selectValue = node.options[nodeIndex].value; console.log(selectValue); var index = select.selectedIndex ; console.log(index); switch(index){ case 0: traversalArr = graph.dfsTraverse(selectValue); break; case 1: traversalArr = graph.bfsTraverse(selectValue); break; default: alert("選擇遍歷方式出錯"); break; } //注意,這裡故意 for(let i =0;i<=traversalArr.length;i++){ setTimeout(function(i){ showGraphTraverse(traversalArr[i]); if(i>=traversalArr.length){ button.click = true; console.log("OK"); } },1000*i,i); } }); })(); </script> </body> </html>
截圖: