20192307 2020-2021-1 《資料結構與面向物件程式設計》實驗九報告
阿新 • • 發佈:2020-12-30
20192307 2020-2021-1 《資料結構與面向物件程式設計》實驗九報告
- 課程:《資料結構與面向物件程式設計》
- 班級: 1923
- 姓名: 常萬里
- 學號: 20192307
- 實驗教師:王志強老師
- 實驗日期:2020年12月29日
- 必修/選修: 必修
一、實驗內容
圖的綜合實踐
- (1) 初始化:根據螢幕提示(例如:輸入1為無向圖,輸入2為有向圖)初始化無向圖和有向圖(可用鄰接矩陣,也可用鄰接表),圖需要自己定義(頂點個數、邊個數,建議先在草稿紙上畫出圖,然後再輸入頂點和邊數)
- (2) 圖的遍歷:完成有向圖和無向圖的遍歷(深度和廣度優先遍歷)(4分)
- (3) 完成有向圖的拓撲排序,並輸出拓撲排序序列或者輸出該圖存在環(3分)
- (4) 完成無向圖的最小生成樹(Prim演算法或Kruscal演算法均可),並輸出(3分)
- (5) 完成有向圖的單源最短路徑求解(迪傑斯特拉演算法)(3分)
二、實驗過程及結果
本次實驗我使用了兩種方法去實現實驗要求。程式碼一、程式碼二、程式碼三是第一種方法,程式碼四是同學講述他的程式碼思路後,進行的優化調整。
第一種實現方法
Edge.java
package Graph; /** * 這個類代表一條邊,有起始節點和終止節點,以及邊的長度 * @author Shape Of My Heart */ public class Edge { String beginCity; String endCity; int cost; public Edge(String beginCity, String endCity, int cost) { super(); this.beginCity = beginCity; this.endCity = endCity; this.cost = cost; } }
Graph.java
package Graph; /** * @author Shape Of My Heart */ public class Graph { public static final int MAX = Integer.MAX_VALUE >>1; int size; int[][] matrix; String[] cityArray; /** * @param cityArray 代表所有的城市資訊 * @param edges 代表所有的邊 * @param direction true代表構建有向圖,false代表無向圖 */ public Graph(String[] cityArray, Edge[] edges, boolean direction) { this.cityArray = cityArray; this.size = cityArray.length; matrix = new int[size][size]; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (i == j) { matrix[i][j] = 0; } else { matrix[i][j] = Integer.MAX_VALUE; } } } for (Edge e : edges) { int begin = findIndex(e.beginCity, cityArray); int end = findIndex(e.endCity, cityArray); matrix[begin][end] = e.cost; if (!direction) { matrix[end][begin] = e.cost; } } } /** * 找出指定陣列中某個元素的位置,如果找不到,則返回-1 */ public int findIndex(String city, String[] cityArray) { for (int i = 0; i < cityArray.length; i++) { if (city.equals(cityArray[i])) { return i; } } return -1; } public void print() { for (int i = 0; i < matrix.length; i++) { int[] ii = matrix[i]; System.out.print(cityArray[i] + " "); for (int j : ii) { System.out.printf("%-16d", j); } System.out.println(); } } /** * 對圖執行深度優先順序遍歷 * * @param start * 遍歷其實節點 * @param visit * 儲存所有節點的遍歷狀態 */ public void dfs(int start, int[] visit) { // 訪問當前節點 System.out.print(cityArray[start] + " "); visit[start] = 1; for (int i = 0; i < visit.length; i++) { if (matrix[start][i] > 0 && visit[i] == 0) { dfs(i, visit); } } } /** * 對圖執行廣度優先順序遍歷 * * @param start * 遍歷開始節點 */ public void bfs(int start) { int[] visit = new int[size]; int[] queue = new int[size]; int front = -1; int tail = -1; // 根節點入隊 tail = (tail + 1) % queue.length; queue[tail] = start; visit[start] = 1; while (front != tail) { // 根節點出隊 front = (front + 1) % queue.length; int index = queue[front]; // 訪問出隊節點 System.out.print(cityArray[index] + " "); for (int i = 0; i < cityArray.length; i++) { if (matrix[index][i] != 0 && visit[i] == 0) { // 下一個節點入隊 tail = (tail + 1) % queue.length; queue[tail] = i; visit[i] = 1; } } } } }
測試程式碼
Test.java
package Graph;
import java.util.ArrayList;
import java.util.List;
/**
* @author Shape Of My Heart
*/
public class Test {
public static void main(String[] args) {
Graph graph = createGraph(false);
System.out.println("圖的矩陣如下:");
graph.print();
System.out.println("\n深度優先遍歷順序如下:");
int[] visit = new int[graph.size];
graph.dfs(0, visit);
System.out.println("\n廣度優先遍歷順序如下:");
graph.bfs(0);
System.out.println("\n");
int sum = prim(graph, 0);
System.out.println("\n最小路徑總長度是:" + sum);
int[] dist = new int[graph.size];
int[] path = new int[graph.size];
djst(graph, 0, dist, path);
System.out.println("\nDijkstra演算法--最短路徑");
for(int i=0;i<graph.size;i++) {
printPath(path, i);
System.out.println();
}
}
/**
*
* @param direction
* 是否生成有向圖
* @return
*/
public static Graph createGraph(boolean direction) {
String[] citys = new String[] { "北京", "上海", "廣州", "重慶", "武漢", "南昌" };
List<Edge> edgeList = new ArrayList<>();
edgeList.add(new Edge("北京", "廣州", 10));
edgeList.add(new Edge("北京", "上海", 11));
edgeList.add(new Edge("上海", "南昌", 6));
edgeList.add(new Edge("廣州", "重慶", 14));
edgeList.add(new Edge("廣州", "武漢", 9));
edgeList.add(new Edge("重慶", "武漢", 20));
edgeList.add(new Edge("武漢", "北京", 13));
edgeList.add(new Edge("武漢", "南昌", 12));
edgeList.add(new Edge("南昌", "廣州", 18));
Edge[] edgeArray = new Edge[edgeList.size()];
return new Graph(citys, edgeList.toArray(edgeArray), true);
}
public static int prim(Graph graph, int start) {
if (graph != null) {
int size = graph.size;
int[] lowCost = new int[size];
int[] visit = new int[size];
// 初始化lowCost陣列
for (int i = 0; i < size; i++) {
lowCost[i] = graph.matrix[start][i];
}
// 對進樹節點的操作
StringBuilder builder = new StringBuilder();
builder.append(graph.cityArray[start]).append(" ");
visit[start] = 1;
int sum = 0;
// 起始節點不需要找,所以我們總共要找(size-1)個節點,故這裡n從1開始
for (int n = 1; n < size; n++) {
int min = Integer.MAX_VALUE;
int k = -1;
// 選出下一個進樹的節點
for (int i = 0; i < size; i++) {
if (visit[i] == 0 && lowCost[i] < min) {
min = lowCost[i];
k = i;
}
}
builder.append(graph.cityArray[k]).append(" ");
visit[k] = 1;
sum += min;
// 更新剩下節點的lowCost
for (int i = 0; i < size; i++) {
if (visit[i] == 0 && graph.matrix[k][i] < lowCost[i]) {
lowCost[i] = graph.matrix[k][i];
}
}
}
System.out.println("Prim演算法--數的構造順序如下:");
System.out.println(builder.toString());
return sum;
}
return 0;
}
public static void djst(Graph graph, int start, int[] dist, int[] path) {
int size = graph.size;
int[][] matrix = graph.matrix;
int[] visit = new int[size];
// 初始化陣列path和dist
for (int i = 0; i < size; i++) {
dist[i] = matrix[start][i];
if (matrix[start][i] < Graph.MAX) {
path[i] = start;
} else {
path[i] = -1;
}
}
// 前面的for迴圈將起始節點的path值設為了start
// 這裡將起始節點的前一個節點為-1
// 整個Djst演算法結束後,只有起始節點的path值為-1
path[start] = -1;
visit[start] = 1;
// 起始節點不需要找,所以我們總共要找(size-1)個節點,故這裡n從1開始
for (int n = 1; n < size; n++) {
int min = Graph.MAX;
int k = -1;
// 找出下一個節點
for (int i = 0; i < size; i++) {
if (visit[i] == 0 && dist[i] < min) {
k = i;
min = dist[i];
}
}
visit[k] = 1;
// 找到了最短的節點之後,更新剩下節點的dist距離
for (int i = 0; i < size; i++) {
if ((visit[i] == 0) && (dist[k] + matrix[k][i] < dist[i])) {
dist[i] = dist[k] + matrix[k][i];
path[i] = k;
}
}
}
}
/**
* 打印出起始結點到每個節點的路徑 path[]陣列中儲存了一棵雙親儲存結構的樹,預設輸出的是葉子節點到根節點路徑,
*
* 我們使用了一個棧,從而實現了逆向輸出。
*
*/
public static void printPath(int[] path, int target) {
//
int[] stack = new int[path.length];
int pos = -1;
int index = target;
while (index != -1) {
stack[++pos] = index;
index = path[index];
}
while (pos!=-1) {
System.out.print(stack[pos--] + " ");
}
}
}
實驗測試截圖
(二)第二種實現方法(完整版)
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author Shape Of My Heart
*/
public class graphTest {
//設有dot個頂點,頂點序號分別為1、2、3...dot。目前頂點數限定在16個以內。
static int dot, side; //dot為頂點數,side為邊數
static int[][] concern = new int[15][15]; //矩陣資訊
static int[] link = new int[30]; //用來存續輸入的資料
static int[] visited = new int[15]; //用來標記已遍歷的點
static Scanner scan = new Scanner(System.in);
static int origin, weight = 0; //origin為起點,weight為權重
static int total2 = 0; //記錄各方法中已訪問的頂點數
static int temp = 9999, temp2 = 0;
static int[] visited2 = new int[15]; //按順序記錄Prim演算法中被選中的點的編號
static int[] dist = new int[15]; //記錄最短路徑長度
static int[] pre = new int[15]; //記錄入度來自哪個頂點
static Queue<Integer> list = new LinkedList<>();
static Stack<Integer> stack = new Stack<>();
static Stack<Integer> stack2 = new Stack<>();
public static void main(String[] args) {
System.out.println("====================================");
System.out.println(" 輸入1構造無向圖");
System.out.println(" 輸入2構造有向圖");
System.out.println("====================================");
AtomicInteger select = new AtomicInteger();
do {
select.set(scan.nextInt());
if (select.get() == 1) {
createUndirectedGraph();
}
if (select.get() == 2) {
createDigraph();
}
} while (select.get() != 1 && select.get() != 2); //避免使用者數字不是1或2
}
public static void createUndirectedGraph() {
System.out.print("頂點數:");
dot = scan.nextInt();
System.out.print("邊數:");
side = scan.nextInt();
int j = 0;
for (int i = 0; i < side; i++) {
System.out.print("請輸入第" + (i + 1) + "條邊相連的兩點(中間用空格分開):");
link[j] = scan.nextInt();
link[j + 1] = scan.nextInt();
j = j + 2;
}
for (int i = 0; i < j; i = i + 2) {
concern[link[i] - 1][link[i + 1] - 1] = 1;
concern[link[i + 1] - 1][link[i] - 1] = 1;
}
System.out.println("1、初始化,該圖的鄰接矩陣為:");
adjacencyMatrix(); //生成並輸出圖的鄰接矩陣
origin = search(); //尋找起點
System.out.println("2.1、圖的遍歷:廣度優先遍歷:");
System.out.print(origin + " "); //先輸出起點並將其標記,然後進行廣度優先遍歷
visited[origin - 1] = 1;
breadthTraversal(origin);
for (int i = 0; i < dot; i++) { //因廣度優先遍歷時已經訪問過所有點,此處需要重新初始化
visited[i] = 0;
}
System.out.println("\n2.2、圖的遍歷:深度優先遍歷:");
System.out.print(origin + " "); //先輸出起點並將其標記,然後進行深度優先遍歷
visited[origin - 1] = 1;
depthTraversal(origin);
System.out.println("\n4、Prim演算法構建無向圖的最小生成樹:");
for (int i = 0; i < dot; i++) { //因遍歷時已經訪問過,此處需要重新初始化
visited[i] = 0;
}
for (int i = 0; i < dot; i++) {
for (j = 0; j < dot; j++) {
if (concern[i][j] == 1) {
System.out.print("請輸入" + (i + 1) + " " + (j + 1) + "兩頂點之間邊的權值:");
concern[i][j] = scan.nextInt();
concern[j][i] = concern[i][j]; //對稱先置零避免重複運算
}
}
}
System.out.print("請輸入起始頂點編號:");
int v = scan.nextInt();
prim(concern, v);
System.out.println("最小權值:" + weight);
}
public static void createDigraph() {
System.out.print("頂點數:");
dot = scan.nextInt();
System.out.print("邊數:");
side = scan.nextInt();
int j = 0;
for (int i = 0; i < side; i++) {
System.out.print("請輸入第" + (i + 1) + "條邊首尾相連的兩點(先首後尾,中間用空格分開):");
link[j] = scan.nextInt();
link[j + 1] = scan.nextInt();
j = j + 2;
}
for (int i = 0; i < j; i = i + 2) {
concern[link[i] - 1][link[i + 1] - 1] = 1;
}
System.out.println("1、初始化,該圖的鄰接矩陣為:");
adjacencyMatrix(); //生成並輸出圖的鄰接矩陣
origin = search(); //尋找起點
System.out.println("2.1、圖的遍歷:廣度優先遍歷:");
System.out.print(origin + " "); //先輸出起點並將其標記,然後進行廣度優先遍歷
visited[origin - 1] = 1;
breadthTraversal(origin);
for (int i = 0; i < dot; i++) { //因廣度優先遍歷時已經訪問過所有點,此處需要重新初始化
visited[i] = 0;
}
System.out.println("\n2.2、圖的遍歷:深度優先遍歷:");
System.out.print(origin + " "); //先輸出起點並將其標記,然後進行深度優先遍歷
visited[origin - 1] = 1;
depthTraversal(origin);
System.out.println("\n====================================");
System.out.println(" 輸入1完成有向圖的拓撲排序");
System.out.println(" 輸入2完成有向圖的單源最短路徑求解");
System.out.println("====================================");
AtomicInteger select = new AtomicInteger();
do {
select.set(scan.nextInt());
} while (select.get() != 1 && select.get() != 2); //避免使用者輸入其他奇奇怪怪的數字
if (select.get() == 1) {
System.out.println("拓撲排序:");
total2 = 0;
for (int i = 0; i < dot; i++) { //因之前的方法已經訪問過,此處需要重新初始化
visited[i] = 0;
}
topology(concern);
}
if (select.get() == 2) {
System.out.println("用迪傑斯特拉演算法完成有向圖的單源最短路徑求解:");
for (int i = 0; i < dot; i++) { //因遍歷時已經訪問過,此處需要重新初始化
visited[i] = 0;
}
System.out.print("請輸入起始頂點編號:");
int v = scan.nextInt();
for (int i = 0; i < dot; i++) {
for (j = 0; j < dot; j++) {
if (concern[i][j] == 1) {
System.out.print("請輸入" + (i + 1) + " " + (j + 1) + "兩頂點之間邊的權值:");
concern[i][j] = scan.nextInt();
}
}
}
for (int i = 0; i < dot; i++) {
dist[i] = concern[v - 1][i]; //記錄起始最短距離,0為自身或無窮大
if (concern[v - 1][i] != 0) {
pre[i] = v; //記錄起始點所連線的點位
}
}
for (int i = 0; i < dot; i++) { //先選出距離最短的點
if (dist[i] != 0 && dist[i] < temp) {
temp = dist[i];
temp2 = i;
}
}
concern[v - 1][v - 1] = 1; //標記表示已訪問
total2 = 1; //初始化並+1
dijkstra(concern, temp2 + 1);
for (int i = 0; i < dot; i++) {
if (i + 1 != v) {
int tempX = i;
while (pre[tempX] != 0) { //不斷訪問其上一個點
stack2.push(tempX + 1);
tempX = pre[tempX] - 1;
}
System.out.print(v + "到" + (i + 1) + "的最短路徑為:" + v + " ");
while (!stack2.isEmpty()) {
System.out.print(stack2.pop() + " ");
}
System.out.println("長度:" + dist[i]);
}
}
}
}
public static void adjacencyMatrix() {
for (int i = 0; i < dot; i++) {
for (int j = 0; j < dot; j++) {
System.out.print(concern[i][j] + " ");
}
System.out.println();
}
}
public static int search() { //尋找起點
int origin = 0, temp = 0, temp2 = 0; //origin記錄起點,temp記錄最高的出度,令其值為-1防止直接進入if,temp2記錄該temp便於下次比較
for (int i = 0; i < dot; i++) {
for (int j = 0; j < dot; j++) {
if (concern[i][j] == 1) {
temp++;
}
}
if (temp == temp2) { //若出度相等則選擇入度較小的點為起點
int temp3 = 0, temp4 = 0; //temp3、temp4分別記錄兩者的入度數
for (int q = 0; q < dot; q++) {
if (concern[q][origin - 1] == 0) {
temp3++;
}
if (concern[q][i] == 0) {
temp4++;
}
}
if (temp3 < temp4) {
origin = i + 1;
temp2 = temp;
}
}
if (temp > temp2) {
origin = i + 1;
temp2 = temp;
}
temp = 0;
}
return origin;
}
public static void breadthTraversal(int origin) {
for (int i = 0; i < dot; i++) {
if (concern[origin - 1][i] == 1) { //尋找該點的鄰接點
if (visited[i] != 1) { //若該點位被訪問過,則輸出該點
System.out.print((i + 1) + " ");
list.add(i + 1); //將鄰接點加入佇列
visited[i] = 1; //標記鄰接點,表示已訪問
}
}
}
while (!list.isEmpty()) {
int temp = list.poll();
breadthTraversal(temp); //遞迴至遍歷完畢
}
}
public static void depthTraversal(int origin) {
for (int i = 0; i < dot; i++) {
if (concern[origin - 1][i] == 1) { //尋找該點的第一個鄰接點
if (visited[i] != 1) { //若該點未被訪問過,則輸出該點
System.out.print((i + 1) + " ");
visited[i] = 1; //標記鄰接點,表示已訪問
depthTraversal(i + 1); //遞迴至遍歷完畢
}
}
}
}
public static void topology(int[][] adjMatrix) {
int i, j, total = 0, temp2 = 0; //total記錄頂點在矩陣中的行數,temp2記錄當前入度為0的頂點的個數
for (j = 0; j < dot; j++) {
for (i = 0; i < dot; i++) {
if (adjMatrix[i][j] == 0) {
total++;
}
if (adjMatrix[i][j] == 1) {
break;
}
}
if (total == dot && visited[j] != 1) { //total=dot時表示入度為0
stack.push(j);
visited[j] = 1;
temp2++;
}
total = 0;
}
while (!stack.isEmpty()) {
int temp = stack.pop(); //temp記錄該點所指向的點
for (int k = 0; k < dot; k++) {
adjMatrix[temp][k] = 0; //去掉該點時該點的指向全部消失
}
System.out.print((temp + 1) + " ");
total2++;
}
if (total2 < dot && temp2 != 0) {
topology(adjMatrix);
}
if (total2 < dot && temp2 == 0) {
System.out.println("存在環,停止拓撲");
}
}
public static void prim(int[][] adjMatrix, int v) {
int temp = 9999, temp2 = 0, temp3 = 0; //temp記錄與該點相連的邊中最小的權值,temp2記錄該點所在列數,temp3記錄該點所在行數
visited2[total2] = v;
total2++;
for (int i = 0; i < total2; i++) { //尋找已訪問的點中權值最小的鄰接邊
for (int k = 0; k < dot; k++) {
if (adjMatrix[visited2[i] - 1][k] > 0 && adjMatrix[visited2[i] - 1][k] < temp && visited[k] != 1) {
temp = adjMatrix[visited2[i] - 1][k];
temp2 = k;
temp3 = visited2[i] - 1;
}
}
}
visited[temp3] = 1;
visited[temp2] = 1;
adjMatrix[temp3][temp2] = -1; //將已訪問的點在矩陣中的權值設為-1,避免重複運算
adjMatrix[temp2][temp3] = -1;
weight += temp;
if (total2 < dot - 1) {
prim(adjMatrix, temp2 + 1);
} else {
System.out.println("該最小生成樹的圖的鄰接矩陣為:");
for (int i = 0; i < dot; i++) { //重新轉化成鄰接矩陣便於直觀觀察
for (int j = 0; j < dot; j++) {
if (adjMatrix[i][j] == -1) {
adjMatrix[i][j] = 1;
} else {
adjMatrix[i][j] = 0;
}
System.out.print(adjMatrix[i][j] + " ");
}
System.out.println();
}
}
}
public static void dijkstra(int[][] adjMatrix, int v) {
int temp = 9999, temp2 = 0, j; //temp存權值,temp2存最小權值的邊指向的頂點在矩陣中的列數,temp2+1為頂點編號
adjMatrix[v - 1][v - 1] = 1;
for (j = 0; j < dot; j++) {
if (adjMatrix[v - 1][j] != 0 && j != v - 1) {
if (dist[j] == 0 || dist[j] > dist[v - 1] + adjMatrix[v - 1][j]) {
dist[j] = dist[v - 1] + adjMatrix[v - 1][j];
pre[j] = v;
}
}
}
for (int i = 0; i < dot; i++) { //找出下一個距離最下的點
if (dist[i] != 0 && dist[i] < temp && adjMatrix[i][i] != 1) {
temp = dist[i];
temp2 = i;
}
}
total2++;
if (total2 < dot) {
dijkstra(adjMatrix, temp2 + 1);
}
}
}
有向圖的拓撲排序,並輸出拓撲排序序列或者輸出該圖存在環
完成無向圖的最小生成樹,並輸出(Prim演算法)
完成有向圖的單源最短路徑求解(迪傑斯特拉演算法)
(五)碼雲倉庫地址
三、心得體會
- 在這次實驗過程中,我遇到了許多問題,其中既有知識上的漏洞,也有不細心導致的馬虎,這一切都補充,完善,豐富,擴充套件了我的計算機知識體系。在不斷修復問題的過程中,我使用了很多方式去查詢資料,例如:《資料結構與面向物件程式設計》,部落格園平臺,CSDN平臺,碼雲平臺,知乎app,等。進一步熟悉了Android studio這個平臺的使用與執行方式,提高了自己自主學習的能力,為我接下來學習資料結構以及JAVA語言程式設計打下了堅實的基礎,並在不斷探索的過程中逐步提升了自己。
四、參考資料
- 《Java程式設計與資料結構教程(第二版)》
- [《Java程式設計與資料結構教程(第二版)》學習指導]