上个月写的笔记了,但是一直拖着,检查了一遍没有错字就发了。有时间仔细整理一下。
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遍历: