20202302 实验九《数据结构与面向对象程序设计》实验报告
课程:《程序设计与数据结构》
班级: 2023
姓名: 吉相融
学号:20202302
实验教师:王志强
实验日期:2021年12月19日
必修/选修: 必修
1.实验内容
(1) 初始化:根据屏幕提示(例如:输入1为无向图,输入2为有向图)初始化无向图和有向图(可用邻接矩阵,也可用邻接表),图需要自己定义(顶点个数、边个数,建议先在草稿纸上画出图,然后再输入顶点和边数)(2分)
(2) 图的遍历:完成有向图和无向图的遍历(深度和广度优先遍历)(4分)
(3) 完成有向图的拓扑排序,并输出拓扑排序序列或者输出该图存在环(3分)
(4) 完成无向图的最小生成树(Prim算法或Kruscal算法均可),并输出(3分)
(5) 完成有向图的单源最短路径求解(迪杰斯特拉算法)(3分)
PS:本题12分。目前没有明确指明图的顶点和连通边,如果雷同或抄袭,本次实验0分。
实验报告中要根据所编写的代码解释图的相关算法
2. 实验过程及结果
先画出用于检验的图:
加权后:
主函数:
public static void main(String[] args) { System.out.println(" 输入1构造无向图"); System.out.println(" 输入2构造有向图"); int select; do{ select = scan.nextInt(); if(select == 1){ CreateUndigraph(); } if(select == 2){ CreateDigraph(); } }while (select != 1 && select != 2); //避免用户输入其他字符 System.out.println("初始化,该图的邻接矩阵为:"); Adjacency_Matrix(); //生成并输出图的邻接矩阵 origin = search(); //寻找起点 System.out.println("图的遍历:广度优先遍历:"); System.out.print(origin + " "); //先输出起点,将其标记 visited[origin - 1] = 1; //被访问的点要做标记,1代表被访问过 Breadth_Traversal(origin); for(int i = 0; i < dot; i++){ //因广度优先遍历时已经访问过所有点,需要重新初始化 visited[i] = 0; } System.out.println("\n2.2、图的遍历:深度优先遍历:"); System.out.print(origin + " "); //先输出起点并将其标记,进行深度优先遍历 visited[origin - 1] = 1; Depth_Traversal(origin); if(select == 1){ System.out.println("\n4、Prim算法构建无向图的最小生成树:"); for(int i = 0; i < dot; i++){ //因遍历时已经访问过,此处需要重新初始化 visited[i] = 0; } for(int i = 0; i < dot; i++){ for(int 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); } if(select == 2){ //拓扑和迪杰斯特拉算法可能改变邻接矩阵,因此此处分开进行 System.out.println(" 输入1完成有向图的拓扑排序"); System.out.println(" 输入2完成有向图的单源最短路径求解"); do{ select = scan.nextInt(); }while (select != 1 && select != 2); //避免用户输入其他字符 if(select == 1){ System.out.println("拓扑排序:"); total2 = 0; for(int i = 0; i < dot; i++){ //因之前的方法已经访问过,此处需要重新初始化 visited[i] = 0; } Topology(concern); } if(select == 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(int 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 tempp = i; while (pre[tempp] != 0){ //不断访问其上一个点 stack2.push(tempp + 1); tempp = pre[tempp] - 1; } System.out.print(v + "到" + (i+1) + "的最短路径为:" + v + " "); while (!stack2.isEmpty()){ System.out.print(stack2.pop() + " "); } System.out.println("长度:" + dist[i]); } } } } }
1.1初始化:将无向图存入邻接矩阵:读入顶点数与边数后,用link数组存储后再存入邻接矩阵中,一次可存储位于对称位置上的两个数。各自减一是因为在存储的最后j+2,数据实际在前面。
public static void CreateUndigraph(){ 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; } }
1.2初始化:将有向图存入邻接矩阵:与构建无向图原理相同。但有向图的存储由于在邻接矩阵中没有对称性,所以一次只能存储一个位置。
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; } }
表示出邻接矩阵:
public static void Adjacency_Matrix(){ for(int i = 0; i < dot; i++){ for(int j = 0; j < dot; j++){ System.out.print(concern[i][j] + " "); } System.out.println(); } }
寻找起点:根据已经创建的邻接矩阵,利用其行表示出度,列表示入度的性质,来确定起点。
先判断出度,即当矩阵的一行中有一个1,temp++,由此得出顶点出度作为比较依据;若出度相等,则比较入度,入度小的作为起点。
这里要注意,origin-1是为了规避第一次进行出度比较时可能出错,由此可让出度判断向前推进。
public static int search(){ //寻找起点 int origin = 0, temp = 0, temp2 = 0; //origin记录起点,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; }
2.1广度优先遍历:利用矩阵遍历,将之前未访问过的邻接点访问并标记,直至每个临界点被标记。把根节点标记,搜索所有在它下一级的元素,把这些元素标记。并把这个元素记为它下一级元素的前驱。找到所要找的元素时结束程序。如果遍历整个树还没有找到,结束程序。
public static void Breadth_Traversal(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 = (int) list.poll(); Breadth_Traversal(temp); //递归至遍历完毕 } }
2.2深度优先遍历:利用矩阵遍历,将之前未访问过的邻接点访问并标记,直至每个临界点被标记。把根节点标记,搜索所有在它上一级的元素,把这些元素标记。并把这个元素记为它上一级元素的前驱。找到所要找的元素时结束程序。如果遍历整个树还没有找到,结束程序。
public static void Depth_Traversal(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; //标记邻接点,已访问过
Depth_Traversal(i + 1); //递归至遍历完毕 } } } }
3.拓扑排序:先找出入度为零的点,输出该点后,将它的指向全部删除,由此循环往复,一直输出入度较小的点,直至所有点被输出。方法的关键在于删除输出邻接点的指向性,用到栈的一些方法。
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 = (int) 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("存在环,停止拓扑"); } }
4.1 prim算法生成树:
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(); } } }
5.迪杰斯特拉算法算最短路径:
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); } }
运行结果:
1.无向图:
有向图:

若存在环,以下图作为检验:
运行结果:
3. 实验过程中遇到的问题和解决过程
1.邻接矩阵一开始在存储无向图的时候,存储不对称(如图),原因是在代码中,有一步将行与列的字母i、j搞反了,直接裂开。。。。。。找了好久问题所在,没想到是这个。
2.构造树以及在寻找起点时,origin一开始我设置为0,导致寻找起点时非常不顺利,起点卡到origin不动了。后来发现如果origin设置为零,那么它的入度一定一定是最小的,根本就不用找了。于是在下面进入循环时设置为origin-1,来规避这个问题。
3.拓扑排序时,去掉输出点的指向性这一步不知如何去做。最后在学长和同学的帮助下利用栈的操作去掉了指向性。
其他(感悟、思考等)
这实验怎么能这么难难难难难难难难难难难,一整个蚌埠住了。完全就是在搞心态,断断续续做了好多天。打辩论还要想着怎么弄图的实验的感受谁能懂????这算法多就算了吧,一个个还贼*********难,出事了调试也耗时间,理解起来也贼费劲。我*****,才把实验搞出来。
不过这也是最后一次实验了,好耶!
这次实验确实很难做,好多实现的理解没跟上,还是在学长的讲解下理解编写的。关于图的内容理解我感觉还好,但是实现就成了难上加难。但是做完实验感觉收获也不小吧。
转眼间本学期的最后一次实验就过去了,我们也成为了最后一批超人带的数据结构与Java课的学生。一学期下来,上Java课感觉还是很欢乐的哈哈哈哈哈哈哈哈哈哈哈哈,而且也对数据结构和Java有了充分理解。最后感谢一学期来同舟共济的同学们,还有我们的超人!祝各位未来一切顺利身体健康哈哈哈哈哈。
我们更高处见!
参考资料
- 《Java程序设计教程(第九版)》
- 《Java软件结构与数据结构(第四版)》