赞
踩
上个月写的笔记了,但是一直拖着,检查了一遍没有错字就发了。有时间仔细整理一下。
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); 类似,核心思想都是找边或弧。
图的遍历:
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;
- }

从顶点 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.1 图的广度优先遍历 → 二、代码实现 这一节中代码已经实现)
出现了一个问题——如果该图是非连通图,则无法遍历完所有结点!
解决方法是再加一个结点扫描程序,继续遍历未被访问到的结点。
空间复杂度:
时间复杂度:
从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遍历:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。