当前位置:   article > 正文

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

图的基本操作

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


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 函数的次数 = 连通分量数

部分伪代码思想:

  1. // 访问标记数组
  2. bool visited[MAX_VERTEX_NUM];
  3. // 检查visited数组,是否有顶点未被访问
  4. void BFSTraverse(Graph G){
  5. for(i = 0; i < G.vexnum; ++i){
  6. visited[i] = FALSE; // 访问标记数组初始化
  7. }
  8. InitQueue(Q); // 初始化辅助队列Q
  9. for(i = 0; i < G.vexnum; ++i){ // 从0号顶点开始遍历,检查是否都被遍历到
  10. if(!visited[i]){
  11. BFS(G, i); // vi未访问过,从vi开始BFS
  12. }
  13. }
  14. }
  15. // 广度优先遍历
  16. // 从顶点v出发,广度优先遍历图G
  17. void BFS(Graph G, int v){
  18. visit(v); // 访问初始顶点v
  19. visited[v] = TRUE; // 对v做已访问标记
  20. Enqueue(Q, v); // 顶点v入队列Q
  21. while(!isEmpty(Q)){
  22. DeQueue(Q, v); // 顶点v出队列
  23. for(w = FirstBeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
  24. // 检测v所有邻接点
  25. if(!visited[w]){ // w为v的尚未访问的邻接顶点
  26. visit(w); // 访问顶点w
  27. visited[w] = TRUE; // 对w做已访问标记
  28. Enqueue(Q, w); // 顶点w入队列
  29. }
  30. }
  31. }
  32. }

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

无向图及其邻接表:

 有向图及相应邻接表:

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

运行结果:

  1. // 20211123 图的广度优先搜索(无向图、有向图的邻接表存储,数据写死)
  2. #include <iostream>
  3. #include <cstdio>
  4. #include <cstdlib>
  5. #include <cstring>
  6. using namespace std;
  7. #define MAX 100
  8. #define LENGTH(a) (sizeof(a)/sizeof(a[0]))
  9. // 邻接表中表对应的链表的顶点
  10. // 边链表
  11. typedef struct _ENode{
  12. int ivex; // 该边所指向的顶点的位置是数组的下标
  13. struct _ENode *next_edge; // 指向下一条弧的指针
  14. }ENode, *PENode;
  15. // 邻接表中表的顶点
  16. typedef struct _VNode{
  17. char data; // 顶点信息
  18. ENode *first_edge; // 指向第一条依附该顶点的弧
  19. }VNode;
  20. // 邻接表
  21. typedef struct _LGraph{
  22. int vexnum; // 图的顶点的数目
  23. int edgenum; // 图的边的数目
  24. VNode vexs[MAX];
  25. }LGraph;
  26. // 返回ch在矩阵中的位置
  27. int get_position(LGraph g, char ch){
  28. int i;
  29. for(i = 0; i < g.vexnum; i++){
  30. if(g.vexs[i].data == ch){
  31. return i;
  32. }
  33. }
  34. return -1;
  35. }
  36. // 将node链接到list的末尾
  37. void link_last(ENode *list, ENode *node){
  38. ENode *p = list;
  39. while(p->next_edge){
  40. p = p->next_edge;
  41. }
  42. p->next_edge = node;
  43. }
  44. // 创建邻接表对应的图(无向图)
  45. LGraph* create_example_lgraph(){
  46. char c1, c2;
  47. char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
  48. char edges[][2] = {
  49. {'A', 'C'},
  50. {'A', 'D'},
  51. {'A', 'F'},
  52. {'B', 'C'},
  53. {'C', 'D'},
  54. {'E', 'G'},
  55. {'F', 'G'}
  56. };
  57. int vlen = LENGTH(vexs);
  58. int elen = LENGTH(edges);
  59. // 上面类似一个邻接矩阵存储
  60. int i, p1, p2;
  61. ENode *node1, *node2;
  62. LGraph* pG;
  63. // 申请空间,初始化为0
  64. if((pG = (LGraph *)malloc((sizeof(LGraph)))) == NULL){
  65. return NULL;
  66. }
  67. memset(pG, 0, sizeof(LGraph));
  68. // 初始化“顶点数”和“边数”
  69. pG->vexnum = vlen;
  70. pG->edgenum = elen;
  71. // 初始化“邻接表”的顶点
  72. for(i = 0; i < pG->vexnum; i++){
  73. pG->vexs[i].data = vexs[i];
  74. pG->vexs[i].first_edge = NULL;
  75. }
  76. // 初始化“邻接表”的边
  77. for(i = 0; i < pG->edgenum; i++){
  78. c1 = edges[i][0];
  79. c2 = edges[i][1];
  80. p1 = get_position(*pG, c1);
  81. p2 = get_position(*pG, c2);
  82. // 初始化node1
  83. node1 = (ENode*)calloc(1, sizeof(ENode));
  84. node1->ivex = p2;
  85. // 将node1链接到“p1所在链表的末尾”
  86. if(pG->vexs[p1].first_edge == NULL){
  87. pG->vexs[p1].first_edge = node1;
  88. }else{
  89. link_last(pG->vexs[p1].first_edge, node1);
  90. }
  91. // 初始化node2
  92. node2 = (ENode*)calloc(1, sizeof(ENode));
  93. node2->ivex = p1;
  94. // 将node2链接到“p2”所在链表的末尾
  95. if(pG->vexs[p2].first_edge == NULL){
  96. pG->vexs[p2].first_edge = node2;
  97. } else{
  98. link_last(pG->vexs[p2].first_edge, node2);
  99. }
  100. }
  101. return pG;
  102. }
  103. // 创建邻接表对应的图(有向图)
  104. LGraph* create_example_lgraph_directed(){
  105. char c1, c2;
  106. char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
  107. char edges[][2] = {
  108. {'A', 'B'},
  109. {'B', 'C'},
  110. {'B', 'E'},
  111. {'B', 'F'},
  112. {'C', 'E'},
  113. {'D', 'C'},
  114. {'E', 'B'},
  115. {'E', 'D'},
  116. {'F', 'G'}
  117. };
  118. int vlen = LENGTH(vexs);
  119. int elen = LENGTH(edges);
  120. int i, p1, p2;
  121. ENode *node1;
  122. LGraph* pG;
  123. // 申请空间
  124. if((pG=(LGraph*)malloc(sizeof(LGraph))) == NULL){
  125. return NULL;
  126. }
  127. memset(pG, 0, sizeof(LGraph));
  128. // 初始化“顶点数”和“边数”
  129. pG->vexnum = vlen;
  130. pG->edgenum = elen;
  131. // 初始化“邻接表”的顶点
  132. for(i = 0; i < pG->vexnum; i++){
  133. pG->vexs[i].data = vexs[i];
  134. pG->vexs[i].first_edge = NULL;
  135. }
  136. // 初始化“邻接表”的边
  137. for(i = 0; i < pG->edgenum; i++){
  138. // 读取边的起始顶点和结束顶点
  139. c1 = edges[i][0];
  140. c2 = edges[i][1];
  141. p1 = get_position(*pG, c1);
  142. p2 = get_position(*pG, c2);
  143. // 初始化node1
  144. node1 = (ENode*)calloc(1, sizeof(ENode));
  145. node1->ivex = p2;
  146. // 将node1链接到“p1所在链表的末尾”
  147. if(pG->vexs[p1].first_edge == NULL){
  148. pG->vexs[p1].first_edge = node1;
  149. }else{
  150. link_last(pG->vexs[p1].first_edge, node1);
  151. }
  152. }
  153. return pG;
  154. }
  155. // 打印邻接表图
  156. void print_lgraph(LGraph G){
  157. int i;
  158. ENode *node;
  159. printf("List Graph:\n");
  160. for(i = 0; i < G.vexnum; i++){ // 遍历所有的顶点
  161. printf("%d(%c):", i, G.vexs[i].data);
  162. node = G.vexs[i].first_edge;
  163. while(node != NULL){ // 把每个顶点连接的边可到达的顶点输出
  164. printf("%d(%c)", node->ivex, G.vexs[node->ivex].data);
  165. node = node->next_edge;
  166. }
  167. printf("\n");
  168. }
  169. }
  170. // 广度优先搜索(类似于树的层次遍历)
  171. void BFS(LGraph G){
  172. int head = 0;
  173. int rear = 0;
  174. int queue[MAX]; // 辅助队列
  175. int visited[MAX]; // 顶点访问标记
  176. int i, j, k;
  177. ENode *node;
  178. // 每个顶点未被访问
  179. for(i = 0; i < G.vexnum; i++){
  180. visited[i] = 0;
  181. }
  182. // 从零号开始遍历
  183. printf("BFS:");
  184. for(i = 0; i < G.vexnum; i++){ // 对于每个连通分量均调用一次BFS
  185. if(!visited[i]){ // 如果没访问过,就打印出来,同时入队,最初是A
  186. visited[i] = 1;
  187. printf("%c ", G.vexs[i].data);
  188. queue[rear++] = i; // 入队列
  189. }
  190. while(head != rear){ // 第一个进来的是A,遍历A所连接的每一条边
  191. j = queue[head++]; // 出队列
  192. node = G.vexs[j].first_edge;
  193. while(node != NULL){
  194. k = node->ivex;
  195. if(!visited[k]){
  196. visited[k] = 1;
  197. printf("%c ", G.vexs[k].data);
  198. queue[rear++] = k; // 类似于树的层次遍历, 遍历到的同时入队
  199. }
  200. node = node->next_edge;
  201. }
  202. }
  203. }
  204. printf("\n");
  205. }
  206. // 深度优先搜索图的递归实现
  207. void DFS(LGraph G, int i, int *visited){
  208. ENode *node;
  209. visited[i] = 1;
  210. printf("%c ", G.vexs[i].data);
  211. node = G.vexs[i].first_edge;
  212. while(node != NULL){
  213. if(!visited[node->ivex]){ // 只要对应顶点没有访问过,深入到下一个顶点访问
  214. DFS(G, node->ivex, visited);
  215. }
  216. node = node->next_edge; // 某个顶点的下一条边,例如B结点的下一条边
  217. }
  218. }
  219. // 深度优先遍历图
  220. void DFSTraverse(LGraph G){
  221. int i;
  222. int visited[MAX]; // 顶点访问标记
  223. // 初始化所有顶点都没有被访问
  224. for(i = 0; i < G.vexnum; i++){
  225. visited[i] = 0;
  226. }
  227. printf("DFS:");
  228. // 从A开始深度优先遍历
  229. for(i = 0; i < G.vexnum; i++){
  230. if(!visited[i]){
  231. DFS(G, i, visited);
  232. }
  233. }
  234. printf("\n");
  235. }
  236. int main(){
  237. LGraph* pG1;
  238. LGraph* pG2;
  239. // 无向图的创建
  240. printf("这里是无向图:\n");
  241. pG1 = create_example_lgraph();
  242. // 打印邻接表
  243. print_lgraph(*pG1);
  244. // 广度优先遍历
  245. BFS(*pG1);
  246. // 深度优先遍历
  247. DFSTraverse(*pG1);
  248. printf("\n\n");
  249. // 有向图的创建
  250. printf("这里是有向图:\n");
  251. pG2 = create_example_lgraph_directed();
  252. // 打印邻接表
  253. print_lgraph(*pG2);
  254. // 广度优先遍历
  255. BFS(*pG2);
  256. // 深度优先遍历
  257. DFSTraverse(*pG2);
  258. return 0;
  259. }

三、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遍历:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/984746?site
推荐阅读
相关标签
  

闽ICP备14008679号