20192307 2020-2021-1 《数据结构与面向对象程序设计》实验九报告
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程序设计与数据结构教程(第二版)》学习指导]