图的定义和常用术语
图是一系列顶点(结点)和描述顶点之间的关系边(弧)组成。图是数据元素的集合,这些数据元素相互连接形成网络。其形式化定义为:G=(V,E)。其中,G表示图,V是顶点的集合,E是边或弧的集合。并且E可以表示为:E=(Vi,Vj),表示顶点Vi和Vj之间有边或弧相连。
顶点集:图中具有相同特性的数据元素的集合;
边(弧):边是一对顶点间的路径,通常带箭头的边称为弧;
度:在无向图中顶点的度是指连接那个顶点的边的数目。在有向图中,每个顶点有两种类型的度(入度、出度);
入度:指向那个顶点的边的数目;
出度:由那个顶点出发的边的数目;
权:有些图的边或弧附带有一些数据信息,这些数据信息称为边或弧的权。在实际问题中,权可以表示某种含义,比如在一个地方的交通图上,边上的权值表示该条线路的长度。
有向图:在一个图中,如果任意两顶点构成的偶对(如果存在)是有序的,那么称该图为有向图;
无向图:在一个图中,如果任意两顶点构成的偶对(如果存在)是无序的,那么称该图为无向图;
有向完全图:在一个有向图中,如果任意两个顶点之间都是有弧相连,则称该图是完全有向图;
有向完全图:在一个无向图中,如果任意两个顶点之间都是有边相连,则称该图是完全无向图;
稀疏图:有很少条边或弧的图;
稠密图:有很多条边或弧的图。
图的实现
一个图的信息主要包括两部分:图中顶点的信息以及描述顶点之间的关系——边或弧的信息。
邻接矩阵和邻接表是图的两种最通用的存储结构。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface IGraph<E> { int getNumOfVertex(); boolean insertVex(E v); boolean deleteVex(E v); int indexOfVex(E v); E valueOfVex(int v); boolean insertEdge(int v1, int v2,int weight); boolean deleteEdge(int v1, int v2); int getEdge(int v1,int v2); String depthFirstSearch(int v ); String breadFirstSearch(int v ); int[] dijkstra(int v); }
|
邻接矩阵
邻接矩阵是用两个数组来表示图:
- 一个数组是一维数组,存储图中的顶点信息;
- 一个数组是二维数组,即矩阵,存储顶点之间相邻的信息,也就是边或弧的信息。
如果图中有n个顶点,就需要n×n的二维数组来表示图。
关于权值的说明:
- 如果图的边没有权值,用0表示顶点之间无边,用1表示顶点之间有边。
- 如果图的弧有权值,用无穷大表示顶点之间无边,用权值表示顶点之间有边,同一点之间的权值为0。
注意:如果图为稀疏图的话,在用邻接矩阵表示图的时候,由于在表示边的时候会导致变成一个系数矩阵,导致很多的浪费。所以,应在图为稠密图的时候使用邻接矩阵实现图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| public class GraphAdjMatrix<E> implements IGraph<E> { private E[] vexs; private int[][] edges; private int numOfVexs; private int maxNumOfVexs; private boolean[] visited; public GraphAdjMatrix(int maxNumOfVexs, Class type) { this.maxNumOfVexs = maxNumOfVexs; edges = new int[maxNumOfVexs][maxNumOfVexs]; vexs = (E[]) Array.newInstance(type, maxNumOfVexs); } public int getNumOfVertex() { return numOfVexs; } public boolean insertVex(E v) { if (numOfVexs >= maxNumOfVexs) return false; vexs[numOfVexs++] = v; return true; } public boolean deleteVex(E v) { for (int i = 0; i < numOfVexs; i++) { if (vexs[i].equals(v)) { for (int j = i; j < numOfVexs - 1; j++) { vexs[j] = vexs[j + 1]; } vexs[numOfVexs - 1] = null; for (int col = i; col < numOfVexs - 1; col++) { for (int row = 0; row < numOfVexs; row++) { edges[col][row] = edges[col + 1][row]; } } for (int row = i; row < numOfVexs - 1; row++) { for (int col = 0; col < numOfVexs; col++) { edges[col][row] = edges[col][row + 1]; } } numOfVexs--; return true; } } return false; } public int indexOfVex(E v) { for (int i = 0; i < numOfVexs; i++) { if (vexs[i].equals(v)) { return i; } } return -1; } public E valueOfVex(int v) { if (v < 0 ||v >= numOfVexs ) return null; return vexs[v]; } public boolean insertEdge(int v1, int v2, int weight) { if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); edges[v1][v2] = weight; edges[v2][v1] = weight; return true; } public boolean deleteEdge(int v1, int v2) { if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); edges[v1][v2] = 0; edges[v2][v1] = 0; return true; } public int getEdge(int v1,int v2){ if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); return edges[v1][v2]; } public String depthFirstSearch(int v) {} public String breadFirstSearch(int v) {} public int[] dijkstra(int v) {} }
|
邻接表
前面的邻接矩阵方法实际上是图的一种静态存储方法。建立这种存储结构时需要预先知道图中顶点的个数。如果图结构本身需要在解决问题的过程中动态地产生,则每增加或者删除一个顶点都需要改变邻接矩阵的大小,显然这样做的效率很低。除此之外,邻接矩阵所占用的存储单元数目至于图中的顶点的个数有关,而与边或弧的数目无关,若图是一个稀疏图的话,必然造成存储空间的浪费。邻接表很好地解决了这些问题。
邻接表的存储方式是一种顺序存储与链式存储相结合的存储方式,
- 顺序存储部分用来保存图中的顶点信息,
- 链式存储部分用来保存图中边或弧的信息。
具体的做法是,使用一个一维数组保存图中顶点的信息,数组中每个数组元素包含两个域。
邻接表是一个数组,数组的每个元素包含顶点信息和单链表的头指针两部分。而单链表的结构分成与顶点相邻的元素信息、边的信息和下一个与顶点相邻的元素指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
| public class GraphAdjList<E> implements IGraph<E> { private static class ENode { int adjvex; int weight; ENode nextadj; public ENode(int adjvex, int weight) { this.adjvex = adjvex; this.weight = weight; } } private static class VNode<E> { E data; ENode firstadj; } private VNode[] vexs; private int numOfVexs; private int maxNumOfVexs; private boolean[] visited; public GraphAdjList(int maxNumOfVexs) { this.maxNumOfVexs = maxNumOfVexs; vexs = (VNode[]) Array.newInstance(VNode.class, maxNumOfVexs); } public int getNumOfVertex() { return numOfVexs; } public boolean insertVex(E v) { if (numOfVexs >= maxNumOfVexs) return false; VNode vex = new VNode(); vex.data = v; vexs[numOfVexs++] = vex; return true; } public boolean deleteVex(E v) { for (int i = 0; i < numOfVexs; i++) { if (vexs[i].data.equals(v)) { for (int j = i; j < numOfVexs - 1; j++) { vexs[j] = vexs[j + 1]; } vexs[numOfVexs - 1] = null; numOfVexs--; ENode current; ENode previous; for (int j = 0; j < numOfVexs; j++) { if (vexs[j].firstadj == null) continue; if (vexs[j].firstadj.adjvex == i) { vexs[j].firstadj = null; continue; } current = vexs[j].firstadj; while (current != null) { previous = current; current = current.nextadj; if (current != null && current.adjvex == i) { previous.nextadj = current.nextadj; break; } } } for (int j = 0; j < numOfVexs; j++) { current = vexs[j].firstadj; while (current != null) { if (current.adjvex > i) current.adjvex--; current = current.nextadj; } } return true; } } return false; } public int indexOfVex(E v) { for (int i = 0; i < numOfVexs; i++) { if (vexs[i].data.equals(v)) { return i; } } return -1; } public E valueOfVex(int v) { if (v < 0 || v >= numOfVexs) return null; return vexs[v].data; } public boolean insertEdge(int v1, int v2, int weight) { if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); ENode vex1 = new ENode(v2, weight); if (vexs[v1].firstadj == null) { vexs[v1].firstadj = vex1; } else { vex1.nextadj = vexs[v1].firstadj; vexs[v1].firstadj = vex1; } ENode vex2 = new ENode(v1, weight); if (vexs[v2].firstadj == null) { vexs[v2].firstadj = vex2; } else { vex2.nextadj = vexs[v2].firstadj; vexs[v2].firstadj = vex2; } return true; } public boolean deleteEdge(int v1, int v2) { if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); ENode current = vexs[v1].firstadj; ENode previous = null; while (current != null && current.adjvex != v2) { previous = current; current = current.nextadj; } if (current != null) previous.nextadj = current.nextadj; current = vexs[v2].firstadj; while (current != null && current.adjvex != v1) { previous = current; current = current.nextadj; } if (current != null) previous.nextadj = current.nextadj; return true; } public int getEdge(int v1, int v2) { if (v1 < 0 || v2 < 0 || v1 >= numOfVexs || v2 >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); ENode current = vexs[v1].firstadj; while (current != null) { if (current.adjvex == v2) { return current.weight; } current = current.nextadj; } return 0; } public String depthFirstSearch(int v) {} public String breadFirstSearch(int v) {} public int[] dijkstra(int v) {} }
|
图的遍历
图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次并且只访问一次。图的遍历是图的一种基本操作,图中的许多其他操作也都是建立在遍历的基础之上。在图中,没有特殊的顶点被指定为起始顶点,图的遍历可以从任何顶点开始。
深度优先搜索算法
算法的思想
从图中的某一个顶点x出发,访问x,然后遍历任何一个与x相邻的未被访问的顶点y,再遍历任何一个与y相邻的未被访问的顶点z……依次类推,直到到达一个所有邻接点都被访问的顶点为止;然后,依次回退到尚有邻接点未被访问过的顶点,重复上述过程,直到图中的全部顶点都被访问过为止。
算法实现的思想
深度优先遍历背后基于堆栈,有两种方式:
- 第一种是在程序中显示构造堆栈,利用压栈出栈操作实现;
- 第二种是利用递归函数调用,基于递归程序栈实现。
本文介绍第一种方式:
- 访问起始顶点,并将其压入栈中;
- 从栈中弹出最上面的顶点,将与其相邻的未被访问的顶点压入栈中;
- 重复第二步,直至栈为空栈。
未被访问的顶点怎么识别呢?利用visited数组来进行标记。
算法的实现
基于邻接矩阵的算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public String depthFirstSearch(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); visited = new boolean[numOfVexs]; StringBuilder sb = new StringBuilder(); Stack stack = new Stack(); stack.push(v); visited[v] = true; while (!stack.isEmpty()) { v = stack.pop(); sb.append(vexs[v] + ","); for (int i = numOfVexs - 1; i >= 0; i--) { if (edges[v][i] != 0 && edges[v][i] != Integer.MAX_VALUE && !visited[i]) { stack.push(i); visited[i] = true; } } } return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : null; }
|
基于邻接表的算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public String depthFirstSearch(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); visited = new boolean[numOfVexs]; StringBuilder sb = new StringBuilder(); Stack stack = new Stack(); stack.push(v); visited[v] = true; ENode current; while (!stack.isEmpty()) { v = stack.pop(); sb.append(vexs[v].data + ","); current = vexs[v].firstadj; while (current != null) { if (!visited[current.adjvex]) { stack.push(current.adjvex); visited[current.adjvex] = true; } current = current.nextadj; } } return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : null; }
|
广度优先搜索算法
算法的思想
从图中的某一个顶点x出发,访问x,然后访问与x所相邻的所有未被访问的顶点x1、x2……xn,接着再依次访问与x1、x2……xn相邻的未被访问的所有顶点。依次类推,直至图中的每个顶点都被访问。
算法实现的思想
广度优先遍历背后基于队列,下面介绍一下具体实现的方法:
- 访问起始顶点,并将插入队列;
- 从队列中删除队头顶点,将与其相邻的未被访问的顶点插入队列中;
- 重复第二步,直至队列为空。
未被访问的顶点怎么识别呢?利用visited数组来进行标记。
算法的实现
基于邻接矩阵的算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public String breadFirstSearch(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); visited = new boolean[numOfVexs]; StringBuilder sb = new StringBuilder(); Queue queue = new LinkedList(); queue.offer(v); visited[v] = true; while (!queue.isEmpty()) { v = queue.poll(); sb.append(vexs[v] + ","); for (int i = 0; i < numOfVexs; i++) { if (edges[v][i] != 0 && edges[v][i] != Integer.MAX_VALUE && !visited[i]) { queue.offer(i); visited[i] = true; } } } return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : null; }
|
基于邻接表的算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public String breadFirstSearch(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); visited = new boolean[numOfVexs]; StringBuilder sb = new StringBuilder(); Queue queue = new LinkedList(); queue.offer(v); visited[v] = true; ENode current; while (!queue.isEmpty()) { v = queue.poll(); sb.append(vexs[v].data + ","); current = vexs[v].firstadj; while (current != null) { if (!visited[current.adjvex]) { queue.offer(current.adjvex); visited[current.adjvex] = true; } current = current.nextadj; } } return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : null; }
|
最短路径
最短路径的问题是比较典型的应用问题。在图中,确定了起始点和终点之后,一般情况下都可以有很多条路径来连接两者。而边或弧的权值最小的那一条路径就称为两点之间的最短路径,路径上的第一个顶点为源点,最后一个顶点为终点。
Dijkstra算法
算法特点
Dijkstra算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
该算法的时间复杂度是n^2^,可以使用堆优化。
但是要注意一点,Dijkstra算法只能适用于权值为正的情况下;如果权值存在负数,则不能使用。
算法思想
- 设置两个顶点集S和T,集合S中存放已经找到最短路径的顶点,集合T中存放着当前还未找到最短路径的顶点;
- 初始状态下,集合S中只包含源点V1,T中为除了源点之外的其余顶点,此时源点到各顶点的最短路径为两个顶点所连的边上的权值,如果源点V1到该顶点没有边,则最小路径为无穷大;
- 从集合T中选取到源点V1的路径长度最短的顶点Vi加入到集合S中;
- 修改源点V1到集合T中剩余顶点Vj的最短路径长度。新的最短路径长度值为Vj原来的最短路径长度值与顶点Vi的最短路径长度加上Vi到Vj的路径长度值中的较小者;
- 不断重复步骤3、4,直至集合T的顶点全部加入到集合S中。
由此分析下来,我们可以得到以下几点:
需要设立两个数组:
一个数组为diatance,用于存放个顶点距离源点的距离;
另一个数组为st,用于判断顶点是在哪一个集合内(true为在S集合,false为在T集合内)。
Dijkstra算法的精髓:
每次循环都将T集合内距离源点最近的那个点加入到S集合中,且加入的那个点距离源点的距离由“最短距离估计值”转变成“最短距离准确值”;
每次循环添加一个点到S集合中后,会导致与加入的那个点相邻的顶点可能会发生距离的更新,也就是“最短距离估计值”的更新。更新方法是取原本的“最短距离估计值”与新加入的那个点的“最短距离确定值”+新加入的那个点与其邻点的距离的较小者。
“最短距离估计值”的真正内涵:其实可以把S集合看成一个黑箱子,“最短距离估计值”就是该顶点经过黑箱子里的各个点到源点的最短距离,但不能保证该顶点是否可以通过黑箱子外(T集合)的顶点绕路达到更短。只有每次循环中“最短距离估计值”中的最小值,才能确定为“最短距离确定值”加入到集合S。
算法实现
基于邻接矩阵的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public int[] dijkstra(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); boolean[] st = new boolean[numOfVexs]; int[] distance = new int[numOfVexs];
for (int i = 0; i < numOfVexs; i++){ for (int j = i + 1; j < numOfVexs; j++) { if (edges[i][j] == 0) { edges[i][j] = Integer.MAX_VALUE; edges[j][i] = Integer.MAX_VALUE; } } } for (int i = 0; i < numOfVexs; i++) { distance[i] = edges[v][i]; } st[v] = true; for (int i = 0; i < numOfVexs; ++i) { int min = Integer.MAX_VALUE; int index = -1; for (int j = 0; j < numOfVexs; ++j) { if (st[j]==false) { if (distance[j] < min) { index = j; min = distance[j]; } } } if(index != -1){ st[index] = true; } for (int w = 0; w < numOfVexs; w++){ if (st[w] == false) { if (edges[index][w] != Integer.MAX_VALUE && (min + edges[index][w] < distance[w])) distance[w] = min + edges[index][w]; } } } return distance; }
|
基于邻接表的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public int[] dijkstra(int v) { if (v < 0 || v >= numOfVexs) throw new ArrayIndexOutOfBoundsException(); boolean[] st = new boolean[numOfVexs]; int[] distance = new int[numOfVexs]; for (int i = 0; i < numOfVexs; i++) { distance[i] = Integer.MAX_VALUE; } ENode current; current = vexs[v].firstadj; while (current != null) { distance[current.adjvex] = current.weight; current = current.nextadj; } distance[v] = 0; st[v] = true; for (int i = 0; i < numOfVexs; i++) { int min = Integer.MAX_VALUE; int index = -1; for (int j = 0; j < numOfVexs; j++) { if (st[j] == false) { if (distance[j] < min) { index = j; min = distance[j]; } } } if (index != -1){ st[index] = true; } for (int w = 0; w < numOfVexs; w++){ if (st[w] == false) { current = vexs[w].firstadj; while (current != null) { if (current.adjvex == w){ if (min + current.weight < distance[w]) { distance[w] = min + current.weight; break; } } current = current.nextadj; } } } } return distance; }
|