
本文为数据结构基础【图】 相关知识,下边将对图的基本概念,图的存储结构,图的遍历包含广度优先遍历和深度优先遍历,循环遍历数组,最小生成树,拓扑排序等进行详尽介绍~
📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)


定义: 图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的;其中,点通常被成为"顶点(vertex)“,而点与点之间的连线则被成为"边或弧”(edege)。通常记为,G=(V,E)。
根据边是否有方向,将图可以划分为:无向图 和有向图。
🍀(1)无向图

上面的图G0是无向图,无向图的所有的边都是不区分方向的。G0=(V1,{E1})。其中:
🍀(2)有向图

上面的图G2是有向图。和无向图不同,有向图的所有的边都是有方向的! G2=(V2,{A2})。其中:
🍀(1)邻接点
🍀(2)度
在学习"哈夫曼树"的时候,了解过"权"的概念。图中权的概念与此类似。

上面就是一个带权的图。
图的存储结构,常用的是"邻接矩阵"和"邻接表"。
邻接矩阵是指用矩阵来表示图。它是采用矩阵来描述图中顶点之间的关系(及弧或边的权)。
假设图中顶点数为n,则邻接矩阵定义为:

下面通过示意图来进行解释。

图中的G1是无向图和它对应的邻接矩阵。

图中的G2是无向图和它对应的邻接矩阵。
通常采用两个数组来实现邻接矩阵:一个一维数组用来保存顶点信息,一个二维数组来用保存边的信息。
邻接矩阵的缺点就是比较耗费空间。
邻接表是图的一种链式存储表示方法。它是改进后的"邻接矩阵",它的缺点是不方便判断两个顶点之间是否有边,但是相对邻接矩阵来说更省空间。

图中的G1是无向图和它对应的邻接矩阵。

图中的G2是无向图和它对应的邻接矩阵。
对于图而言,我们常用的遍历方式有bfs和dfs两种:
🍀(1)广度优先搜索介绍
🍀(2)广度优先搜索图解
无向图的广度优先搜索:

因此访问顺序是:A -> C -> D -> F -> B -> G -> E
有向图的广度优先搜索:

因此访问顺序是:A -> B -> C -> E -> F -> D -> G
🍀(3)广度优先搜索代码实现
public class Graph {/*** 定义顶点的抽象* @param */public static class Vertex{// 要保存的数据private T t;// 其他和我管理的邻接节点private List> neighborList;private boolean visited = false;public Vertex(T t) {this.t = t;}}// bfs 广度优先遍历算法public static void bfs(Vertex vertex){// 1、定义一个临时存储的空间,使用队列Queue> queue = new ArrayBlockingQueue<>(8);// 2、增加一个用来保存已经遍历过的数据的集合HashSet> mome = new HashSet<>(8);// 3、将第一个顶点放入队列queue.add(vertex);while (!queue.isEmpty()){// 将第一个元素拿出来Vertex temp = queue.poll();// 进行操作if (!mome.contains(temp)){System.out.println(temp.t);mome.add(temp);}// 将他所有的邻接节点放进去if(temp.neighborList != null){queue.addAll(temp.neighborList);}}}
}
🍀(1)深度优先搜索介绍
🍀(2)深度优先搜索图解
无向图的深度优先搜索:

对上面的图G1进行深度优先遍历,从顶点A开始

因此访问顺序是:A -> C -> B -> D -> F -> G -> E
有向图的深度优先搜索:

对上面的图G2进行深度优先遍历,从顶点A开始。

因此访问顺序是:A -> B -> C -> E -> D -> F -> G
🍀(3)深度优先搜索代码实现
public class Graph {/*** 定义顶点的抽象* @param */public static class Vertex{// 要保存的数据private T t;// 其他和我管理的邻接节点private List> neighborList;private boolean visited = false;public Vertex(T t) {this.t = t;}}// dfs 深度优先遍历算法public static void dfs(Vertex vertex){// 1、定义一个临时存储的空间Stack> stack = new Stack<>();// 2、将第一个顶点放入栈中stack.push(vertex);while (!stack.isEmpty()){// 3、将栈顶的元素取出Vertex temp = stack.pop();// 4、执行操作if(!temp.visited){System.out.println(temp.t);temp.visited = true;}// 5、将邻接节点压栈if(temp.neighborList != null){stack.addAll(temp.neighborList);}}}
}
在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。

例如,对于如上图G4所示的连通网可以有多棵权值总和不相同的生成树。

🍀(1)克鲁斯卡尔算法介绍
🍀(2)克鲁斯卡尔算法图解
以上图G4为例,来对克鲁斯卡尔进行演示(假设,用数组R保存最小生成树结果)。

此时,最小生成树构造完成!它包括的边依次是:
🍀(3)克鲁斯卡尔算法代码实现
这里选取"邻接矩阵"对克鲁斯卡尔算法进行说明。
// 边的结构体
private static class EData {char start; // 边的起点char end; // 边的终点int weight; // 边的权重public EData(char start, char end, int weight) {this.start = start;this.end = end;this.weight = weight;}
}
// 邻接矩阵边对应的结构体
public class MatrixUDG {private int mEdgNum; // 边的数量private char[] mVexs; // 顶点集合private int[][] mMatrix; // 邻接矩阵private static final int INF = Integer.MAX_VALUE; // 最大值...
}
/** 克鲁斯卡尔(Kruskal)最小生成树*/
public void kruskal() {int index = 0; // rets数组的索引int[] vends = new int[mEdgNum]; // 用于保存"已有最小生成树"中每个顶点在该最小树中的终点。EData[] rets = new EData[mEdgNum]; // 结果数组,保存kruskal最小生成树的边EData[] edges; // 图对应的所有边// 获取"图中所有的边"edges = getEdges();// 将边按照"权"的大小进行排序(从小到大)sortEdges(edges, mEdgNum);for (int i=0; iint p1 = getPosition(edges[i].start); // 获取第i条边的"起点"的序号int p2 = getPosition(edges[i].end); // 获取第i条边的"终点"的序号int m = getEnd(vends, p1); // 获取p1在"已有的最小生成树"中的终点int n = getEnd(vends, p2); // 获取p2在"已有的最小生成树"中的终点// 如果m!=n,意味着"边i"与"已经添加到最小生成树中的顶点"没有形成环路if (m != n) {vends[m] = n; // 设置m在"已有的最小生成树"中的终点为nrets[index++] = edges[i]; // 保存结果}}// 统计并打印"kruskal最小生成树"的信息int length = 0;for (int i = 0; i < index; i++)length += rets[i].weight;System.out.printf("Kruskal=%d: ", length);for (int i = 0; i < index; i++)System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);System.out.printf("\n");
}
🍀(1)普里姆算法介绍
🍀(2)普里姆算法图解

以上图G4为例,来对普里姆进行演示(从第一个顶点A开始通过普里姆算法生成最小生成树)。

初始状态:V是所有顶点的集合,即V={A,B,C,D,E,F,G};U和T都是空!
此时,最小生成树构造完成!它包括的顶点依次是:A B F E D C G。
🍀(3)普里姆算法代码实现
这里以"邻接矩阵"为例对普里姆算法进行说明。
// 邻接矩阵对应的结构体
public class MatrixUDG {private char[] mVexs; // 顶点集合private int[][] mMatrix; // 邻接矩阵private static final int INF = Integer.MAX_VALUE; // 最大值...
}
/** prim最小生成树** 参数说明:* start -- 从图中的第start个元素开始,生成最小树*/
public void prim(int start) {int num = mVexs.length; // 顶点个数int index=0; // prim最小树的索引,即prims数组的索引char[] prims = new char[num]; // prim最小树的结果数组int[] weights = new int[num]; // 顶点间边的权值// prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。prims[index++] = mVexs[start];// 初始化"顶点的权值数组",// 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。for (int i = 0; i < num; i++ )weights[i] = mMatrix[start][i];// 将第start个顶点的权值初始化为0。// 可以理解为"第start个顶点到它自身的距离为0"。weights[start] = 0;for (int i = 0; i < num; i++) {// 由于从start开始的,因此不需要再对第start个顶点进行处理。if(start == i)continue;int j = 0;int k = 0;int min = INF;// 在未被加入到最小生成树的顶点中,找出权值最小的顶点。while (j < num) {// 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。if (weights[j] != 0 && weights[j] < min) {min = weights[j];k = j;}j++;}// 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。// 将第k个顶点加入到最小生成树的结果数组中prims[index++] = mVexs[k];// 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。weights[k] = 0;// 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。for (j = 0 ; j < num; j++) {// 当第j个节点没有被处理,并且需要更新时才被更新。if (weights[j] != 0 && mMatrix[k][j] < weights[j])weights[j] = mMatrix[k][j];}}// 计算最小生成树的权值int sum = 0;for (int i = 1; i < index; i++) {int min = INF;// 获取prims[i]在mMatrix中的位置int n = getPosition(prims[i]);// 在vexs[0...i]中,找出到j的权值最小的顶点。for (int j = 0; j < i; j++) {int m = getPosition(prims[j]);if (mMatrix[m][n]
拓扑排序算法的基本步骤:
注:顶点A没有依赖顶点,是指不存在以A为终点的边。

以上图为例,来对拓扑排序进行演示。

因此访问顺序是:B -> C -> A -> D -> E -> F -> G
拓扑排序是对有向无向图的排序。下面以邻接表实现的有向图来对拓扑排序进行说明。
// 邻接表对应的结构体
public class ListDG {// 邻接表中表对应的链表的顶点private class ENode {int ivex; // 该边所指向的顶点的位置ENode nextEdge; // 指向下一条弧的指针}// 邻接表中表的顶点private class VNode {char data; // 顶点信息ENode firstEdge; // 指向第一条依附该顶点的弧};private VNode[] mVexs; // 顶点数组...
}
/** 拓扑排序** 返回值:* -1 -- 失败(由于内存不足等原因导致)* 0 -- 成功排序,并输入结果* 1 -- 失败(该有向图是有环的)*/
public int topologicalSort() {int index = 0;int num = mVexs.size();int[] ins; // 入度数组char[] tops; // 拓扑排序结果数组,记录每个节点的排序后的序号。Queue queue; // 辅组队列ins = new int[num];tops = new char[num];queue = new LinkedList();// 统计每个顶点的入度数for(int i = 0; i < num; i++) {ENode node = mVexs.get(i).firstEdge;while (node != null) {ins[node.ivex]++;node = node.nextEdge;}}// 将所有入度为0的顶点入队列for(int i = 0; i < num; i ++)if(ins[i] == 0)queue.offer(i); // 入队列while (!queue.isEmpty()) { // 队列非空int j = queue.poll().intValue(); // 出队列。j是顶点的序号tops[index++] = mVexs.get(j).data; // 将该顶点添加到tops中,tops是排序结果ENode node = mVexs.get(j).firstEdge;// 获取以该顶点为起点的出边队列// 将与"node"关联的节点的入度减1;// 若减1之后,该节点的入度为0;则将该节点添加到队列中。while(node != null) {// 将节点(序号为node.ivex)的入度减1。ins[node.ivex]--;// 若节点的入度为0,则将其"入队列"if( ins[node.ivex] == 0)queue.offer(node.ivex); // 入队列node = node.nextEdge;}}if(index != num) {System.out.printf("Graph has a cycle\n");return 1;}// 打印拓扑排序结果System.out.printf("== TopSort: ");for(int i = 0; i < num; i ++)System.out.printf("%c ", tops[i]);System.out.printf("\n");return 0;
}

👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
上一篇:羞日捂袖,愁春懒补