1. 程式人生 > >資料結構-基於鄰接表實現圖的遍歷視覺化及使用Floyd、Dijkstra演算法求解最短路徑(JavaScript實現)

資料結構-基於鄰接表實現圖的遍歷視覺化及使用Floyd、Dijkstra演算法求解最短路徑(JavaScript實現)

使用 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>

截圖: