• 顶点u到顶点v是可达的,意味着:有一条从顶点v到顶点u的路径
  • 这种搜索有两种常用的方法:
  • 广度优先搜索(breadth first search,BFS)
  • 深度优先搜索(depth first search,DFS)
  • 要获得效率更高的图的算法,深度优先搜索方法使用得更多

一、广度优先搜索(BFS)


BFS原理

  • 从图的某一结点出发,首先依次访问该结点的所有邻接点Vi1,Vi2,...Vin,再按这些顶点被访问的先手次序依次访问与他们相邻接的所有未被访问的顶点
  • 重复上面的过程,直至所有顶点均被访问为止



图示案例1

  • 假设有下面的有向图,现在要搜索从顶点1可到达的所有顶点,广度优先搜索的方法如下:
  • 先确定邻接于顶点1的顶点集合,这个集合是{2,3,4}
  • 然后确定邻接于{2,3,4}的新的(即还没有到达过的)顶点集合为{5,6,7}
  • 然后确定邻接于{5,6,7}的新的顶点集合为{8,9}
  • 最终,从顶点1开始搜索,可以达到的顶点集合为{1,2,3,4,5,6,7,8,9}

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历

图示案例2

  • 假设现在有一个迷宫,现在我们需要点亮所有的灯泡

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_02

  • BFS第一步:假设我们从左上角的灯泡开始比那里,此时BFS遍历结果如下

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_深度优先搜索DFS_03

  • BFS第二步:根据第1步,我们可以遍历到3个灯泡,因此把3个邻接灯泡电量

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_04

  • BFS第三步:根据上面的结果,我们此时再去访问其他没有被访问过的邻接点,假设此次遍历箭头所指的邻接点,将其右下方的灯泡点亮,则结果如下

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_邻接矩阵_05

  • BFS第四步:根据上面的结果,我们此时再去访问其他没有被访问过的邻接点,假设此次遍历箭头所指的邻接点,将其左侧和下侧的2个灯泡点亮,则结果如下

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_广度优先搜索BFS_06

  • BFS第五步:根据上面的结果,我们此时再去访问其他没有被访问过的邻接点,假设此次遍历箭头所指的邻接点,将其右上方的灯泡点亮,则结果如下

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_07

图示案例3

  • 假设现在有下面的图

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_08

  • 则BFS结果为:V1=>V2=>V3=>V4=>V5=>V6=>V7=>V8

图示案例4

  • 假设现在我们有下面的一个非连通图

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_结点_09

  • 则BFS结果为:a、c、d、e、f、h、k、b、g



伪代码

  • 这种搜索方法可以使用队列实现,图的BFS和二叉树的层次遍历是相似的

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_结点_10

  • 如果将该伪代码应用于上面的有向图,则步骤为:
  • 伪代码中的v=1,在外层while循环的第一次迭代中,顶点2,3,4被一次加入到队列中
  • 第二次迭代中,从队列中删除顶点2,加入顶点5
  • 然后从队列中删除顶点3,但是没有加入新顶点;从队列中删除顶点4,加入顶点6和7
  • 从队列中删除顶点5,加入顶点8;从队列中删除顶点6,但是没有加入新的顶点;从队列中删除顶点7,加入顶点9
  • 最后从队列中删除8和9,队列为空,过程终止
  • 下图是遍历所经过的边:

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_11



BFS编码实现

  • 下面我们假设我们的无向图使用邻接链表进行存储

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_12

  • 然后建立一个辅助数组,用来表示节点是否被访问过,初始化全部为0表示结点没有被访问过,如果访问过了对应的索引处被置为1

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_深度优先搜索DFS_13

  • 假设我们从2节点开始遍历,则DFS结果为1->2->3->4->5->6->7->8
  • 整个代码如下:

#include <iostream>
#include <queue>
#include <string.h>

using namespace std;

#define MVNum 100 // 顶点个数

typedef int VerTexType; // 假设顶点的数据类型为int
typedef int ArcType; // 假设边的权值类型为整型

struct ArcNode;

// 顶点的结构
typedef struct VNode {
VerTexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的边的指针
} VNode, AdjList[MVNum];

// 弧(边)的结点结构
typedef struct ArcNode {
int adjvex; // 该边指向的顶点的位置
struct ArcNode* nextarc; // 指向下一条边的指针
//OtherInfo info; // 和边相关的信息(例如权等)
} ArcNode;

// 图的定义
typedef struct {
AdjList vertices;
int vexnum, arcnum; // 图的当前顶点数和弧数
} ALGraph;

bool *visited;

int LocateVex(ALGraph G, VerTexType u);

// 创建无向网的邻接表实现
int CreateUDG(ALGraph &G)
{
// 输入总顶点数,总边数
std::cout << "输入总顶点数和总边数: ";
std::cin >> G.vexnum >> G.arcnum;

// BFS使用
visited = new bool[G.vexnum];
memset(visited, false, G.vexnum);

int i, j;

// 输入各点, 构造表头结点表
std::cout << "输入" << G.vexnum << "个顶点的值:";
for(int i = 0; i < G.vexnum; ++i)
{
// 输入顶点值
std::cin >> G.vertices[i].data;
// 初始化表头结点的指针域
G.vertices[i].firstarc = NULL;
}

// 输入各边,构造邻接表
std::cout << "输入" << G.arcnum << "条边的顶点信息:" << std::endl;
for(int k = 0; k < G.arcnum; ++k)
{
// 输入一条边依附的两个顶点
int v1, v2;
std::cin >> v1 >> v2;

// 在图G中找到v1和v2对应的下标
i = LocateVex(G, v1);
j = LocateVex(G, v2);

// 生成一个新的边结点p1
ArcNode *p1 = new ArcNode;
p1->adjvex = j; // 邻接点序号为j
p1->nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p1; // 将新结点p1插入顶点Vi的边表头部

// 生成一个新的边结点p2
ArcNode *p2 = new ArcNode;
p2->adjvex = i; // 邻接点序号为i
p2->nextarc = G.vertices[j].firstarc;
G.vertices[j].firstarc = p2; // 将新结点p2插入顶点Vj的边表头部
}

return 1;
}

// 在图G中查找顶点u,存在则返回顶点表中的下标,否则返回-1
int LocateVex(ALGraph G, VerTexType u)
{
int i;
for(i = 0; i < G.vexnum; ++i)
if(u == G.vertices[i].data)
{
return i;
}

return -1;
}

void printGraph(ALGraph &G)
{
for(int i = 0; i < G.vexnum; ++i)
{
std::cout << G.vertices[i].data;
ArcNode* temp = G.vertices[i].firstarc;
while(temp != NULL)
{
std::cout << "->" << temp->adjvex;
temp = temp->nextarc;
}
std::cout << std::endl;
}
}

/********************* BFS ****************************/
int FirstAdjVex(ALGraph G, int u);
int NextAdjVex(ALGraph G, int u, int w);

void BFS(ALGraph G, int v)
{
// 输入第1个节点
std::cout << v + 1 << " ";
// 把对应的位置置为1
visited[v] = true;

// 新建一个队列, 将v入队
std::queue<int> _qu;
_qu.push(v);

// 遍历队列
while(!_qu.empty())
{
// 得到队头
int u = _qu.front();
// 出队列
_qu.pop();

for(int w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
{
// 如果w为u的尚未访问的邻接顶点
if(!visited[w])
{
// 打印w并且入队
std::cout << w + 1 << " ";
visited[w] = true;
_qu.push(w);
}
}
}
}


int FirstAdjVex(ALGraph G, int u)
{
int i;

// 如果u没有被访问过
if(u >= 0 && u < G.vexnum)
{
if(G.vertices[u].firstarc)
return G.vertices[u].firstarc->adjvex;
}

return -1;
}

// 返回u的"第一个邻接点". 若该顶点在G中没有邻接点, 则返回-1
int NextAdjVex(ALGraph G, int u, int w)
{
ArcNode *p;
if(u >= 0 && u < G.vexnum && w >= 0 && w < G.vexnum)
{
p = G.vertices[u].firstarc;
while(p->nextarc)
{
if(p->adjvex == w)
return p->nextarc->adjvex;
else
p = p->nextarc;
}
}

return -1;
}


int main()
{
ALGraph G;

// 创建无向网的邻接表实现
CreateUDG(G);

// 打印无向网
printGraph(G);

// 广度优先遍历结果
std::cout << "广度优先的遍历结果为: ";
BFS(G, 0);
std::cout << std::endl;

return 0;
}

  • 代码运行结果与预期效果一致

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_深度优先搜索DFS_14

  • 其中BFS的核心代码为BFS()函数:

int FirstAdjVex(ALGraph G, int u);
int NextAdjVex(ALGraph G, int u, int w);

void BFS(ALGraph G, int v)
{
// 输入第1个节点
std::cout << v + 1 << " ";
// 把对应的位置置为1
visited[v] = true;

// 新建一个队列, 将v入队
std::queue<int> _qu;
_qu.push(v);

// 遍历队列
while(!_qu.empty())
{
// 得到队头
int u = _qu.front();
// 出队列
_qu.pop();

for(int w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
{
// 如果w为u的尚未访问的邻接顶点
if(!visited[w])
{
// 打印w并且入队
std::cout << w + 1 << " ";
visited[w] = true;
_qu.push(w);
}
}
}
}


int FirstAdjVex(ALGraph G, int u)
{
int i;

// 如果u没有被访问过
if(u >= 0 && u < G.vexnum)
{
if(G.vertices[u].firstarc)
return G.vertices[u].firstarc->adjvex;
}

return -1;
}

// 返回u的"第一个邻接点". 若该顶点在G中没有邻接点, 则返回-1
int NextAdjVex(ALGraph G, int u, int w)
{
ArcNode *p;
if(u >= 0 && u < G.vexnum && w >= 0 && w < G.vexnum)
{
p = G.vertices[u].firstarc;
while(p->nextarc)
{
if(p->adjvex == w)
return p->nextarc->adjvex;
else
p = p->nextarc;
}
}

return -1;
}



BFS算法效率分析

  • 如果使用图邻接矩阵实现:则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行(n个元素),总的时间代价为O(C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_15)
  • 如果使用图邻接链表实现:虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)


二、深度优先搜索(DFS)


DFS原理

  • 在访问图中某一其实顶点v后,由v出发,访问它的任一邻接顶点W1
  • 再从W1触发,访问与W1邻接但还未被访问过的顶点W2
  • 然后再从W2出发,进行类似的访问......
  • 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止
  • 接着,退回一步,退到前一次刚访问过的顶点,看是否还有其他没有被访问的邻接顶点
  • 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问
  • 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止



备注

  • 连通图的深度优先遍历类似于树的先根遍历



图示案例1

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历

  • 假设v=1,那么顶点2、3、4成为u的候选:
  • 假设选择顶点2,从顶点2开始DFS:
  • 将顶点2标记为已到达顶点,这时只有顶点5,然后从顶点5开始DFS
  • 将顶点5为已到达顶点,这时只有顶点8,从顶点8开始DFS
  • 将顶点8为已到达顶点,由于从顶点8开始没有不可到达的邻接顶点,因此返回到顶点5,顶点5也没有新的可到达的邻接顶点,因此在此返回到顶点2,顶点2也没有新的可到达的邻接顶点,再次返回到顶点1
  • 现在还剩两个候选顶点3和4,假设从顶点4开始DFS:
  • 将顶点4标记为已到达顶点,现在顶点3、6、7都成为候选顶点
  • 假设选中顶点6,这时顶点3是唯一的候选,从顶点3开始DFS
  • 将顶点3标记为已到达顶点,因为没有邻接于3的顶点,所以返回到顶点6;因为没有邻接于6的新顶点,所以返回到顶点4,这时顶点7成为新的候选,然后从顶点7开始DFS
  • 将顶点7标记为已到达顶点,然后达到顶点9,而没有邻接于9的顶点,这一次我们最终返回到顶点1
  • 现在没有邻接于1的新顶点了,算法终止

图示案例2

  • 假设现在有一个迷宫,现在我们需要点亮所有的灯泡
  • 深度优先遍历如下(图中的数字就是遍历的步骤)

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_深度优先搜索DFS_17

图示案例3

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_广度优先搜索BFS_18

  • 对于上面的图,深度优先遍历的方式有如下很多种,每一行都代表一种遍历方式

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_19

图示案例4

  • 下面是一个非连通图,其一种深度优先遍历顺序为:a、c、h、d、f、k、e、b、g

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_深度优先搜索DFS_20



伪代码

  • 下图是DFS的伪代码

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_广度优先搜索BFS_21

  • 从一个顶点v出发,DFS按如下过程进行:
  • 首先将v标记为已到达的顶点,然后选择一个邻接于v的尚未到达的顶点u
  • 如果这样的u不存在,则搜索终止;如果这样的u存在,那么从u又开始一个新的DFS
  • 当这种搜索结束时,再选择另外一个邻接于v的尚未到达的顶点。如果这样的顶点不存在,那么搜索终止。如果这样的顶点存在,又从这个顶点开始进行DFS
  • 如此继续下去....



DFS编码实现

  • 下面我们假设我们的无向图使用邻接矩阵进行存储

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_22

  • 然后建立一个辅助数组,用来表示节点是否被访问过,初始化全部为0表示结点没有被访问过,如果访问过了对应的索引处被置为1

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_结点_23

  • 假设我们从2节点开始遍历,则DFS结果为2->1->3->5->4->6
  • 整个代码如下:

#include <iostream>
#include <string.h>

using namespace std;

#define MVNum 100 // 顶点个数

typedef int VerTexType; // 假设顶点的数据类型为int
typedef int ArcType; // 假设边的权值类型为整型

typedef struct
{
VerTexType vexs[MVNum]; // 顶点表
ArcType arcs[MVNum][MVNum]; // 邻接矩阵
int vecnum, arcnum; // 图的当前顶点数和边数
} AMGraph;

bool *visited;

int LocateVex(AMGraph G, VerTexType u);

// 创建无向网G
int createUDN(AMGraph &G)
{
// 输入总顶点数、总边数
std::cout << "请输入总顶点数与总边数: ";
std::cin >> G.vecnum >> G.arcnum;

// 后面DFS使用
visited = new bool[G.vecnum];
memset(visited, false, G.vecnum);

int i, j, k;

// 依次输入点的信息
std::cout << "请输入点的信息: ";
for(i = 0; i < G.vecnum; ++i)
std::cin >> G.vexs[i];

// 初始化邻接矩阵
for(i = 0; i < G.vecnum; ++i)
{
for(j = 0; j < G.vecnum; ++j)
G.arcs[i][j] = 0; // 边的权值均置为0(也可以置为极大值)
}

std::cout << "请输入" << G.arcnum << "条边的信息: " << std::endl;
for(k = 0; k < G.arcnum; ++k)
{
int v1, v2, w;
// 输入一条边所依附的顶点及边的权值
std::cin >> v1 >> v2 >> w;

// 确定v1和v2在G中的位置
i = LocateVex(G, v1);
j = LocateVex(G, v2);

G.arcs[i][j] = w; // 边<v1, v2>的权值置为w
G.arcs[j][i] = G.arcs[i][j]; // 置<v1, v2>的对称边<v2, v1>的权值为w
}

return 1;
}

// 在图G中查找顶点u,存在则返回顶点表中的下标,否则返回-1
int LocateVex(AMGraph G, VerTexType u)
{
int i;
for(i = 0; i < G.vecnum; ++i)
if(u == G.vexs[i])
return i;

return -1;
}

void printUDN(AMGraph &G)
{
for(int i = 0; i < G.vecnum; ++i)
{
for(int j = 0; j < G.vecnum; ++j)
{
std::cout << G.arcs[i][j] << " ";
}
std::cout << std::endl;
}
}

void DFS(AMGraph G, int v)
{
// 打印第v个顶点(此处v为索引, 但是我们打印的是节点数字, 因此将v+1)
std::cout << (v + 1) << " ";
// 并把对应的位置设置为true,表示已经遍历过了
visited[v] = true;

// 依次检查邻接矩阵v所在的行
for(int w = 0; w < G.vecnum; w++)
{
if((G.arcs[v][w] != 0) && (!visited[w]))
DFS(G, w);
}
}

int main()
{
AMGraph G;

// 创建无向网
createUDN(G);

// 打印无向网信息
std::cout << "无向网的邻接矩阵为: " << std::endl;
printUDN(G);

// DFS打印
std::cout << "深度优先遍历搜索的结果为: ";
DFS(G, 1);
std::cout << std::endl;

return 0;
}

  • 代码运行结果与预期效果一致

C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_广度优先搜索BFS_24

  • 其中DFS的核心代码为DFS函数:

void DFS(AMGraph G, int v)
{
// 打印第v个顶点(此处v为索引, 但是我们打印的是节点数字, 因此将v+1)
std::cout << (v + 1) << " ";
// 并把对应的位置设置为true,表示已经遍历过了
visited[v] = true;

// 依次检查邻接矩阵v所在的行
for(int w = 0; w < G.vecnum; w++)
{
if((G.arcs[v][w] != 0) && (!visited[w]))
DFS(G, w);
}
}



DFS算法效率分析

  • 当用邻接矩阵表示图时:
  • 遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(C++(数据结构与算法):57---图的遍历(广度优先搜索(BFS)、深度优先搜索(DFS))_图的遍历_15)
  • 稠密图适用于在邻接矩阵上进行深度遍历
  • 当用邻接表来表示图时:
  • 虽然有2e个表结点,但只需扫描e个节点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
  • 稀疏图适用于在邻接矩阵上进行深度遍历


三、DFS与BFS的效率比较

  • 空间复杂度相同:都是O(n)(借用了堆栈或者队列)
  • 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关