数据结构与算法(十一):图的基础以及遍历代码实现
本篇目录
一、图定义
图是一种较线性表和树更为复杂的数据结构,其定义为:
图是由顶点的有穷非空集合与顶点之间边的集合构成,通常表示为:G(V, E), G表示一个图,V表示图中顶点的集合,E表示顶点之间边的集合。
如下,就是一个图:
二、图术语了解
图中数据元素我们称之为顶点,图中任意两个顶点都可能存在关系,顶点之间关系用边来表示。
若两个顶点Vi与Vj之间的边没有方向,则称这条边为无向边, 用(Vi,Vj)表示,注意这里是圆括号
如果图中任意顶点之间都是无向边,则称该图为无向图,在无向图中任意两个顶点之间都存在边,则称该图为无向完全图,上图就是一个无向完全图。
若两个顶点Vi与Vj之间的边有方向,则称这条边为有向边,也称作弧, 用<vi,Vj>表示,注意这里是尖括号</v
如果图中任意顶点之间都是有向边,则称该图为有向图,在有向图中任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图,如下图:
有些图的边或者弧具有与其相关的数字,这种与图或弧相关的数字叫做权值。
在无向图中如果两个顶点之间有路径,则称这两个顶点是联通的,如果图中任意两个顶点都是联通的,则称图是连通图。
在无向图中顶点的边数叫做顶点的度。
在有向图中顶点分为出度和入度,入度就是指向自己的边数,而出度则相反。
如下图:
A结点:出度为3,入度为1
B结点:出度为1,入度为2
以上讲解了一些图的基本术语,没什么难度,大概了解一下就可以了。
三、图的存储
图的结构比较复杂,任意两个元素都可能产生关系,所以一般存储结构无法满足。
邻接矩阵存储方式
图是由顶点和边(或者弧)组成的,可以单独存储顶点和边,顶点简单,可以用一位数组来存储,但是边呢?边就是两个顶点之间的关系,可以用一个二维数组来存储,这种存储方式就是邻接矩阵存储方式。
邻接矩阵存储方式就是用两个数组来存储,一个一维数组,一个二维数组,一维数组用来存储顶点,二维数组用来存储顶点之间的关系,也就是边或者弧。
无向图邻接矩阵存储方式:
如下无向图:
一维数组存储顶点数组:
二维数组存储顶点(边,弧)之间关系:
无向图边之间关系的二维数组用0或者1来填充,如果两个顶点之间直接连通则为1,不直接连通则为0,有个注意点就是直接不是间接连通。比如A与B之间直接连通所以填1,而A与D之间不直接连通则为0.
有向图邻接矩阵存储方式:
如下有向图:
存储顶点的一维数组与上面一样
二维数组存储顶点(边,弧)之间关系:
无向图中两个顶点之间有直接相连的边则为1,而有向图中如上图中B与C之间,有B指向C的边则为1,而没有C指向B的边则为0。
带权邻接矩阵存储方式:
如下图有向带权图:
存储顶点的一维数组与上面一样
二维数组存储顶点(边,弧)之间关系:
很简单,就是将有向图中1替换为对应权值,此外,如图中A,C之间,没有A指向C的边则用∞表示。
对于带权有向图将各个顶点理解成一个个村庄,边就是村庄之间的路,权值就是距离,AB村庄之间有A指向B的路程,并且路程是100,则二维数组中直接用100表示,BA之间没有路,则用无穷大表示,代表永远不可达,自己到自己就是0了。
好了,以上介绍了图的一些术语以及其中一种存储方式,另一种方式为邻接表的存储方式,这里就不提了,有兴趣可以自己去找找资料了解一下。
四、java实现图的常用方法
在上面我们了解了邻接矩阵的方式存储图,图大部分信息已经展示在二维数组里面了,很多方法也都是从二维数组中提取信息,接下来我们用java具体实现图以及一些核心方法。
图的创建
图的创建就是外部输入图的信息我们来存储下来就可以了,代码如下:
- 1 private int[] vertices;// 存储图的顶点
- 2 public int[][] matrix; // 存储图的边
- 3 private int verticeSize; // 顶点的数量
- 4 //带权有向图中代表相邻两个顶点之间不可达
- 5 private static final int MAX_WEIGHT = 0xFFFF;
- 6
- 7 public Graph(int verticeSize) {
- 8 this.verticeSize = verticeSize;
- 9 vertices = new int[verticeSize];
- 10 matrix = new int[verticeSize][verticeSize];
- 11 //初始化存储顶点的一维数组
- 12 for(int i = 0; i < verticeSize; i++) {
- 13 vertices[i] = i;
- 14 }
- 15 }
获取图中两个顶点的权值
获取图中两个顶点的权值其实就是二维数组中对应位置的值:
- 1 /**
- 2 * 计算V1到v2 的权值
- 3 * @param v1
- 4 * @param v2
- 5 * @return
- 6 */
- 7 public int getWidget(int v1, int v2) {
- 8 int weight = matrix[v1][v2];
- 9 return weight == 0 ? 0 : (weight == MAX_WEIGHT ? -1:weight);
- 10 }
获取图某个顶点V的出度
顶点V的出度的获取只需要遍历其所在二维数组那一行数据有几个有效权值即可:
- 1 /**
- 2 * 计算某个顶点V的出度
- 3 * @param v
- 4 * @return
- 5 */
- 6 public int getOutDegree(int v) {
- 7 int count = 0;
- 8 for(int i = 0; i < verticeSize; i++ ) {
- 9 //遍历其所在二维数组那一行的数据
- 10 if (matrix[v][i] != 0 && matrix[v][i] != MAX_WEIGHT) {
- 11 count ++;
- 12 }
- 13 }
- 14 return count;
- 15 }
获取图某个顶点V的入度
出度是遍历其所在行,入度就是遍历其所在列了(这里不明白可以自己想想):
- 1 /**
- 2 * 计算某个顶点V的入度
- 3 * @param v
- 4 * @return
- 5 */
- 6 public int getInDegree(int v) {
- 7 int count = 0;
- 8 for(int i = 0; i < verticeSize; i++) {
- 9 if (matrix[i][v] != 0 && matrix[i][v] != MAX_WEIGHT) {
- 10 count ++;
- 11 }
- 12 }
- 13 return count;
- 14 }
获取某个顶点的第一个邻接点
第一个邻接点就是顶点第一个可以直接到达的点,比如顶点A有直接指向B,C,D的边,而A第一个指向B,其次C,D,则B是A的第一个邻接点,查找邻接点也很简单,就是顶点所在二维数组的那一行数据第一个有效权值的角标:
- 1 /**
- 2 * 获取某个顶点的第一个邻接点
- 3 * @param v
- 4 * @return
- 5 */
- 6 public int getFirstNeightbor(int v) {
- 7 for(int i = 0; i < verticeSize; i++) {
- 8 if (matrix[v][i] > 0 && matrix[v][i] != MAX_WEIGHT) {
- 9 return i;
- 10 }
- 11 }
- 12 return -1;
- 13 }
查找节点v ,index开始的下一个邻接点
上面是从0查找节点V的第一个邻接点,这里是从index角标开始查找其第一个邻接点:
- 1 /**
- 2 * 查找节点v ,index开始的下一个邻接点
- 3 * @param v 节点
- 4 * @param index 开始角标
- 5 * @return
- 6 */
- 7 public int getNextNeightBor(int v,int index) {
- 8 for(int j = index +1; j < verticeSize; j++) {
- 9 if (matrix[v][j] > 0 && matrix[v][j] != MAX_WEIGHT) {
- 10 return j;
- 11 }
- 12 }
- 13 return -1;
- 14 }
以上了解了一些图的常用方法,接下来我们输入一个图测试一下。
图如下:
我们需要自己提取出图中各顶点之间的关系也就是二维数组:
然后我们就可以测试了:
- 1 Graph graph = new Graph(4);//创建图
- 2 //输入图的边关系二维数组
- 3 int[] a0 = new int[]{0, 8, 4, 6};
- 4 int[] a1 = new int[]{Graph.MAX_WEIGHT, 0, Graph.MAX_WEIGHT, 7};
- 5 int[] a2 = new int[]{Graph.MAX_WEIGHT, Graph.MAX_WEIGHT, 0, Graph.MAX_WEIGHT};
- 6 int[] a3 = new int[]{Graph.MAX_WEIGHT, Graph.MAX_WEIGHT, 5, 0};
- 7 graph.matrix[0] = a0;
- 8 graph.matrix[1] = a1;
- 9 graph.matrix[2] = a2;
- 10 graph.matrix[3] = a3;
- 11 //以下求结点0的信息,图内部一维数组存储的并不是A,B,C,D 而是0,1,2,3 一一对应
- 12 //这里也就是求节点A的入度,出度,第一个邻接点,C节点以后的第一个邻接点
- 13 System.out.println(graph.getInDegree(0));
- 14 System.out.println(graph.getOutDegree(0));
- 15 System.out.println(graph.getFirstNeightbor(0));
- 16 System.out.println(graph.getNextNeightBor(0, 2));
打印信息如下:
- 0 //A节点入度为0
- 3 //A节点出度为3
- 1 //A节点第一个邻接点为1,代表B
- 3 //A节点C节点之后的第一个邻接点为3,代表D结点
好了,以上讲解一下图的基础概念以及代码实现一些重要方法,都比较简单,没有过多分析,接下来我们了解一下图的遍历方式。
五、图的遍历
和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。
图的遍历主要分为深度优先和广度优先遍历,它们对无向图和有向图都适用。
深度优先遍历
大体思路:初始状态图中所有顶点都未曾被访问,则深度优先遍历可以从图中某一顶点V出发,先访问此顶点,然后依次从V的未被访问的邻接点出发深度优先遍历图,直至图中和V有路径相通的顶点都被访问到;若此时图中仍未有顶点没被访问到,则另选图中另一个未被访问的顶点作起始点,重复上述操作,直至图中所有顶点都被访问到。
如下图:
进行深度优先遍历:
先访问A,A有邻接点B,那么访问B,B有邻接点D,则访问D,D有邻接点C,则继续访问C。
所以上图深度优先遍历为:
深度优先代码实现:
isVisited为一个数组,记录节点是否被访问过
- 1 /**
- 2 * 深度优先
- 3 */
- 4 public void dfs() {
- 5 //对每一个节点进行一次深度优先遍历
- 6 for (int i = 0; i < verticeSize; i++) {
- 7 if (!isVisited[i]) {//当前结点没有被访问过
- 8 System.out.println("viested vertice " + i);
- 9 dfs(i);//从当前节点开始进行一次深度优先遍历
- 10 }
- 11 }
- 12 }
- 13
- 14 //从节点i开始进行深度优先遍历
- 15 public void dfs(int i) {
- 16 //节点设置为已经被访问
- 17 isVisited[i] = true;
- 18 //获取第一个邻接点
- 19 int v = getFirstNeightbor(i);
- 20 //有邻接点
- 21 while (v != -1) {
- 22 //并且没有被访问过
- 23 if (!isVisited[v]) {
- 24 //访问节点
- 25 System.out.println("visted vertice " + v);
- 26 //从当前节点开始继续深度遍历
- 27 dfs(v);
- 28 }
- 29 //上一个邻接点已经被访问则继续查找下一个邻接点
- 30 v = getNextNeightBor(i, v);
- 31 }
- 32 }
广度优先遍历
大体思路:假设从图中V出发,在访问了V之后依次访问V的各个未曾被访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先与“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到,若此时图中尚有顶点未被访问,则另选图中一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
如下图:
进行广度优先遍历;
先访问节点A,A有邻接点B, C,则依次访问B, C,然后访问B的邻接点(B先于C被访问,所以先访问B的邻接点)D,在访问C的邻接点,此时所有结点都已经被访问了,所以遍历结束。
所以上图广度优先遍历为:
广度优先遍历代码实现:
- 1 /**
- 2 * 广度优先
- 3 */
- 4 public void bfs(){
- 5 for (int i = 0; i < verticeSize; i++) {
- 6 isVisited[i]=false;
- 7 }
- 8 for (int i = 0; i < verticeSize; i++) {
- 9 if(!isVisited[i]){
- 10 isVisited[i]=true;
- 11 System.out.println("visited vertice:"+ i);
- 12 bfs(i);
- 13 }
- 14 }
- 15 }
- 16
- 17 public void bfs(int i) {
- 18 //当做队列使用,先访问的先放入队列
- 19 LinkedList<Integer> queue = new LinkedList<>();
- 20 //找第一个邻接点
- 21 int fn = getFirstNeightbor(i);
- 22 if (fn == -1) {
- 23 return;
- 24 }
- 25 //没有被访问过
- 26 if (!isVisited[fn]) {
- 27 isVisited[fn] = true;//设置为被访问
- 28 System.out.println("visted vertice:" + fn);
- 29 queue.offer(fn);
- 30 }
- 31 //访问各个未曾访问过的邻接点
- 32 int next = getNextNeightBor(i, fn);
- 33 while (next != -1) {
- 34 if (!isVisited[next]) {
- 35 isVisited[next] = true;
- 36 System.out.println("visted vertice:" + next);
- 37 queue.offer(next);
- 38 }
- 39 next = getNextNeightBor(i, next);
- 40 }
- 41 //从队列中取出来一个,重复之前的操作
- 42 while(!queue.isEmpty()){
- 43 int point=queue.poll();
- 44 bfs(point);
- 45 }
- 46 }
好了,以上就是图的遍历方式,介绍到此。
关于图部分,本系列就介绍到此,主要就介绍了一些基础概念,重要方法以及两种遍历方式,当然图部分还有最小生成树算法以及最短路径算法,感兴趣的同学可以自己去查找学习一下。
好了,本篇到此为止。
最后附上整个类的代码:
- 1 public class Graph {
- 2 private int[] vertices;// 存储图的顶点
- 3 public int[][] matrix; // 存储图的边
- 4 private int verticeSize; // 顶点的数量
- 5 //带权有向图中代表相邻两个顶点之间不可达
- 6 public static final int MAX_WEIGHT = 0xFFFF;
- 7 //记录节点是否被访问过
- 8 private boolean[] isVisited;
- 9
- 10 public Graph(int verticeSize) {
- 11 this.verticeSize = verticeSize;
- 12 vertices = new int[verticeSize];
- 13 matrix = new int[verticeSize][verticeSize];
- 14 isVisited = new boolean[verticeSize];
- 15 for(int i = 0; i < verticeSize; i++) {
- 16 vertices[i] = i;
- 17 }
- 18 }
- 19
- 20 /**
- 21 * 计算V1到v2 的权值
- 22 * @param v1
- 23 * @param v2
- 24 * @return
- 25 */
- 26 public int getWidget(int v1, int v2) {
- 27 int weight = matrix[v1][v2];
- 28 return weight == 0 ? 0 : (weight == MAX_WEIGHT ? -1:weight);
- 29 }
- 30
- 31 /**
- 32 * 获取所有的顶点
- 33 * @return
- 34 */
- 35 public int[] getVertices() {
- 36 return vertices;
- 37 }
- 38
- 39 /**
- 40 * 计算某个顶点V的出度
- 41 * @param v
- 42 * @return
- 43 */
- 44 public int getOutDegree(int v) {
- 45 int count = 0;
- 46 for(int i = 0; i < verticeSize; i++ ) {
- 47 if (matrix[v][i] != 0 && matrix[v][i] != MAX_WEIGHT) {
- 48 count ++;
- 49 }
- 50 }
- 51 return count;
- 52 }
- 53
- 54 /**
- 55 * 计算某个顶点V的入度
- 56 * @param v
- 57 * @return
- 58 */
- 59 public int getInDegree(int v) {
- 60 int count = 0;
- 61 for(int i = 0; i < verticeSize; i++) {
- 62 if (matrix[i][v] != 0 && matrix[i][v] != MAX_WEIGHT) {
- 63 count ++;
- 64 }
- 65 }
- 66 return count;
- 67 }
- 68
- 69 /**
- 70 * 获取某个顶点的第一个邻接点
- 71 * @param v
- 72 * @return
- 73 */
- 74 public int getFirstNeightbor(int v) {
- 75 for(int i = 0; i < verticeSize; i++) {
- 76 if (matrix[v][i] > 0 && matrix[v][i] != MAX_WEIGHT) {
- 77 return i;
- 78 }
- 79 }
- 80 return -1;
- 81 }
- 82
- 83 /**
- 84 * 查找节点v ,index开始的下一个邻接点
- 85 * @param v 节点
- 86 * @param index 节点
- 87 * @return
- 88 */
- 89 public int getNextNeightBor(int v,int index) {
- 90 for(int j = index +1; j < verticeSize; j++) {
- 91 if (matrix[v][j] > 0 && matrix[v][j] != MAX_WEIGHT) {
- 92 return j;
- 93 }
- 94 }
- 95 return -1;
- 96 }
- 97
- 98
- 99 /**
- 100 * 深度优先
- 101 */
- 102 public void dfs() {
- 103 //对每一个节点进行一次深度优先遍历
- 104 for (int i = 0; i < verticeSize; i++) {
- 105 if (!isVisited[i]) {//当前结点没有被访问过
- 106 System.out.println("viested vertice " + i);
- 107 dfs(i);//从当前节点开始进行一次深度优先遍历
- 108 }
- 109 }
- 110 }
- 111
- 112 //从节点i开始进行深度优先遍历
- 113 public void dfs(int i) {
- 114 //节点设置为已经被访问
- 115 isVisited[i] = true;
- 116 //获取第一个邻接点
- 117 int v = getFirstNeightbor(i);
- 118 //有邻接点
- 119 while (v != -1) {
- 120 //并且没有被访问过
- 121 if (!isVisited[v]) {
- 122 //访问节点
- 123 System.out.println("visted vertice " + v);
- 124 //从当前节点开始继续深度遍历
- 125 dfs(v);
- 126 }
- 127 //上一个邻接点已经被访问则继续查找下一个邻接点
- 128 v = getNextNeightBor(i, v);
- 129 }
- 130 }
- 131
- 132 /**
- 133 * 广度优先
- 134 */
- 135 public void bfs(){
- 136 for (int i = 0; i < verticeSize; i++) {
- 137 isVisited[i]=false;
- 138 }
- 139 for (int i = 0; i < verticeSize; i++) {
- 140 if(!isVisited[i]){
- 141 isVisited[i]=true;
- 142 System.out.println("visited vertice:"+ i);
- 143 bfs(i);
- 144 }
- 145 }
- 146 }
- 147
- 148 public void bfs(int i) {
- 149 //当做队列使用,先访问的先放入队列
- 150 LinkedList<Integer> queue = new LinkedList<>();
- 151 //找第一个邻接点
- 152 int fn = getFirstNeightbor(i);
- 153 if (fn == -1) {
- 154 return;
- 155 }
- 156 //没有被访问过
- 157 if (!isVisited[fn]) {
- 158 isVisited[fn] = true;//设置为被访问
- 159 System.out.println("visted vertice:" + fn);
- 160 queue.offer(fn);
- 161 }
- 162 //访问各个未曾访问过的邻接点
- 163 int next = getNextNeightBor(i, fn);
- 164 while (next != -1) {
- 165 if (!isVisited[next]) {
- 166 isVisited[next] = true;
- 167 System.out.println("visted vertice:" + next);
- 168 queue.offer(next);
- 169 }
- 170 next = getNextNeightBor(i, next);
- 171 }
- 172 //从队列中取出来一个,重复之前的操作
- 173 while(!queue.isEmpty()){
- 174 int point=queue.poll();
- 175 bfs(point);
- 176 }
- 177 }
- 178}