【数据结构】第六章图:图的基本操作;图的广度优先遍历;图的深度优先遍历

上个月写的笔记了,但是一直拖着,检查了一遍没有错字就发了。有时间仔细整理一下。


6.2.4 图的基本操作

Adjacent(G, x, y)

判断图 G 是否存在边 <x, y> 或 (x, y)

Neighbors(G, x)

列出图 G 中与结点 x 邻接的边

InsertVertex(G, x)

在图 G 中插入顶点 x

DeleteVertex(G, x)

从图 G 中删除顶点 x

AddEdge(G, x, y)

若无相边 (x, y) 或有向边 <x, y> 不存在,则向图 G 中添加该边

RemoveEdge(G, x, y)

若无相边 (x, y) 或有向边 <x, y> 存在,则向图 G 中删除该边

FirstNeighbor(G, x)

求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x,则返回 -1

NextNeighbor(G, x, y)

假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个临界点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1

Get_edge_value(G, x, y)

获取图 G 中边 (x, y) 或 <x, y> 对应的权值

Set_edge_value(G, x, y, v)

设置图 G 中边 (x, y) 或 <x, y> 对应的权值v

在这一小节中只探讨邻接矩阵邻接表这两种存储结构的实现操作。

Adjacent(G, x, y):判断图 G 是否存在边 <x, y> 或 (x, y);

无向图:

在邻接矩阵存储中,判断 x 元素对应的行 y 元素对应的列存储的数据是否为 1,时间复杂度为 O(1);

在邻接表存储中,判断 x 元素所在的顶点数组指针域指向的链表中,是否有存 y 元素所在顶点数组的下标,时间复杂度最优 O(1),最差 O(|V| - 1);

有向图:

同上。

Neighbors(G, x):列出图 G 中与结点 x 邻接的边;

无向图:

在邻接矩阵存储中,遍历 x 所在元素的行或列,统计存储多少个1,时间复杂度 O(|V| - 1);

在邻接表存储中,遍历 x 元素头结点所对应的边结点的链表,时间复杂度最优 O(1),最差 O(|V|);

有向图:

在邻接矩阵存储中,遍历 x 所在元素的行和列,统计存储多少个1,时间复杂度 O(|V|);

在邻接表存储中,找出边,同上(无向图邻接表存储);找入边则需便利所有边结点链表,时间复杂度 O(|E|);

InsertVertex(G, x):在图 G 中插入顶点 x;

无向图:

在邻接矩阵存储中,给原矩阵增加 x 所在的行与列,时间复杂度为 O(1);

在邻接表存储中,将 x 元素存入顶点数组,时间复杂度为 O(1);

有向图:

同上。

DeleteVertex(G, x):从图 G 中删除顶点 x;

无向图:

在邻接矩阵存储中,在邻接矩阵给该元素行列置 0,在顶点的结构体中增加一个 bool 型变量,用于表示这个结点是否为空顶点,时间复杂度 O(|V|);

在邻接表存储中,删除 x 所在顶点数组,和其对应的边链表,下一步接着遍历边结点删除和此顶点有关的边,时间复杂度最优 O(1),最差 O(|E|);

有向图:

在邻接矩阵存储中,同上(无向图的邻接矩阵);

在邻接表存储中,删除入边,遍历所有边链表,时间复杂度 O(|E|);删除出边,遍历结点所对应的边链表,时间复杂度最优 O(1),最差 O(|V|);

AddEdge(G, x, y):若无相边 (x, y) 或有向边 <x, y> 不存在,则向图 G 中添加该边;

无向图:

在邻接矩阵存储中,将 x 元素对应的行 y 元素对应的列存储的数据赋 1,时间复杂度为 O(1);

在邻接表存储中,使用尾插法或头插法将新的边插入边链表,(头插法最简单)时间复杂度为 O(1);

有向图:

同上。

FirstNeighbor(G, x):求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x,则返回 -1;

无向图:

在邻接矩阵存储中,扫描 x 元素对应的行或列,找出第一个存储 1 的位置,时间复杂度最优 O(1),最差 O(|V|);

在邻接表存储中,找 x 元素对应的边链表的第一个结点,时间复杂度为 O(1);

有向图:

 在邻接矩阵存储中,扫描 x 元素对应的(出边)行或(入边)列,找出第一个存储 1 的位置,时间复杂度最优 O(1),最差 O(|V|);

在邻接表存储中,找出边如上(无向图邻接表存储);找入边,扫描所有边链表,时间复杂度最优 O(1),最差 O(|E|);

NextNeighbor(G, x, y):假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个临界点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1;

无向图:

在邻接矩阵存储中,扫描 x 元素对应的行或列,找出 y 之后的下一个存储 1 的位置,时间复杂度最优 O(1),最差 O(|V|);

在邻接表存储中,找 x 元素对应的边链表的 y 之后的下一个结点,时间复杂度为 O(1);

有向图:

同上。

Get_edge_value(G, x, y):获取图 G 中边 (x, y) 或 <x, y> 对应的权值;

Set_edge_value(G, x, y, v):设置图 G 中边 (x, y) 或 <x, y> 对应的权值v;

以上两种找边的权值和设置边的权值和 Adjacent(G, x, y):判断图 G 是否存在边 <x, y> 或 (x, y); 类似,核心思想都是找边或弧。

图的遍历:

 6.3.1 图的广度优先遍历

一、算法思想

Breadth-First Search,BFS 广度优先遍历(层序遍历)。

回想,对于之前学过的二叉树的 BFS,它不存在”回路“,搜索相邻的结点时,不可能搜到已经访问过的结点。

当时用到了辅助队列,过程如下:

① 若树非空,则根结点入队;

② 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;

③ 重复第 ② 步直到队列为空;

得出,广度优先遍历的要点为:

① 找到与一个顶点相邻的所有结点;

② 标记哪些顶点被访问过;

③ 需要一个辅助队列;

图的 BFS 搜索相邻的顶点时,有可能搜到已经访问过的顶点。

关于找未被访问过的顶点,需要用到上一小节中的两个函数:

FirstNeighbor(G, x):求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x,则返回 -1;

NextNeighbor(G, x, y):假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个临界点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1;

关于辅助队列存储顶点,需要 bool visited[MAX_VERTEX_NUM]; 来访问顶点数组有没有被访问过;

二、代码实现

 但是如果此图为非连通图,则无法遍历完所有结点,所以要检查 visited 数组接着遍历,直到里面所有的顶点都被访问到。

对于无向图,调用 BFS 函数的次数 = 连通分量数

部分伪代码思想:

// 访问标记数组
bool visited[MAX_VERTEX_NUM];

// 检查visited数组,是否有顶点未被访问
void BFSTraverse(Graph G){
	for(i = 0; i < G.vexnum; ++i){
		visited[i] = FALSE;  // 访问标记数组初始化 
	}
	InitQueue(Q);  // 初始化辅助队列Q
	for(i = 0; i < G.vexnum; ++i){  // 从0号顶点开始遍历,检查是否都被遍历到 
		if(!visited[i]){
			BFS(G, i);  // vi未访问过,从vi开始BFS 
		}
	} 
}

// 广度优先遍历
// 从顶点v出发,广度优先遍历图G 
void BFS(Graph G, int v){ 
	visit(v);  // 访问初始顶点v
	visited[v] = TRUE;  // 对v做已访问标记
	Enqueue(Q, v);  // 顶点v入队列Q
	while(!isEmpty(Q)){
		DeQueue(Q, v);  // 顶点v出队列
		for(w = FirstBeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
			// 检测v所有邻接点
			if(!visited[w]){  // w为v的尚未访问的邻接顶点
				visit(w);  // 访问顶点w
				visited[w] = TRUE;  // 对w做已访问标记
				Enqueue(Q, w);  // 顶点w入队列 
			} 
		} 
	} 
}

完整代码:(无向图、有向图的广度优先遍历)

无向图及其邻接表:

 有向图及相应邻接表:

 ! 代码中有向图的现有数据是按着(弧尾, 弧头)的方式记录的

运行结果:

// 20211123 图的广度优先搜索(无向图、有向图的邻接表存储,数据写死) 

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;

#define MAX 100
#define LENGTH(a) (sizeof(a)/sizeof(a[0]))

// 邻接表中表对应的链表的顶点
// 边链表
typedef struct _ENode{
	int ivex;  // 该边所指向的顶点的位置是数组的下标
	struct _ENode *next_edge;  // 指向下一条弧的指针 
}ENode, *PENode;

// 邻接表中表的顶点
typedef struct _VNode{
	char data;  // 顶点信息 
	ENode *first_edge;  // 指向第一条依附该顶点的弧 
}VNode; 

// 邻接表
typedef struct _LGraph{
	int vexnum;  // 图的顶点的数目
	int edgenum;  // 图的边的数目
	VNode vexs[MAX]; 
}LGraph; 

// 返回ch在矩阵中的位置
int get_position(LGraph g, char ch){
	int i;
	for(i = 0; i < g.vexnum; i++){
		if(g.vexs[i].data == ch){
			return i;
		}
	}
	return -1;
}

// 将node链接到list的末尾
void link_last(ENode *list, ENode *node){
	ENode *p = list;
	
	while(p->next_edge){
		p = p->next_edge;
	}
	p->next_edge = node;
}


// 创建邻接表对应的图(无向图)
LGraph* create_example_lgraph(){
	char c1, c2;
	char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
	char edges[][2] = {
		{'A', 'C'},
		{'A', 'D'},
		{'A', 'F'},
		{'B', 'C'},
		{'C', 'D'},
		{'E', 'G'},
		{'F', 'G'}
	};
	int vlen = LENGTH(vexs);
	int elen = LENGTH(edges);
	// 上面类似一个邻接矩阵存储 
	
	int i, p1, p2;
	ENode *node1, *node2;
	LGraph* pG;
	
	// 申请空间,初始化为0 
	if((pG = (LGraph *)malloc((sizeof(LGraph)))) == NULL){
		return NULL;
	}
	memset(pG, 0, sizeof(LGraph));
	
	// 初始化“顶点数”和“边数” 
	pG->vexnum = vlen;
	pG->edgenum = elen;
	// 初始化“邻接表”的顶点
	for(i = 0; i < pG->vexnum; i++){
		pG->vexs[i].data = vexs[i];
		pG->vexs[i].first_edge = NULL;
	} 
	
	// 初始化“邻接表”的边
	for(i = 0; i < pG->edgenum; i++){
		c1 = edges[i][0];
		c2 = edges[i][1];
		
		p1 = get_position(*pG, c1);
		p2 = get_position(*pG, c2);
		
		// 初始化node1
		node1 = (ENode*)calloc(1, sizeof(ENode));
		node1->ivex = p2;
		// 将node1链接到“p1所在链表的末尾”
		if(pG->vexs[p1].first_edge == NULL){
			pG->vexs[p1].first_edge = node1; 
		}else{
			link_last(pG->vexs[p1].first_edge, node1);
		}
		// 初始化node2
		node2 = (ENode*)calloc(1, sizeof(ENode));
		node2->ivex = p1;
		// 将node2链接到“p2”所在链表的末尾
		if(pG->vexs[p2].first_edge == NULL){
			pG->vexs[p2].first_edge = node2;
		} else{
			link_last(pG->vexs[p2].first_edge, node2);
		}
	} 
	return pG;	
} 

// 创建邻接表对应的图(有向图)
LGraph* create_example_lgraph_directed(){
	char c1, c2;
	char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
	char edges[][2] = {
		{'A', 'B'},
		{'B', 'C'},
		{'B', 'E'},
		{'B', 'F'},
		{'C', 'E'},
		{'D', 'C'},
		{'E', 'B'},
		{'E', 'D'},
		{'F', 'G'}
	};
	int vlen = LENGTH(vexs);
	int elen = LENGTH(edges);
	
	int i, p1, p2;
	ENode *node1;
	LGraph* pG;
	
	// 申请空间
	if((pG=(LGraph*)malloc(sizeof(LGraph))) == NULL){
		return NULL;
	} 
	memset(pG, 0, sizeof(LGraph));
	
	// 初始化“顶点数”和“边数”
	pG->vexnum = vlen;
	pG->edgenum = elen;
	// 初始化“邻接表”的顶点
	for(i = 0; i < pG->vexnum; i++){
		pG->vexs[i].data = vexs[i];
		pG->vexs[i].first_edge = NULL;
	} 
	// 初始化“邻接表”的边
	for(i = 0; i < pG->edgenum; i++){
		// 读取边的起始顶点和结束顶点
		c1 = edges[i][0];
		c2 = edges[i][1];
		
		p1 = get_position(*pG, c1);
		p2 = get_position(*pG, c2); 
		// 初始化node1
		node1 = (ENode*)calloc(1, sizeof(ENode));
		node1->ivex = p2;
		// 将node1链接到“p1所在链表的末尾”
		if(pG->vexs[p1].first_edge == NULL){
			pG->vexs[p1].first_edge = node1;
		}else{
			link_last(pG->vexs[p1].first_edge, node1);
		}
	} 
	
	return pG;
} 

// 打印邻接表图
void print_lgraph(LGraph G){
	int i;
	ENode *node;
	
	printf("List Graph:\n");
	for(i = 0; i < G.vexnum; i++){  // 遍历所有的顶点 
		printf("%d(%c):", i, G.vexs[i].data);
		node = G.vexs[i].first_edge;
		while(node != NULL){  // 把每个顶点连接的边可到达的顶点输出
			printf("%d(%c)", node->ivex, G.vexs[node->ivex].data);
			node = node->next_edge; 	
		}
		printf("\n");
	} 
}


// 广度优先搜索(类似于树的层次遍历)
void BFS(LGraph G){
	int head = 0;
	int rear = 0;
	int queue[MAX];  // 辅助队列
	int visited[MAX];  // 顶点访问标记
	int i, j, k;
	ENode *node;
	
	// 每个顶点未被访问
	for(i = 0; i < G.vexnum; i++){
		visited[i] = 0;
	}
	// 从零号开始遍历
	printf("BFS:");
	for(i = 0; i < G.vexnum; i++){  // 对于每个连通分量均调用一次BFS
		if(!visited[i]){  // 如果没访问过,就打印出来,同时入队,最初是A 
			visited[i] = 1;
			printf("%c ", G.vexs[i].data);
			queue[rear++] = i;  // 入队列 
		} 
		while(head != rear){  // 第一个进来的是A,遍历A所连接的每一条边
			j = queue[head++];  // 出队列
			node = G.vexs[j].first_edge;
			while(node != NULL){
				k = node->ivex;
				if(!visited[k]){
					visited[k] = 1;
					printf("%c ", G.vexs[k].data);
					queue[rear++] = k;  // 类似于树的层次遍历, 遍历到的同时入队 
				}
				node = node->next_edge; 
			}
			
		} 
		
	} 
	printf("\n");
} 

// 深度优先搜索图的递归实现
void DFS(LGraph G, int i, int *visited){
	ENode *node;
	
	visited[i] = 1;
	printf("%c ", G.vexs[i].data);
	node = G.vexs[i].first_edge;
	while(node != NULL){
		if(!visited[node->ivex]){  // 只要对应顶点没有访问过,深入到下一个顶点访问 
			DFS(G, node->ivex, visited); 
		}
		node = node->next_edge;  // 某个顶点的下一条边,例如B结点的下一条边 
	}
} 
// 深度优先遍历图
void DFSTraverse(LGraph G){
	int i;
	int visited[MAX];  // 顶点访问标记
	
	// 初始化所有顶点都没有被访问
	for(i = 0; i < G.vexnum; i++){
		visited[i] = 0;
	} 
	
	printf("DFS:");
	// 从A开始深度优先遍历
	for(i = 0; i < G.vexnum; i++){
		if(!visited[i]){
			DFS(G, i, visited);
		}
	} 
	printf("\n");
} 

int main(){
	LGraph* pG1;
	LGraph* pG2;
	
	// 无向图的创建 
	printf("这里是无向图:\n");
	pG1 = create_example_lgraph();
	// 打印邻接表
	print_lgraph(*pG1);
	// 广度优先遍历
	BFS(*pG1); 
	// 深度优先遍历
	DFSTraverse(*pG1);
	
	printf("\n\n");
	
	// 有向图的创建
	printf("这里是有向图:\n");
	pG2 = create_example_lgraph_directed();	
	// 打印邻接表
	print_lgraph(*pG2);
	// 广度优先遍历 
	BFS(*pG2);
	// 深度优先遍历
	DFSTraverse(*pG2); 
	
	return 0;
} 

三、BFS 遍历序列

从顶点 2 出发的 BFS 遍历序列:2, 1, 6, 5, 3, 7, 4, 8;

从顶点 1 出发的 BFS 遍历序列:1, 2, 5, 6, 3, 7, 4, 8;

从顶点 3 出发的 BFS 遍历序列:3, 4, 6, 7, 8, 2, 1, 5;

同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一;

同一个图邻接表表示方式不唯一,因此广度优先遍历不唯一;

四、空间复杂度和时间复杂度

 五、广度优先生成树

 留下图中各顶点第一次被访问到的边,得到一个广度优先生成树

 六、广度优先生成森林

 6.3.2 图的深度优先遍历

 一、算法思想

首先回顾树的深度优先遍历,分先序遍历和后序遍历两种写法。

先序遍历(先根遍历):根左右;

 二、代码实现

 (6.3.1 图的广度优先遍历 → 二、代码实现 这一节中代码已经实现)

出现了一个问题——如果该图是非连通图,则无法遍历完所有结点!

解决方法是再加一个结点扫描程序,继续遍历未被访问到的结点。

三、空间复杂度和时间复杂度

空间复杂度:

 时间复杂度:

四、DFS遍历序列

从2出发的深度优先遍历序列:2,1,5,6,3,4,7,8

从3出发的深度优先遍历序列:3,4,7,6,2,1,5,8

从1出发的深度优先遍历序列:1,2,6,3,4,7,8,5

邻接表不一样的话即使从同一个结点出发,它们的遍历序列也不一样。假设改成下表。

 从2出发的深度优先遍历序列:2,6,7,8,4,3,1,5

同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一;

同一个图邻接表表示方式不唯一,因此深度优先遍历序列不唯一;

五、深度优先生成树

 六、深度优先生成森林

 以上非连通图通过多次调用DFS函数生成深度优先生成森林。

对无向图进行BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数;

对于连通图,只需调用1次BFS/DFS;

对有向图进行BFS/DFS遍历:

  • 4
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值