图——图的遍历(DFS与BFS算法详解)

news2024/9/23 11:27:45

前面的文章中我们学习了图的基本概念和存储结构,大家可以通过下面的链接学习:

图的定义和基本术语

图的类型定义和存储结构

这篇文章就来学习一下图的重要章节——图的遍历。

目录

一,图的遍历定义:

二,深度优先搜索(DFS)

连通图的深度优先遍历

邻接矩阵法实现DFS

邻接表法实现DFS

DFS算法效率分析:

非连通图的深度优先遍历

三, 广度优先搜索(BFS)

邻接矩阵法实现BFS

邻接表法实现BFS

 BFS算法效率分析:

DFS与BFS算法效率比较


一,图的遍历定义:

        从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算

图遍历的实质:找每个顶点的邻接点的过程。

要进行图的遍历,我们需要知道图的特点,从而用合适,高效的方法来实现遍历。

图有哪些特点?

图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点 

那么,如何避免在遍历时重复访问?

解决思路:

设置辅助数组 visited [n ],用来标记每个被访问过的顶点。

        初始状态为0

        被访问,改 visited [i]1,防止被多次访问。

图常用的遍历有两种:

深度优先搜索(Depth First Search,DFS    

广度优先搜索(Breadth First Search,BFS)    

二,深度优先搜索(DFS)

引例:

点亮迷宫中所有的灯,我们会一条道走到头,如果走不动了,再往回退寻找其他没有走过的。

因此我们可以总结DFS的详细归纳:

在访问图中某一起始顶点 v 后,由 v 出发,访问 它的任一邻接顶点 w 1
再从 w 1 出发DFS 邻接 但还 未被访问 过的顶点 w 2
然后再从 w 2 出发,进行类似的访问,
如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止。
接着,退回一步, 退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。
          如果有, 则访问此顶点,之后再从此顶点出发,进行与前述类似的访问。    

         如果没有,再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。

连通图的深度优先遍历

仿树的先序遍历过程

 先根,再左子树,最后右子树。

下面是一个练习:

 它的深度优先搜索结果是:2→1→3→5→4→6

那么,在计算机中我们该如何实现DFS的过程?

邻接矩阵法实现DFS

在上一篇文章中我们学习了图的存储结构的邻接矩阵法,借助一个辅助数组 visited [n ]来保存邻接矩阵的信息。

在编程时,使用邻接矩阵法DFS的实现可以采用递归算法:

void DFS(AMGraph G, int v){        	//图G为邻接矩阵类型 
  cout<<v;  visited[v] = true;  		//访问第v个顶点
  for(w = 0; w< G.vexnum; w++)  	//依次检查邻接矩阵v所在的行  
        if((G.arcs[v][w]!=0)&& (!visited[w]))  
            DFS(G, w); 
      //w是v的邻接点,如果w未访问,则递归调用DFS 
} 

而下面是完整的c语言使用邻接矩阵法来实现图的深度优先搜索:

注意:

这里我使用了C语言的一个库#include <stdbool.h>,来声明我使用了布尔型数据

大家也可以自己定义布尔类型,true代表1为真,false代表0为假

typedef int bool;
#define true 1
#define false 0

#include <stdio.h>
#include <stdbool.h>

#define MAX_VERTICES 100

// 定义邻接矩阵和访问标记数组
int graph[MAX_VERTICES][MAX_VERTICES];
bool visited[MAX_VERTICES];
int num_vertices;

// 深度优先搜索函数
void dfs(int vertex) {
    // 标记当前节点为已访问
    visited[vertex] = true;
    // 输出当前节点
    printf("%d ", vertex);

    // 遍历当前节点的所有邻接节点
    for (int i = 0; i < num_vertices; i++) {
        // 如果邻接节点存在且未被访问,则递归调用dfs函数
        if (graph[vertex][i] == 1 && !visited[i]) {
            dfs(i);
        }
    }
}

int main() {
    // 输入顶点数和边数
    printf("请输入顶点数和边数:");
    scanf("%d", &num_vertices);

    // 输入邻接矩阵
    printf("请输入邻接矩阵:");
    for (int i = 0; i < num_vertices; i++) {
        for (int j = 0; j < num_vertices; j++) {
            scanf("%d", &graph[i][j]);
        }
    }

    // 输出深度优先遍历结果
    printf("深度优先遍历结果:");
    // 初始化访问标记数组
    for (int i = 0; i < num_vertices; i++) {
        visited[i] = false;
    }

    // 遍历所有节点,如果节点未被访问,则调用dfs函数
    for (int i = 0; i < num_vertices; i++) {
        if (!visited[i]) {
            dfs(i);
        }
    }

    return 0;
}

输入实例:

输出结果:

 

邻接表法实现DFS

我们知道图的存储结构除了邻接矩阵法之外还有邻接表法,那么使用邻接表法能否实现图的深度优先搜索? 

当然可以,同样要借助辅助数组visited [n ]

 使用邻接表法DFS的实现也可以采用递归算法:

void DFS(ALGraph G, int v){        //图G为邻接表类型 
  cout<<v;  visited[v] = true;          //访问第v个顶点
  p= G.vertices[v].firstarc;             //p指向v的边链表的第一个边结点 
while(p!=NULL){              	      //边结点非空 
  w=p->adjvex;               	      //表示w是v的邻接点 
  if(!visited[w])  DFS(G, w); 	     //如果w未访问,则递归调用DFS 
  p=p->nextarc;                	     //p指向下一个边结点 
 } 
} 

在实际的编程中,使用邻接表法实现的DFS代码:

#include <stdio.h>
#include <stdlib.h>

#define MAX_VERTICES 100

typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

typedef struct Graph {
    int numVertices;
    Node* adjLists[MAX_VERTICES];
} Graph;

// 添加边函数
void addEdge(Graph* graph, int src, int dest) {
    // 创建新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->vertex = dest;
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
}

// 深度优先搜索函数
void DFS(Graph* graph, int vertex, int visited[]) {
    // 标记当前节点为已访问
    visited[vertex] = 1;
    // 输出当前节点
    printf("%d ", vertex);

    // 遍历当前节点的所有邻接节点
    Node* temp = graph->adjLists[vertex];
    while (temp) {
        int connectedVertex = temp->vertex;
        // 如果邻接节点未被访问,则递归调用DFS函数
        if (!visited[connectedVertex]) {
            DFS(graph, connectedVertex, visited);
        }
        temp = temp->next;
    }
}

// 深度优先遍历函数
void DFSTraversal(Graph* graph) {
    // 初始化访问标记数组
    int visited[MAX_VERTICES] = {0};
    for (int i = 0; i < graph->numVertices; i++) {
        // 如果节点未被访问,则调用DFS函数
        if (!visited[i]) {
            DFS(graph, i, visited);
        }
    }
}

int main() {
    // 创建图
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->numVertices = 5;

    // 初始化邻接表
    for (int i = 0; i < graph->numVertices; i++) {
        graph->adjLists[i] = NULL;
    }

    // 添加边
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 1, 4);

    // 输出深度优先遍历结果
    printf("Depth First Traversal: ");
    DFSTraversal(graph);

    return 0;
}

输出结果:

Depth First Traversal: 0 2 1 4 3 

DFS算法效率分析:

邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)

邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为O(n+e)

结论:

稠密图适于在邻接矩阵上进行深度遍历;

稀疏图适于在邻接表上进行深度遍历。

非连通图的深度优先遍历

如下图例子,连通分量分开访问,先DFS访问完第一个,再访问第二个。

三, 广度优先搜索(BFS)

同样是点亮迷宫中的灯案例,广度优先搜索没有一条道走到黑,而是每个道都走,一层一层实现遍历。

 简单归纳:

        在访问了起始点v之后,依次访问 v的邻接点

        然后再依次访问这些顶点中未被访问过的邻接点

        直到所有顶点都被访问过为止。

 广度优先搜索是一种 分层 的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有回退的情况。
  因此,广度优先搜索 不是一个递归 的过程,其算法也不是递归的。

 BFS基本思想:——仿树的层次遍历过程

在计算机中,如何实现BFS?

与DFS相比,除辅助数组visited [n ]外,还需再开一辅助队列。

算法思想:

从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。

只要队列不空,则重复下述处理。

           ① 队头顶点u出队。

           ② 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。

同样对于BFS可以使用邻接矩阵或邻接表法来实现。

邻接矩阵法实现BFS

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

// 定义队列结构体
typedef struct {
    int data[MAX_SIZE]; // 队列数据
    int front, rear; // 队头和队尾指针
} Queue;

// 初始化队列
void initQueue(Queue *q) {
    q->front = q->rear = 0;
}

// 判断队列是否已满
int isFull(Queue *q) {
    return (q->rear + 1) % MAX_SIZE == q->front;
}

// 判断队列是否为空
int isEmpty(Queue *q) {
    return q->front == q->rear;
}

// 入队
void enqueue(Queue *q, int x) {
    if (isFull(q)) {
        printf("Queue is full!\n");
        exit(1);
    }
    q->data[q->rear] = x;
    q->rear = (q->rear + 1) % MAX_SIZE;
}

// 出队
int dequeue(Queue *q) {
    if (isEmpty(q)) {
        printf("Queue is empty!\n");
        exit(1);
    }
    int x = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    return x;
}

// 广度优先搜索
void BFS(int graph[][MAX_SIZE], int visited[], int start, int n) {
    Queue q;
    initQueue(&q);
    enqueue(&q, start);
    visited[start] = 1;
    printf("%d ", start);

    while (!isEmpty(&q)) {
        int current = dequeue(&q);
        for (int i = 0; i < n; i++) {
            if (graph[current][i] && !visited[i]) {
                enqueue(&q, i);
                visited[i] = 1;
                printf("%d ", i);
            }
        }
    }
}

int main() {
    int n, e;
    printf("Enter the number of vertices and edges: ");
    scanf("%d %d", &n, &e);

    int graph[MAX_SIZE][MAX_SIZE] = {0};
    int visited[MAX_SIZE] = {0};

    printf("Enter the edges (u v):\n");
    for (int i = 0; i < e; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        graph[u][v] = 1;
        graph[v][u] = 1; 
    }

    printf("BFS traversal starting from vertex 0:\n");
    BFS(graph, visited, 0, n);

    return 0;
}

 输入案例

输出结果

邻接表法实现BFS

#include <stdio.h>
#include <stdlib.h>

// 定义邻接表结构体
typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

// 定义图结构体
typedef struct Graph {
    int numVertices;
    Node** adjLists;
} Graph;

// 创建新的节点
Node* createNode(int v) {
    Node* newNode = malloc(sizeof(Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

// 添加边到邻接表
void addEdge(Graph* graph, int src, int dest) {
    Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;

    // 如果是无向图,需要添加反向边
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

// 初始化图
Graph* createGraph(int vertices) {
    Graph* graph = malloc(sizeof(Graph));
    graph->numVertices = vertices;
    graph->adjLists = malloc(vertices * sizeof(Node*));
    for (int i = 0; i < vertices; i++) {
        graph->adjLists[i] = NULL;
    }
    return graph;
}

// BFS遍历图
void BFS(Graph* graph, int startVertex) {
    int visited[graph->numVertices];
    for (int i = 0; i < graph->numVertices; i++) {
        visited[i] = 0;
    }

    // 创建一个队列并初始化起始顶点
    Node* queue = createNode(startVertex);
    visited[startVertex] = 1;

    // 当队列不为空时继续遍历
    while (queue != NULL) {
        // 打印当前顶点
        printf("%d ", queue->vertex);

        // 获取当前顶点的邻接顶点列表
        Node* temp = graph->adjLists[queue->vertex];

        // 遍历邻接顶点列表
        while (temp != NULL) {
            int adjVertex = temp->vertex;
            if (!visited[adjVertex]) {
                // 将未访问过的邻接顶点加入队列并标记为已访问
                Node* newNode = createNode(adjVertex);
                newNode->next = queue->next;
                queue->next = newNode;
                visited[adjVertex] = 1;
            }
            temp = temp->next;
        }

        // 弹出队列的第一个元素
        Node* dequeuedNode = queue;
        queue = queue->next;
        free(dequeuedNode);
    }
}

int main() {
    int vertices = 6;
    Graph* graph = createGraph(vertices);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 1, 4);
    addEdge(graph, 2, 4);
    addEdge(graph, 3, 5);
    addEdge(graph, 4, 5);

    printf("BFS traversal starting from vertex 0: ");
    BFS(graph, 0);

    return 0;
}

输出结果:

 BFS算法效率分析:

如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行( n 个元素),总的时间代价为O(n2)

用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为O(n+e)

DFS与BFS算法效率比较

空间复杂度相同,都是O(n)(借用了堆栈或队列);

时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。

最后是一个小练习


 图的遍历到此就结束啦,如果文章对你有用的话请点个赞支持一下吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1934052.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】:学习数据库必须要知道的背景知识

客户端—服务器 客户端是一个“客户端—服务器”结构的程序 C&#xff08;client&#xff09;—S&#xff08;server&#xff09; 客户端和服务器是两个独立的程序&#xff0c;这两个程序之间通过“网络”进行通信&#xff08;相当于是两种角色&#xff09; 客户端 主动发起网…

CV12_ONNX转RKNN模型(谛听盒子)

暂时简单整理一下&#xff1a; 1.在边缘设备上配置相关环境。 2.配置完成后&#xff0c;获取模型中间的输入输出结果&#xff0c;保存为npy格式。 3.将onnx格式的模型&#xff0c;以及中间输入输出文件传送到边缘设备上。 4.编写一个python文件用于转换模型格式&#xff0c…

对某根域的一次渗透测试

前言 两个月之前的一个渗透测试项目是基于某网站根域进行渗透测试&#xff0c;发现该项目其实挺好搞的&#xff0c;就纯粹的没有任何防御措施与安全意识所以该项目完成的挺快&#xff0c;但是并没有完成的很好&#xff0c;因为有好几处文件上传没有绕过&#xff08;虽然从一个…

linux|多线程(一)

主要介绍了为什么要有线程 和线程的调用 和简单的对线程进行封装。 背景知识 a.重谈地址空间 我们知道物理内存的最小单元大小是4kB 物理内存是4G那么这样的单元友1M个 操作系统先描述再组织struct page[1M] 对于32位数据字长的机器&#xff0c;页表有2^32条也就是4G条&#…

springboot的JWT令牌

生成JWT令牌 依赖 <!--jwt令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>javax.xml.bind<…

怎样在 PostgreSQL 中优化对大数据量的分页查询?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 《PostgreSQL 中大数据量分页查询的优化之道》一、理解分页查询的基本原理二、优化分页查询的策略&…

2024年06月CCF-GESP编程能力等级认证C++编程七级真题解析

本文收录于专栏《C等级认证CCF-GESP真题解析》&#xff0c;专栏总目录&#xff1a;点这里。订阅后可阅读专栏内所有文章。 一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 下列C代码的输出结果是&#xff08; &#xff09;。 #include <iostr…

SwiftUI 6.0(Xcode 16)新 PreviewModifier 协议让预览调试如虎添翼

概览 用 SwiftUI 框架开发过应用的小伙伴们都知道&#xff0c;SwiftUI 中的视图由各种属性和绑定“扑朔迷离”的缠绕在一起&#xff0c;自成体系。 想要在 Xcode 预览中泰然处之的调试 SwiftUI 视图有时并不是件容易的事。其中&#xff0c;最让人秃头码农们头疼的恐怕就要数如…

Spring Cloud Gateway 自定义断言以及过滤器

1.Spring Cloud gateway介绍 Spring Cloud Gateway 是一个基于 Spring Framework 和 Spring Boot 的 API 网关服务&#xff0c;它利用了 Spring WebFlux 来提供响应式非阻塞式Web请求处理能力。它的核心功能是路由&#xff0c;即根据请求的特定规则将请求转发到后端服务&#…

DP(1500-1700)(刷题)

1.状态机模型&#xff1a;https://codeforces.com/contest/1984/problem/C2 记一下max与min状态转移即可&#xff0c;下面是AC代码&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long ll; ll a[200010],t,n; ll dp[200010][2];//dp[i][0]表示…

啊?现在不懂 AI ,相当于十年前不懂电脑?

最近有关萝卜快跑的新闻铺天盖地&#xff0c;一篇篇都在唱衰&#xff0c;好像千万滴滴师傅立马就要失业了一样。 还没等多久&#xff0c;在朋友圈看到这样一句话&#xff0c;“现在不懂 AI &#xff0c;相当于十年前不懂电脑”。 我想了许久&#xff0c;最终不得不承认这个事实…

深度学习入门——误差反向传播

要正确理解误差反向传播法&#xff0c;我个人认为有两种方法&#xff1a;一种是基于数学式&#xff1b;另一种是基于计算图&#xff08;computational graph&#xff09; 前者是比较常见的方法&#xff0c;机器学习相关的图书中多数都是以数学式为中心展开论述的。因为这种方法…

达梦数据库的系统视图v$sqltext

达梦数据库的系统视图v$sqltext 在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;V$SQLTEXT 是一个系统视图&#xff0c;用于显示当前正在执行或最近执行的SQL语句的文本信息。这个视图对于监控和分析数据库中的SQL活动非常有用&#xff0c;尤其是在需要调试性…

【python】OpenCV—Coordinates Sorted Clockwise

文章目录 1、需求介绍2、算法实现3、完整代码 1、需求介绍 调用 opencv 库&#xff0c;绘制轮廓的矩形边框&#xff0c;坐标顺序为右下→左下→左上→右上&#xff0c;我们实现一下转化为熟悉的 左上→右上→右下→左下 形式 按照这样的顺序组织边界框坐标是执行透视转换或匹…

13. C++继承 | 详解 | 虚拟继承及底层实现

目录 1.定义 1.1继承的概念 1.2 继承的定义 2. 对象赋值转换 3. 继承中的作用域 a. 隐藏/重定义 (Hiding/Redefinition) b. 重载 (Overloading) c. 重写/覆盖 (Overriding) d. 编译报错 (Compilation Error) 4. 派生类的默认成员函数 构造 拷贝构造 运算符重载 析…

处理uniapp刷新后,点击返回按钮跳转到登录页的问题

在使用uniapp的原生返回的按钮时&#xff0c;如果没有刷新会正常返回到对应的页面&#xff0c;如果刷新后会在当前页反复横跳&#xff0c;或者跳转到登录页。那个时候我第一个想法时&#xff1a;使用浏览器的history.back()方法。因为浏览器刷新后还是可以通过右上角的返回按钮…

Vscode+Pyside6开发之虚拟环境配置以及错误解决

Pyside开发之虚拟环境配置以及错误解决 开发环境一、项目创建以及虚拟环境设置1.创建项目2. 新建py文件,新建虚拟环境3.激活虚拟环境二、项目位置改变pip命令报错1.删除原来的虚拟环境2. 产生包列表文件requirements.txt3.重新创建虚拟环境4.重新安装包文件5.其他错误开发环境…

操作系统 输入输出系统

输入输出系统 I/O系统的功能、模型和接口 功能 隐藏物理设备的细节&#xff1a;仅向上层进程提供少量的、抽象的读/写命令 与设备无关性&#xff1a;用户不仅可以使用抽象的I/O命令&#xff0c;还可使用抽象的逻辑设备名来使用设备 提高处理机和I/O设备的利用率&#xff1a;…

IDEA SpringBoot实现定时任务(保姆级教程,超详细!!!)

目录 1. 前言 2. 创建SpringBoot项目 3. Maven依赖引入 4. 创建定时任务 5. 问题&#xff1a;执行时间延迟和单线程执行 5.1 问题原因 5.2 解决方式 1. 前言 在现代化应用中&#xff0c;定时任务&#xff08;Scheduled Tasks&#xff09;是不可或缺的一部分&#xff…

pytorch学习(五)tensorboard使用

1. 创建环境 首先创建一个环境: conda create -n pytorch conda activate pytorch 然后安装tensorboard pip install tensorboard 安装opencv pip install opencv-python 2. 简单的案例 标量和图像的显示&#xff1a; 2.1标量实现的方法是add_scalar,第一个参数是给显…