数据结构系列内容的学习目录 → \rightarrow 浙大版数据结构学习系列内容汇总

2. 图的遍历

2.1 深度优先搜索

  深度优先搜索(Depth First Search, DFS)属于图算法的一种,是一个针对图和树的遍历算法。深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。

  深度优先遍历图的方法是,从图中某顶点v出发:
  (1)访问顶点v;
  (2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历,直至图中和v有路径相通的顶点都被访问;
  (3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

  深度优先搜系类似于树的先序遍历,伪代码如下所示。

void DFS(Vertex V)
{ 
    visited[V] = true;
    for(V的每个邻接点W)
        if (!visited[W])
            DFS(W) ;
}

  若有N个顶点、E条边,时间复杂度:
    ⋆ \star 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
    ⋆ \star 用邻接矩阵存储图,为 O ( N 2 ) O(N^{2}) O(N2)

2.2 广度优先搜索

  广度优先搜索(又称宽度优先搜索, Breadth First Search, BFS)是连通图的一种遍历算法,也是很多重要的图的算法的原型,它属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

  广度优先搜索的基本过程:BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。一般用队列数据结构来辅助实现BFS算法。

数据结构(四)—— 图(2):图的遍历_数据结构
  广度优先搜索类似于树的层序遍历,伪代码如下所示。

void BES(Vertex V)
{ 
    visited[V] = true;
    Enqueue(V, Q);
    while(!IsEmpty (Q)){
        V = Dequeue(Q);
        for (V的每个邻接点W)
            if(!visited[W]){
                visited[W]= true;
                Enqueue (W, Q);
            }
    }
}

  若有N个顶点、E条边,时间复杂度:
    ⋆ \star 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
    ⋆ \star 用邻接矩阵存储图,有,为 O ( N 2 ) O(N^{2}) O(N2)

2.3 广度优先搜索与深度优先搜索的对比

   ∙ \bullet 深度优先搜索用栈(stack)来实现,整个过程可以想象成一个倒立的树形:
    ⋄ \diamond 1. 把根节点压入栈中。
    ⋄ \diamond 2. 每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中,并把这个元素记为它下一级元素的前驱。
    ⋄ \diamond 3. 找到所要找的元素时结束程序。
    ⋄ \diamond 4. 如果遍历整个树还没有找到,结束程序。

   ∙ \bullet 广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:
    ⋄ \diamond 1. 把根节点放到队列的末尾。
    ⋄ \diamond 2. 每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾,并把这个元素记为它下一级元素的前驱。
    ⋄ \diamond 3. 找到所要找的元素时结束程序。
    ⋄ \diamond 4. 如果遍历整个树还没有找到,结束程序。

2.4 图不连通怎么办?

  连通: 如果从V到W存在一条(无向)路径,则称V和W是连通的。
  路径: V到W的路径是一系列顶点 { V , V 1 , V 2 , … , V n , W } \{V, V_{1}, V_{2},… ,V_{n},W\} {V,V1,V2,,Vn,W}的集合,其中任一对相邻的顶点间都有图中的边。路径的长度是路径中的边数(如果带权,则是所有边的权重和)。如果V到W之间的所有顶点都不同,则称简单路径。
  回路: 起点等于终点的路径。
  连通图: 图中任意两顶点均连通。
  连通分量: 无向图的极大连通子图。
    ⋆ \star 极大顶点数: 再加1个顶点就不连通了。
    ⋆ \star 极大边数: 包含子图中所有顶点相连的所有边.

数据结构(四)—— 图(2):图的遍历_数据结构_02
  强连通: 有向图中顶点V和W之间存在双向路径,则称V和W是强连通的。
  强连通图: 有向图中任意两顶点均强连通。
  强连通分量: 有向图的极大强连通子图。

数据结构(四)—— 图(2):图的遍历_数据结构_03

  图不连通的解决方案代码如下所示。

// 每调用一次DFS(V),就把V所在的连通分量遍历了一遍,BFS也是一样
void DFS(Vertex V)
{ 
    visited[V] = true;
    for(V的每个邻接点W)
        if (!visited[W])
            DFS(W);
}

void ListComponents(Graph G)
{ 
    for(each V in G)
        if (!visited[V]){
            DFS (V);  /*or BFS(V)*/
        }
}

2.5 图的遍历的实现

  对于图1所示的图,分别用深度优先搜索算法和广度优先搜索算法实现图的遍历。

数据结构(四)—— 图(2):图的遍历_数据结构_04

图1

2.5.1 使用深度优先搜索算法实现图的遍历

  使用深度优先搜索算法遍历邻接表存储的图的代码如下所示。

#include<iostream>
using namespace std;

/* 邻接表存储的图 - DFS */

#define MaxVertexNum 100  // 最大顶点数设为100
typedef int Vertex;  // 用顶点下标表示顶点,为整型
typedef int WeightType;  // 边的权值设为整型
typedef char DataType;  // 顶点存储的数据类型设为字符型
bool Visited[MaxVertexNum];

// 边的定义
typedef struct ENode *PtrToENode;
struct ENode {
	Vertex V1, V2;  // 有向边<V1, V2>
	WeightType Weight;  // 权重
};
typedef PtrToENode Edge;

// 邻接点的定义
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode {
	Vertex AdjV;  // 邻接点下标
	WeightType Weight;  // 边权重
	PtrToAdjVNode Next;  // 指向下一个邻接点的指针
};

// 顶点表头结点的定义
typedef struct Vnode { 
	PtrToAdjVNode FirstEdge;  // 边表头指针
	DataType Data;  // 存顶点的数据(注意:很多情况下,顶点无数据,此时Data可以不用出现)
} AdjList[MaxVertexNum];  // AdjList是邻接表类型

// 图结点的定义
typedef struct GNode *PtrToGNode;
struct GNode {
	int Nv;  // 顶点数
	int Ne;  // 边数
	AdjList G;  // 邻接表
};
typedef PtrToGNode LGraph;  // 以邻接表方式存储的图类型

// 初始化一个有VertexNum个顶点但没有边的图
LGraph CreateGraph(int VertexNum)
{
	Vertex V;
	LGraph Graph;

	Graph = (LGraph)malloc(sizeof(struct GNode));  // 建立图
	Graph->Nv = VertexNum;  // 初始化边
	Graph->Ne = 0;  // 初始化点
	/* 初始化邻接表头指针 */
	/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
	for (V = 0; V < Graph->Nv; V++)
		Graph->G[V].FirstEdge = NULL;

	return Graph;
}

// 插入一条边到邻接表的顶点指针之后
void InsertEdge(LGraph Graph, Edge E) 
{
	PtrToAdjVNode NewNode;

	/* 插入边 <V1, V2> */
	// 为V2建立新的邻接点
	NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
	NewNode->AdjV = E->V2;
	NewNode->Weight = E->Weight;
	// 将V2插入V1的表头
	NewNode->Next = Graph->G[E->V1].FirstEdge;
	Graph->G[E->V1].FirstEdge = NewNode;

	/* 若是无向图,还要插入边 <V2, V1> */
	// 为V1建立新的邻接点
	NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
	NewNode->AdjV = E->V1;
	NewNode->Weight = E->Weight;

	// 将V1插入V2的表头
	NewNode->Next = Graph->G[E->V2].FirstEdge;
	Graph->G[E->V2].FirstEdge = NewNode;
}

// 建图
LGraph BuildGraph()
{
	LGraph Graph;
	Edge E;
	Vertex V;
	int Nv, i;

	cin >> Nv;  // 读入顶点个数
	Graph = CreateGraph(Nv);  // 初始化有Nv个顶点但没有边的图

	cin >> (Graph->Ne);  // 读入边数
	if (Graph->Ne != 0)  // 如果有边
	{
		E = (Edge)malloc(sizeof(struct ENode));  // 建立边结点
		for (i = 0; i < Graph->Ne; i++) {
			cin >> E->V1 >> E->V2 >> E->Weight;  // 读入边,格式为"起点 终点 权重",插入邻接矩阵
			InsertEdge(Graph, E);
		}
	}

	// 如果顶点有数据的话,读入数据
	for (V = 0; V < Graph->Nv; V++)
		cin >> (Graph->G[V].Data);

	return Graph;
}

// 以V为出发点对邻接表存储的图Graph进行DFS搜索
void DFS(LGraph Graph, Vertex V, void(*Visit)(Vertex))
{ 
	PtrToAdjVNode W;

	Visit(V);  // 访问第V个顶点
	Visited[V] = true;  // 标记V已访问,Visited[]为全局变量,已经初始化为false

	for (W = Graph->G[V].FirstEdge; W; W = W->Next) // 对V的每个邻接点W->AdjV 
		if (!Visited[W->AdjV])    // 若W->AdjV未被访问
			DFS(Graph, W->AdjV, Visit);    // 则递归访问之
}

void Visit(Vertex V)
{
	cout << "正在访问顶点" << V << endl;
}

int main() 
{
	LGraph Graph = BuildGraph();
	for (int i = 0; i < MaxVertexNum; i++)
	    Visited[i] = false;
	DFS(Graph, 0, Visit);
	system("pause");
	return 0;
}

  图1所示的图使用深度优先搜索算法实现图的遍历测试效果如下图所示。

数据结构(四)—— 图(2):图的遍历_数据结构_05

2.5.2 使用广度优先搜索算法实现图的遍历

  使用广度优先搜索算法遍历邻接矩阵存储的图的代码如下所示。

#include<iostream>
using namespace std;

/* 邻接矩阵存储的图 - BFS */

#define MaxVertexNum 100    // 最大顶点数设为100
#define INFINITY 65535     // ∞设为双字节无符号整数的最大值65535  
#define ERROR 0  
typedef int Vertex;         // 用顶点下标表示顶点,为整型
typedef int WeightType;        // 边的权值设为整型
typedef char DataType;        // 顶点存储的数据类型设为字符型
bool Visited[MaxVertexNum];

// 边的定义
typedef struct ENode *PtrToENode;
struct ENode {
	Vertex V1, V2;    // 有向边<V1,V2> 
	WeightType Weight;  // 权重 
};
typedef PtrToENode Edge;

// 图结点的定义
typedef struct GNode *PtrToGNode;
struct GNode {
	int Nv;   // 顶点数 
	int Ne;   // 边数
	WeightType G[MaxVertexNum][MaxVertexNum];  //邻接矩阵
	DataType Data[MaxVertexNum]; // 存顶点的数据(注意:很多情况下,顶点无数据,此时Data[]可以不用出现)
};
typedef PtrToGNode MGraph;  // 以邻接矩阵存储的图类型

struct Node {
	int Data;
	struct Node *Next;
};

struct QNode {
	struct Node *rear;
	struct Node *front;
};
typedef struct QNode *Queue;

// 初始化图 
MGraph CreateGraph(int VertexNum)
{
	Vertex V, W;
	MGraph Graph;

	Graph = (MGraph)malloc(sizeof(struct GNode));  // 建立图
	Graph->Nv = VertexNum;
	Graph->Ne = 0;
	/* 初始化邻接矩阵 */
	/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
	for (V = 0; V < VertexNum; V++)
		for (W = 0; W < VertexNum; W++)
			Graph->G[V][W] = 0;
	return Graph;
}

// 插入边 
void InsertEdge(MGraph Graph, Edge E)
{
	// 插入边 <V1,V2>
	Graph->G[E->V1][E->V2] = E->Weight;

	// 如果是无向图,还需要插入边 <V2,V1>
	Graph->G[E->V2][E->V1] = E->Weight;
}

// 建图 
MGraph BuildGraph()
{
	MGraph Graph;
	Edge E;
	Vertex V;
	int Nv, i;

	cin >> Nv;   // 读入顶点数 
	Graph = CreateGraph(Nv);  // 初始化有Nv个顶点但没有边的图

	cin >> (Graph->Ne);  // 读入边数 
	if (Graph->Ne != 0)  // 如果有边
	{
		E = (Edge)malloc(sizeof(struct ENode));  // 建立边结点
		for (i = 0; i < Graph->Ne; i++)
		{
			cin >> E->V1 >> E->V2 >> E->Weight;// 读入边,格式为"起点 终点 权重",插入邻接矩阵 
			InsertEdge(Graph, E);
		}
	}

	// 如果顶点有数据的话,读入数据
	for (V = 0; V < Graph->Nv; V++)
		cin >> (Graph->Data[V]);

	return Graph;
}

int IsEmpty(Queue Q) 
{
	return(Q->front == NULL);
};

Queue CreateQueue() 
{
	Queue PtrQ;
	PtrQ = (Queue)malloc(sizeof(struct QNode));
	struct Node *rear;
	struct Node *front;
	rear = (Node*)malloc(sizeof(struct Node));
	rear = NULL;
	front = (Node*)malloc(sizeof(struct Node));
	front = NULL;
	PtrQ->front = front;
	PtrQ->rear = rear;
	return PtrQ;
};

int DeleteQ(Queue PtrQ) 
{
	struct Node *FrontCell;
	int FrontElem;

	if (IsEmpty(PtrQ)) 
	{
		cout << "队列空" << endl;
		return ERROR;
	}
	FrontCell = PtrQ->front;
	if (PtrQ->front == PtrQ->rear)
		PtrQ->front = PtrQ->rear = NULL;
	else 
		PtrQ->front = PtrQ->front->Next;
	FrontElem = FrontCell->Data;
	free(FrontCell);
	return FrontElem;
}

void InsertQ(int item, Queue PtrQ) 
{
	struct Node *FrontCell;
	FrontCell = (Node*)malloc(sizeof(struct Node));
	FrontCell->Data = item;
	FrontCell->Next = NULL;

	if (IsEmpty(PtrQ)) 
	{
		PtrQ->front = FrontCell;
		PtrQ->rear = FrontCell;
	}
	else 
	{
		PtrQ->rear->Next = FrontCell;
		PtrQ->rear = FrontCell;
	}
};

/* IsEdge(Graph, V, W)检查<V, W>是否图Graph中的一条边,即W是否V的邻接点。  */
/* 此函数根据图的不同类型要做不同的实现,关键取决于对不存在的边的表示方法。*/
/* 例如对有权图, 如果不存在的边被初始化为INFINITY, 则函数实现如下:         */
bool IsEdge(MGraph Graph, Vertex V, Vertex W)
{
	return Graph->G[V][W] < INFINITY ? true : false;
}

// 以S为出发点对邻接矩阵存储的图Graph进行BFS搜
void BFS(MGraph Graph, Vertex S, void(*Visit)(Vertex))
{
	Queue Q;
	Vertex V, W;

	Q = CreateQueue();  // 创建空队列
	Visit(S);
	Visited[S] = true;  // 标记S已访问,Visited[]为全局变量,已经初始化为false
	InsertQ(S, Q);  // S入队列

	while (!IsEmpty(Q)) 
	{
		V = DeleteQ(Q);   // 弹出V
		for (W = 0; W < Graph->Nv; W++)  // 对图中的每个顶点W
			// 若W是V的邻接点并且未访问过
			if (!Visited[W] && IsEdge(Graph, V, W)) 
			{
				Visit(W);  // 访问顶点
				Visited[W] = true;  // 标记W已访问
				InsertQ(W, Q);  // W入队列
			}
	} /* while结束*/
}

void Visit(Vertex V)
{
	cout << "正在访问顶点" << V << endl;
}

int main() 
{
	MGraph Graph = BuildGraph();
	for (int i = 0; i < MaxVertexNum; i++)
	    Visited[i] = false;
	BFS(Graph, 0, Visit);
	system("pause");
	return 0;
}

  图1所示的图使用广度优先搜索算法实现图的遍历测试效果如下图所示。

数据结构(四)—— 图(2):图的遍历_数据结构_06