图的遍历

图的遍历

在之前的章节当中,我们介绍了 图结构图的存储结构,所以可以得知 图的存储结构 主要有五种方式,有两种使用较多的『邻接矩阵』和『邻接表』,另外还有三种使用较少的『十字链表』,『邻接多重表』和『边集数组』,本章我们主要来看一下图的遍历

图的遍历

在之前介绍过的二叉树的遍历当中,我们提到来四种遍历方式,它们分别是『前序遍历』,『中序遍历』,『后序遍历』和『层序遍历』,针对于二叉树而言,因为根结点只有一个,并且所有的结点都只有一个双亲,所以我们实现起来不算太过困难

但是针对图的遍历,因为它的任一顶点都可以和其余的所有顶点相邻接,所以极有可能存在重复走过某个顶点或漏了某个顶点的遍历过程,所以对于图的遍历,如果要避免以上情况,那就需要科学地设计遍历方案,通常有两种遍历方案,它们是『深度优先遍历』和『广度优先遍历』,下面我们就一个一个来进行了解

深度优先遍历

深度优化遍历(DepthFirstSearch),也有称为深度优化搜索,简称为 DFS,我们以下面这个图为例

如果我们想要进行遍历,我们可以约定右手原则,即在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号,如果走到终点的时候发现分叉路都是已经被标记过的,则执行回退操作,一直回退到起点,比如针对上图,它的执行流程是

  • 我们首先从 A 出发开始进行遍历,按照我们的右手原则(比如上例当中就是我们面对着 A),它会依次经过 A,B,C,D,E,最终到达 F
  • 到达 F 以后可以发现,AE 都是已经被标记过了,所以它会走向 G,这时发现 BD 也都是已经标记过了,所以会走向 H
  • 到达 H 以后会发现,E,D,G 都是已经标记过了,所以这时就会回退到 G 到位置,同理此时可以发现 B,D,F 也都是标记过了,所以依次经过 F,E,一直会回退到 D 到位置
  • 到达 D 到位置以后会发现 C 是被标记过到,但是 I 是没有标记过到,所以会走向 I,下一步同理,发现 BC 都是被标记过到,所以会往回走,在依次经过 D,C,B 回到 A 到位置上
  • 回到 A 到位置上以后就算是结束遍历过程,这就是『深度优先遍历』

简单总结一下可以发现,『深度优先遍历』其实就是一个『递归』的过程,如果再细心观察,可以发现,其实整个遍历过程就像是一棵树的『前序遍历』,我们将上面到流程简单总结一下其实就是下图这样到流程

深度优先遍历的代码实现

下面我们来看看如何进行实现,图结构的生成可以参考我们在 图的存储结构 当中介绍过的邻接矩阵的实现,所以在这里我们只介绍遍历方法,以邻接矩阵为例

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
// 访问标志数组,遍历时使用
let visited = []

// 邻接矩阵的深度优先递归算法
function DFS(i) {

// 访问过的顶点设置为 TRUE
visited[i] = true
console.log('打印顶点:', G.vexs[i])

for (let j = 0; j < G.numVertexes; j++) {
if (G.arc[i][j] == 1 && !visited[j]) {
console.log(G.vexs[i], '->', G.vexs[j])
// 对未访问的顶点进行递归
DFS(j)
}
}
}

// 邻接矩阵的深度遍历操作
function DFSTraverse() {
for (let i = 0; i < G.numVertexes; i++) {
// 初始化所有顶点状态都是未访问过状态
visited[i] = false;
}

for (let i = 0; i < G.numVertexes; i++) {
// 若是连通图,只会执行一次
if (!visited[i])
DFS(i)
}
}

马踏棋盘算法

马踏棋盘问题(又称骑士周游或骑士漫游问题)是算法设计的经典问题之一,题目是这样的,国际象棋的棋盘为 8 * 8 的方格棋盘,现将马放在任意指定的方格中,按照马走棋的规则将马进行移动,要求每个方格只能进入一次,最终使得马走遍棋盘 64 个方格,如下图所示,当马在棋盘上任意一点当时候,它的走法会有八种

在此之前,我们先来了解两个概念

  • 回溯法
    • 之前在 递归 章节中我们解决八皇后问题的时候,我们曾经提到过回溯法,简单来说就是一条路走到黑,碰壁了再回来一条路走到黑,如此循环
    • 一般和递归可以很好的搭配使用,还有深度优先搜索(DFS
  • 哈密尔顿路径
    • G 中的哈密尔顿路径指的是经过图 G 中每个顶点,且『只经过一次的一条轨迹』
    • 如果这条轨迹是一条闭合的路径(从起点出发不重复地遍历所有点后仍能回到起始点),那么这条路径称为『哈密尔顿回路』

关于马踏棋盘问题,主要的解决方案有两种,一种是基于『深度优先搜索』的方法,另一种是基于『贪婪算法』的方法

  • 第一种基于深度优先搜索的方法是比较常用的算法,深度优先搜索算法也是数据结构中的经典算法之一,主要是采用递归的思想,一级一级的寻找,遍历出所有的结果,最后找到合适的解
  • 第二种基于贪婪的算法则是制定贪心准则,一旦设定不能修改,他只关心局部最优解,但不一定能得到最优解

我们先来简单的分析一下,其实可以分为两种情况

  • 在四角,马踏日走只有两个选择
  • 在其余部分,马踏日走有四、六、八不等的选择

所以我们可以在外层另外加上两层,确保 8 * 8 方格中的每一个格子都有 8 中不同的选择,为了确保每个格子能走日字,而且选择的可能性等同,初始化除了最外两层格子标记没有被访问,最外两层中每个格子都标记为已被访问即可达到目标,如下图所示

如上,为了确保每个格子能走日字,而且选择的可能性等同,初始化除了最外两层格子标记没有被访问,最外两层中每个格子都标记为已被访问即可达到目标,并且每一个表格中马在访问时都有 8 种不同的选择,这 8 种不同的选择都会在其相应的 xy 坐标上进行追加标记,如下所示

x y
2 1
1 2
-1 2
-2 1
-2 -1
-1 -2
1 -2
2 -1

先来看第一种实现方式,递归求解(回溯法求解),列出所有的解,并从中找出从 (2, 2) 位置出发的合适解

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
#include <iostream>
#include <stdlib.h>

using namespace std;

int chessboard[12][12] = {0};

// 标记马已走的方格数
int cnt = 0;

// 标记马走完全程的具体方案数
int sum = 0;

// 初始马当前位置向其周围相邻八个日字的 x, y 的偏移量
int move[8][2] = {{2, 1}, {1, 2}, {-1, 2}, {-2, 1}, {-2, -1}, {-1, -2}, {1, -2}, {2, -1}};

// 输出马踏棋盘的解
void PrintChess();

// 马踏棋盘递归过程
void Horse(int x, int y);

int main(void) {
int i, j;
for (i = 0; i < 12; i++) {
for (j = 0; j < 12; j++) {
if (i == 0 || i == 1 || i == 10 || i == 11 || j == 0 || j == 1 || j == 10 || j == 11) {
// 在 8 * 8 的外层再加上两层,确保 8 * 8 方格中的每一个格子都有 8 种不同的日字选择
chessboard[i][j] = -1;
}
}
}

// 从起始位置开始求得所有解
chessboard[2][2] = ++cnt;

// 递归调用当前当前位置附近的 8 个日字,看看是否满足条件
Horse(2, 2);
return 0;
}

// 马永远踏的是 x, y 位置,而不是 a, b
void Horse(int x, int y) {

// 临界值,马走日字全部踏完,成功求出问题解
if (cnt >= 64) {
sum++;
PrintChess();
return;
}

for (int i = 0; i < 8; i++) {

int a = x + move[i][0]; // 拿到当前马位置相邻的 8 个日字的 x 坐标
int b = y + move[i][1]; // 拿到当前马位置相邻的 8 个日字的 y 坐标

// 判断当前马位置相邻的日字是否已被访问
if (chessboard[a][b] == 0) {
cnt++;
chessboard[a][b] = cnt; // 标志已被访问
Horse(a, b); // 从当前马的位置继续往下访问
cnt--;
chessboard[a][b] = 0; // 回溯回来修改其相邻的日字的访问情况
}
}
}

// 输出马踏棋盘的解
void PrintChess() {
cout << endl << "马踏棋盘第 " << sum << "组解为:" << endl;
int i, j;
for (i = 2; i < 10; i++) {
for (j = 2; j < 10; j++) {
cout << " " << chessboard[i][j];
}
cout << endl;
}
}

再来看看贪心算法求解,列出从 (2, 2) 位置出发的合适解,局部最优

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
#include <iostream>
#include <stdlib.h>

using namespace std;

/*

typedef struct{
int x; // 记录当前马位置的 x 坐标
int y; // 记录当前马位置的 y 坐标
int i; // 记录从当前马的位置前往下一个日字的序号 i (0 < i < 8)
}StackHorse;

*/

// 申请一个栈空间(里面存储的就是 x,y,i 三个具体的变量值)来标记马走的具体位置
int StackHorse[100][3] = {0};

// 记录 8 * 8 棋盘马走的具体脚印
int chessboard[12][12] = {0};

// 标记马已走的方格数
int cnt = 1;

// 初始马当前位置向其周围相邻八个日字的 x, y 的偏移量
int move[8][2] = {{2, 1}, {1, 2}, {-1, 2}, {-2, 1}, {-2, -1}, {-1, -2}, {1, -2}, {2, -1}};

// 输出马踏棋盘的解
void PrintChess();

// 马踏棋盘递归过程
void Horse(int x, int y);

int main(void) {
int i, j;
// 初始化马踏棋盘的具体值(0 代表未被访问,1 代表已被访问,-1 代表新加的最外面两层)
for (i = 0; i < 12; i++) {
for (j = 0; j < 12; j++) {
if (i == 0 || i == 1 || i == 10 || i == 11 || j == 0 || j == 1 || j == 10 || j == 11) {
chessboard[i][j] = -1;
}
}
}

// 从 (2, 2) 的位置开始跑,求得马踏棋盘的一组解
Horse(2, 2);
PrintChess();

return 0;
}

// 非递归求一组解的过程
void Horse(int x, int y) {
int top = 0, i = 0;
int a, b; // 记录当前马位置附近的日字坐标
chessboard[x][y] = 1; // 标记当前起始位置已被访问

// 记录当前马的位置
StackHorse[top][0] = StackHorse[top][1] = 2;

while (cnt < 64) {
for (; i < 8; i++) {
a = x + move[i][0];
b = y + move[i][1];

// 如果当前马位置附近的日字没有被访问,跳出循环
if (chessboard[a][b] == 0) {
break;
}
}

// 能够访问当前马位置附近的日字
if (i < 8) {
chessboard[a][b] = ++cnt;
StackHorse[top][2] = i; // 记录访问当前马位置附近的日字序号 (0 < i < 8)
top++; // top 指向新的栈顶
StackHorse[top][0] = a; // 向新的栈顶放入马踏入的 x 坐标
StackHorse[top][1] = b; // 向新的栈顶放入马踏入的 y 坐标
x = a; // 标记新的 x
y = b; // 标记新的 y
i = 0; // 从栈顶马位置开始寻找附近的 8 个日字
} else {
// 没有在当前马位置附近找到符合条件的日字
cnt--; // 回溯
chessboard[x][y] = 0;
top--; // 出栈
x = StackHorse[top][0]; // 拿到当前马位置的 x 坐标
y = StackHorse[top][1]; // 拿到当前马位置的 y 坐标
i = StackHorse[top][2]; // 拿到当前马位置前往下一日字的序号
i++; // 继续搜索从当前马位置访问的日字序号的下一位置继续访问
}
}
}

// 输出马踏棋盘的解
void PrintChess() {
cout << "马踏棋盘一组解为:" << endl;
int i, j;
for (i = 2; i < 10; i++) {
for (j = 2; j < 10; j++) {
cout << " " << chessboard[i][j];
}
cout << endl;
}
}

广度优先遍历

广度优先遍历(BreadthFirstSearch),又称为广度优先搜索,简称 BFS,是最简便的图的搜索算法之一,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果,换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止,我们可以对照下图来进行理解

要实现对上图的广度遍历,我们可以利用队列来实现,比如我们以 A 为起点,流程如下

广度优先遍历的代码实现

同深度优先遍历一样,我们这里也只介绍遍历算法的实现,原理是借助队列来进行实现

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
// 邻接矩阵 的广度遍历算法
function BFSTraverse() {
// 初始化队列
let queue = []

for (let i = 0; i < G.numVertexes; i++) {
// 初始化所有顶点状态都是未访问过状态
visited[i] = false
}

// 对每一个顶点做循环
for (let i = 0; i < G.numVertexes; i++) {
// 如果没有访问过就处理
if (!visited[i]) {

// 访问过的顶点设置为TRUE
visited[i] = true
console.log('打印顶点:', G.vexs[i])

// 将此顶点入队列
queue.push(i)

// 退出条件为当前队列不为空
while (queue.length != 0) {
queue.shift()

for (let j = 0; j < G.numVertexes; j++) {
// 判断其他顶点若与当前顶点存在边且未访问过
if (G.arc[i][j] == 1 && !visited[j]) {
visited[j] = true
console.log(G.vexs[i], '->', G.vexs[j])
console.log('打印顶点:', G.vexs[j])

// 将此顶点放入队列
queue.push(j)
}
}
}
}
}
}
# Essay

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×