1、图的定义
图通常有个固定的形状,这是由物理或抽象的问题所决定的。比如图中节点表示城市,而边可能表示城市间的班机航线。如下图是美国加利福利亚简化的高速公路网:
①、邻接:如果两个顶点被同一条边连接,就称这两个顶点是邻接的。如上图 I 和 G 就是邻接的,而 I 和 F 就不是。
②、路径:从某顶点到另一顶点经过的边的序列。比如从顶点B到顶点J的路径为 BAEJ。
③、连通图和非连通图:至少有一条路径可以连接起所有的顶点,则称为连通图;反之为非连通图。
④、有向图和无向图:如果边是有方向的,可以从任意一边到达另一边,则称为有向图;反之为无向图。
⑤、有权图和无权图:边被赋予一个权值,能代表两个顶点间的物理距离,这种图被称为有权图;反之为无权图。
2、在程序中表示图
①、顶点:
在大多数情况下,顶点表示某个真实世界的对象,这个对象必须用数据项来描述。通常用一个顶点类的对象来表示一个顶点,这里我们仅仅在顶点中存储了一个字母来标识顶点,同时还有一个标志位,用来判断该顶点有没有被访问过(用于后面的搜索)。
package yrwan13;
// 顶点类
public class Vertex {
public char label;
public boolean wasVisited;
public Vertex(char label) {
this.label = label;
wasVisited = false;
}
}
②、边:
图没有固定的结构,图的每个顶点可以与任意多个顶点相连,为了模拟这种自由形式的组织结构,用如下两种方式表示图:邻接矩阵和邻接表(如果一条边连接两个顶点,那么这两个顶点就是邻接的)。
邻接矩阵:
邻接矩阵是一个二维数组,数据项表示两点间是否存在边,如果图中有 N 个顶点,邻接矩阵就是 N*N 的数组。上图用邻接矩阵表示如下:
1表示有边,0表示没有边,也可以用布尔变量true和false来表示。顶点与自身相连用 0 表示,所以这个矩阵从左上角到右上角的对角线全是 0 。
邻接表:
邻接表是一个链表数组(或者是链表的链表),每个单独的链表表示了有哪些顶点与当前顶点邻接。
3、搜索
在图中实现最基本的操作之一就是搜索从一个指定顶点可以到达哪些顶点,比如从武汉出发的高铁可以到达哪些城市,一些城市可以直达,一些城市不能直达。现在有一份全国高铁模拟图,要从某个城市(顶点)开始,沿着铁轨(边)移动到其他城市(顶点),有两种方法可以用来搜索图:深度优先搜索(DFS)和广度优先搜索(BFS)。它们最终都会到达所有连通的顶点,深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现,不同的实现机制导致不同的搜索方式。
①、深度优先搜索(DFS)
深度优先搜索算法有如下规则:
- 如果可能,访问一个邻接的未访问顶点,标记它,并将它放入栈中。
- 当不能执行规则 1 时,如果栈不为空,就从栈中弹出一个顶点。
- 当不能执行规则 1 和规则 2 时,就完成了整个搜索过程。
对于上图,应用深度优先搜索如下:假设选取 A 顶点为起始点,并且按照字母优先顺序进行访问,那么应用规则 1 ,接下来访问顶点 B,然后标记它,并将它放入栈中;再次应用规则 1,接下来访问顶点 F,再次应用规则 1,访问顶点 H。我们这时候发现,没有 H 顶点的邻接点了,这时候应用规则 2,从栈中弹出 H,这时候回到了顶点 F,但是我们发现 F 也除了 H 也没有与之邻接且未访问的顶点了,那么再弹出 F,这时候回到顶点 B,同理规则 1 应用不了,应用规则 2,弹出 B,这时候栈中只有顶点 A了,然后 A 还有未访问的邻接点,所有接下来访问顶点 C,但是 C又是这条线的终点,所以从栈中弹出它,再次回到 A,接着访问 D,G,I,最后也回到了 A,然后访问 E,但是最后又回到了顶点 A,这时候我们发现 A没有未访问的邻接点了,所以也把它弹出栈。现在栈中已无顶点,于是应用规则 3,完成了整个搜索过程。
深度优先搜索在于能够找到与某一顶点邻接且没有访问过的顶点。这里以邻接矩阵为例,找到顶点所在的行,从第一列开始向后寻找值为1的列;列号是邻接顶点的号码,检查这个顶点是否未访问过,如果是这样,那么这就是要访问的下一个顶点,如果该行没有顶点既等于1(邻接)且又是未访问的,那么与指定点相邻接的顶点就全部访问过了(后面会用算法实现)。
②、广度优先搜索(BFS)
深度优先搜索要尽可能的远离起始点,而广度优先搜索则要尽可能的靠近起始点,它首先访问起始顶点的所有邻接点,然后再访问较远的区域,这种搜索不能用栈实现,而是用队列实现。
- 访问下一个未访问的邻接点(如果存在),这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。
- 如果已经没有未访问的邻接点而不能执行规则 1 时,那么从队列列头取出一个顶点(如果存在),并使其成为当前顶点。
- 如果因为队列为空而不能执行规则 2,则搜索结束。
对于上面的图,应用广度优先搜索:以A为起始点,首先访问所有与 A 相邻的顶点,并在访问的同时将其插入队列中,现在已经访问了 A,B,C,D和E。这时队列(从头到尾)包含 BCDE,已经没有未访问的且与顶点 A 邻接的顶点了,所以从队列中取出B,寻找与B邻接的顶点,这时找到F,所以把F插入到队列中。已经没有未访问且与B邻接的顶点了,所以从队列列头取出C,它没有未访问的邻接点。因此取出 D 并访问 G,D也没有未访问的邻接点了,所以取出E,现在队列中有 FG,在取出 F,访问 H,然后取出 G,访问 I,现在队列中有 HI,当取出他们时,发现没有其它为访问的顶点了,这时队列为空,搜索结束。
③、程序实现
package yrwan13;
import java.util.ArrayDeque;
import java.util.Deque;
public class Graph {
private Vertex[] vertexList;// 顶点数组
private int[][] adjMat;// 邻接矩阵
private int nVertex;// 当前数量
private Deque<Integer> stack;// 用栈实现深度优先搜索
private Deque<Integer> queue;// 用队列实现广度优先搜索
public Graph(int size) {
vertexList = new Vertex[size];
adjMat = new int[size][size];
// 初始化邻接矩阵所有元素都为0,即所有顶点都没有边
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
adjMat[i][j] = 0;
}
}
nVertex = 0;// 初始化顶点个数为0
stack = new ArrayDeque<>();
queue = new ArrayDeque<>();
}
public void addVertex(char label) {
vertexList[nVertex++] = new Vertex(label);
}
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
public void displayAdjMat() {
for (int i = 0; i < nVertex; i++) {
for (int j = 0; j < nVertex; j++) {
System.out.print(adjMat[i][j] + " ");
}
System.out.println();
}
}
// 打印某个顶点表示的值
public void display(int v) {
System.out.print(vertexList[v].label);
}
// 找到与某一顶点邻接且未被访问的顶点
public int getAdjUnvisitedVertex(int v) {
for (int i = 0; i < nVertex; i++) {
if (adjMat[v][i] == 1 && vertexList[i].wasVisited == false) {
return i;
}
}
return -1;
}
/**
* 深度优先搜索算法:
* 1、用peek()方法检查栈顶的顶点
* 2、用getAdjUnvisitedVertex()方法找到当前栈顶点邻接且未被访问的顶点
* 3、第二步方法返回值不等于-1则找到下一个未访问的邻接顶点,访问这个顶点,并入栈;如果第二步方法返回值等于 -1,则没有找到,出栈
*/
public void dfs() {
// 从第一个顶点开始访问
vertexList[0].wasVisited = true;// 访问之后标记为true
display(0);// 打印访问的第一个顶点
stack.push(0);// 将第一个顶点放入栈中
while (!stack.isEmpty()) {
// 找到栈当前顶点邻接且未被访问的顶点
int v = getAdjUnvisitedVertex(stack.peek());
if (v == -1) {// 如果当前顶点值为-1,则表示没有邻接且未被访问顶点,那么出栈顶点
stack.pop();
} else {// 否则访问下一个邻接顶点
vertexList[v].wasVisited = true;
display(v);
stack.push(v);
}
}
// 搜索完毕,重置所有标记位
for (int i = 0; i < nVertex; i++) {
vertexList[i].wasVisited = false;
}
}
/**
* 广度优先搜索算法:
* 1、用remove()方法检查栈顶的顶点
* 2、试图找到这个顶点还未访问的邻节点
* 3、 如果没有找到,该顶点出列
* 4、 如果找到这样的顶点,访问这个顶点,并把它放入队列中
*/
public void bfs() {
vertexList[0].wasVisited = true;
display(0);
queue.offer(0);
while (!queue.isEmpty()) {
int temp = queue.poll();
int v;
while ((v = getAdjUnvisitedVertex(temp)) != -1) {
vertexList[v].wasVisited = true;
display(v);
queue.offer(v);
}
}
// 搜索完毕,重置所有标记位
for (int i = 0; i < nVertex; i++) {
vertexList[i].wasVisited = false;
}
}
}