0%

图算法

图的定义和常用术语

图是一系列顶点(结点)和描述顶点之间的关系边(弧)组成。图是数据元素的集合,这些数据元素相互连接形成网络。其形式化定义为: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) {}
// 实现Dijkstra算法
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; // //邻接表的第1个结点
}

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);

// 索引为index1的顶点没有邻接顶点
if (vexs[v1].firstadj == null) {
vexs[v1].firstadj = vex1;
}
// 索引为index1的顶点有邻接顶点
else {
vex1.nextadj = vexs[v1].firstadj;
vexs[v1].firstadj = vex1;
}
ENode vex2 = new ENode(v1, weight);
// 索引为index2的顶点没有邻接顶点
if (vexs[v2].firstadj == null) {
vexs[v2].firstadj = vex2;
}
// 索引为index1的顶点有邻接顶点
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();
// 删除索引为index1的顶点与索引为index2的顶点之间的边
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;
// 删除索引为index2的顶点与索引为index1的顶点之间的边
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) {}

// 实现Dijkstra算法
public int[] dijkstra(int v) {}
}

图的遍历

图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次并且只访问一次。图的遍历是图的一种基本操作,图中的许多其他操作也都是建立在遍历的基础之上。在图中,没有特殊的顶点被指定为起始顶点,图的遍历可以从任何顶点开始。

深度优先搜索算法

算法的思想

从图中的某一个顶点x出发,访问x,然后遍历任何一个与x相邻的未被访问的顶点y,再遍历任何一个与y相邻的未被访问的顶点z……依次类推,直到到达一个所有邻接点都被访问的顶点为止;然后,依次回退到尚有邻接点未被访问过的顶点,重复上述过程,直到图中的全部顶点都被访问过为止。

算法实现的思想

深度优先遍历背后基于堆栈,有两种方式:

  • 第一种是在程序中显示构造堆栈,利用压栈出栈操作实现;
  • 第二种是利用递归函数调用,基于递归程序栈实现。

本文介绍第一种方式:

  1. 访问起始顶点,并将其压入栈中;
  2. 从栈中弹出最上面的顶点,将与其相邻的未被访问的顶点压入栈中;
  3. 重复第二步,直至栈为空栈。

未被访问的顶点怎么识别呢?利用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相邻的未被访问的所有顶点。依次类推,直至图中的每个顶点都被访问。

算法实现的思想

广度优先遍历背后基于队列,下面介绍一下具体实现的方法:

  1. 访问起始顶点,并将插入队列;
  2. 从队列中删除队头顶点,将与其相邻的未被访问的顶点插入队列中;
  3. 重复第二步,直至队列为空。

未被访问的顶点怎么识别呢?利用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算法只能适用于权值为正的情况下;如果权值存在负数,则不能使用。

算法思想

  1. 设置两个顶点集S和T,集合S中存放已经找到最短路径的顶点,集合T中存放着当前还未找到最短路径的顶点;
  2. 初始状态下,集合S中只包含源点V1,T中为除了源点之外的其余顶点,此时源点到各顶点的最短路径为两个顶点所连的边上的权值,如果源点V1到该顶点没有边,则最小路径为无穷大;
  3. 从集合T中选取到源点V1的路径长度最短的顶点Vi加入到集合S中;
  4. 修改源点V1到集合T中剩余顶点Vj的最短路径长度。新的最短路径长度值为Vj原来的最短路径长度值与顶点Vi的最短路径长度加上Vi到Vj的路径长度值中的较小者;
  5. 不断重复步骤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];// 默认初始为false
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) {
// 从源点到j顶点的最短路径还没有找到
if (st[j]==false) {
// 从源点到j顶点的路径长度最小
if (distance[j] < min) {
index = j;
min = distance[j];
}
}
}
//找到源点到索引为index顶点的最短路径长度
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];// 默认初始为false
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++) {
// 从源点到j顶点的最短路径还没有找到
if (st[j] == false) {
// 从源点到j顶点的路径长度最小
if (distance[j] < min) {
index = j;
min = distance[j];
}
}
}
// 找到源点到索引为index顶点的最短路径长度
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;
}