图的遍历——深度优先搜索(DFS)与广度优先搜索(BFS)(附带C语言源码)

简介: 图的遍历——深度优先搜索(DFS)与广度优先搜索(BFS)(附带C语言源码)

前言


在此之前我们学习过了图的一些基本概念,如同在二叉树中我们有前序遍历,中序遍历,后序遍历一般,在图中也有两种特殊的遍历方式——深度优先遍历与广度优先遍历


深度优先搜索 (DFS)


深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。


深度优先搜索是一种在开发爬虫早期使用较多的方法。它的目的是要达到被搜索结构的叶结点(即那些不包含任何超链的HTML文件) 。在一个HTML文件中,当一个超链被选择后,被链接的HTML文件将执行深度优先搜索,即在搜索其余的超链结果之前必须先完整地搜索单独的一条链。深度优先搜索沿着HTML文件上的超链走到不能再深入为止,然后返回到某一个HTML文件,再继续选择该HTML文件中的其他超链。当不再有其他超链可选择时,说明搜索已经结束。


深度优先搜索沿着一条路径一直搜索下去,在无法搜索时,回退到刚刚访问过的节点。深度优先遍历是按照深度优先搜索的方式对图进行遍历的。


算法原理


深度优先遍历图的方法是,从图中某顶点v出发:


访问顶点v;

依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;

若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

在dfs的方法中传入一个表示当前顶点序号的参数,在dfs方法内部使用一个for循环来检查当前顶点连接的其他邻居,在递归方法中每访问一个节点那么我们就可以使用输出语句来进行输出,输出结果为深度优先搜索访问节点的顺序


搜索算法简而言之就是穷举所有可能情况并找到合适的答案,所以最基本的问题就是罗列出所有可能的情况,这其实就是一种产生式系统。

我们将所要解答的问题划分成若干个阶段或者步骤,当一个阶段计算完毕,下面往往有多种可选选择,所有的选择共同组成了问题的解空间,对搜索算法而言,将所有的阶段或步骤画出来就类似是树的结构。

我们使用递归实现DFS。首先访问起点s,并标记它已经被访问。然后遍历s的邻居节点,如果某个邻居节点w未被访问过,就递归地访问w。这个递归过程会使得我们从s开始尽可能地往下深入,直到遇到死胡同才会回溯到上一个节点继续遍历其他未访问的邻居。执行完所有的递归操作后,我们就完成了整个图的深度遍历。


所有的搜索算法从其最终的算法实现上来看,都可以划分成两个部分──控制结构和产生系统。


1684829045251.png


代码实现(C语言)


#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX_NUM 100 // 定义最大的顶点数目
// 定义一个图的数据结构(使用邻接表表示)
typedef struct Graph {
    int V; // 顶点数目
    int** adj_list; // 邻接表
} Graph;
// 创建一条边
int* create_edge(int w) {
    int* edge = (int*) malloc(sizeof(int));
    *edge = w;
    return edge;
}
// 构造函数
Graph* create_graph(int V) {
    Graph* G = (Graph*) malloc(sizeof(Graph));
    G->V = V;
    G->adj_list = (int**) malloc(V * sizeof(int*));
    for (int i = 0; i < V; i++) {
        G->adj_list[i] = NULL;
    }
    return G;
}
// 添加一条边
void add_edge(Graph* G, int v, int w) {
    G->adj_list[v] = create_edge(w);
}
// 释放图的内存
void free_graph(Graph* G) {
    for (int i = 0; i < G->V; i++) {
        if (G->adj_list[i] != NULL) {
            free(G->adj_list[i]);
        }
    }
    free(G->adj_list);
    free(G);
}
// 深度优先搜索算法
void dfs_helper(Graph* G, int v, int visited[]) {
    visited[v] = 1;
    printf("%d ", v);
    int* adj = G->adj_list[v];
    while (adj != NULL) {
        int w = *adj;
        if (visited[w] == 0) {
            dfs_helper(G, w, visited);
        }
        adj = adj + 1;
    }
}
void dfs(Graph* G) {
    // 初始化visited数组
    int visited[MAX_VERTEX_NUM];
    for (int i = 0; i < G->V; i++) {
        visited[i] = 0;
    }
    // 对每个未被访问的顶点调用dfs_helper函数
    for (int i = 0; i < G->V; i++) {
        if (visited[i] == 0) {
            dfs_helper(G, i, visited);
        }
    }
}
int main() {
    Graph* G = create_graph(6);
    add_edge(G, 0, 1);
    add_edge(G, 0, 2);
    add_edge(G, 1, 3);
    add_edge(G, 2, 3);
    add_edge(G, 2, 4);
    add_edge(G, 3, 5);
    dfs(G);
    free_graph(G);
    return 0;
}
注`在这里插入代码片`意,此代码示例中使用一个较简单的图数据结构表示。具体实现时,可以根据实际需要自定义图的数据结构和相关操作。


广度优先搜索


广度优先算法(Breadth-First Search),同广度优先搜索,又称作宽度优先搜索,或横向优先搜索,简称BFS,是一种图形搜索演算法。简单的说,BFS是从根节点开始,沿着树的宽度遍历树的节点,如果发现目标,则演算终止。广度优先搜索的实现一般采用open-closed表。


BFS是一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。BFS并不使用经验法则算法。

从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的伫列中。一般的实作里,其邻居节点尚未被检验过的节点会被放置在一个被称为open的容器中(例如伫列或是链表),而被检验过的节点则被放置在被称为closed的容器中。(open-closed表)


算法原理


所谓广度,就是一层一层的,向下遍历,层层堵截


访问顶点vi ;

访问vi 的所有未被访问的邻接点w1 ,w2 , …wk ;

依次从这些邻接点(在步骤②中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问;

BFS使用一个队列来维护当前正在遍历的节点,首先将起点加入队列,然后只要队列不为空,就从队列中取出一个节点进行拓展(即获取它的邻居节点),依次将这些邻居节点加入队列中。因此,BFS按照离起点的距离逐层遍历,可以求出起点到每个节点的最短路径。

我们先定义了一个队列来维护当前正在遍历的节点,使用数组实现即可。我们还需要记录每个节点是否已经被访问,使用一个数组visited来实现。在BFS算法中,我们每次取出队列的头部节点进行拓展,将其邻居节点加入队列的尾部,并标记它们已经被访问。执行完这些拓展操作后,我们继续从队列头部取出下一个节点进行拓展,直到队列为空。


代码实现(C语言)


#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX_NUM 100 // 定义最大的顶点数目
// 定义一个图的数据结构(使用邻接表表示)
typedef struct Graph {
    int V; // 顶点数目
    int** adj_list; // 邻接表
} Graph;
// 创建一条边
int* create_edge(int w) {
    int* edge = (int*) malloc(sizeof(int));
    *edge = w;
    return edge;
}
// 构造函数
Graph* create_graph(int V) {
    Graph* G = (Graph*) malloc(sizeof(Graph));
    G->V = V;
    G->adj_list = (int**) malloc(V * sizeof(int*));
    for (int i = 0; i < V; i++) {
        G->adj_list[i] = NULL;
    }
    return G;
}
// 添加一条边
void add_edge(Graph* G, int v, int w) {
    G->adj_list[v] = create_edge(w);
}
// 释放图的内存
void free_graph(Graph* G) {
    for (int i = 0; i < G->V; i++) {
        if (G->adj_list[i] != NULL) {
            free(G->adj_list[i]);
        }
    }
    free(G->adj_list);
    free(G);
}
// 广度优先搜索算法
void bfs(Graph* G, int s) {
    int visited[MAX_VERTEX_NUM] = {0}; // 记录每个节点是否已经被访问
    int queue[MAX_VERTEX_NUM], head = 0, tail = 0; // 使用队列来存储正在遍历的节点
    visited[s] = 1;
    queue[tail++] = s;
    while (head < tail) {
        int v = queue[head++];
        printf("%d ", v);
        int* adj = G->adj_list[v];
        while (adj != NULL) {
            int w = *adj;
            if (visited[w] == 0) {
                visited[w] = 1;
                queue[tail++] = w;
            }
            adj = adj + 1;
        }
    }
}
int main() {
    Graph* G = create_graph(6);
    add_edge(G, 0, 1);
    add_edge(G, 0, 2);
    add_edge(G, 1, 3);
    add_edge(G, 2, 3);
    add_edge(G, 2, 4);
    add_edge(G, 3, 5);
    bfs(G, 0);
    free_graph(G);
    return 0;
}


目录
相关文章
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
1194 10
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
130 1
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
762 16
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
841 4
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
567 7
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
815 8
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
1624 9
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
182 1
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
1494 4
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
538 3