文章目录
图的两种遍历算法:广度优先搜索(BFS)与深度优先搜索(DFS)
图(Graph)是计算机科学中表示实体间关系的重要数据结构,广泛应用于社交网络、路径规划、网络拓扑分析等领域。图的遍历是图算法的基础,其中 广度优先搜索(BFS) 和 深度优先搜索(DFS) 是两种最核心的遍历方法。本文将从原理、实现、复杂度到经典例题进行系统讲解。
一、图的表示与基本概念
1. 图的定义
图由 顶点(Vertex/Node) 和 边(Edge) 组成,分为:
- 无向图:边无方向(如朋友关系)。
- 有向图:边有方向(如网页超链接)。
- 带权图:边有权重(如地图距离)。
2. 图的存储方式
- 邻接矩阵:二维数组表示顶点间的连接关系,适合稠密图。
- 邻接表:链表或数组的数组,存储每个顶点的邻居,适合稀疏图。
二、广度优先搜索(BFS)
1. 算法思想
BFS 按“层次”遍历图,从起点开始逐层向外扩散,确保先访问离起点最近的节点。
核心数据结构:队列(FIFO)。
2. 算法步骤
- 初始化队列,将起点加入队列,并标记为已访问。
- 循环执行以下操作,直到队列为空:
- 取出队列头部节点
u
。 - 访问
u
的所有未访问邻居节点,依次标记并加入队列。
- 取出队列头部节点
3. 代码实现(C语言)
#include <stdio.h>
#include <stdlib.h>
#define MAX_NODES 100
// 邻接表节点
struct Node {
int vertex;
struct Node* next;
};
// 图结构
struct Graph {
int numVertices;
struct Node** adjLists;
int* visited;
};
// 创建节点
struct Node* createNode(int v) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->vertex = v;
newNode->next = NULL;
return newNode;
}
// 创建图
struct Graph* createGraph(int vertices) {
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->numVertices = vertices;
graph->adjLists = (struct Node**)malloc(vertices * sizeof(struct Node*));
graph->visited = (int*)malloc(vertices * sizeof(int));
for (int i = 0; i < vertices; i++) {
graph->adjLists[i] = NULL;
graph->visited[i] = 0;
}
return graph;
}
// 添加边(无向图)
void addEdge(struct Graph* graph, int src, int dest) {
// 添加 src 到 dest 的边
struct Node* newNode = createNode(dest);
newNode->next = graph->adjLists[src];
graph->adjLists[src] = newNode;
// 添加 dest 到 src 的边
newNode = createNode(src);
newNode->next = graph->adjLists[dest];
graph->adjLists[dest] = newNode;
}
// BFS 实现
void bfs(struct Graph* graph, int startVertex) {
int queue[MAX_NODES];
int front = 0, rear = 0;
graph->visited[startVertex] = 1;
queue[rear++] = startVertex;
while (front < rear) {
int currentVertex = queue[front++];
printf("Visited %d\n", currentVertex);
struct Node* temp = graph->adjLists[currentVertex];
while (temp) {
int adjVertex = temp->vertex;
if (!graph->visited[adjVertex]) {
graph->visited[adjVertex] = 1;
queue[rear++] = adjVertex;
}
temp = temp->next;
}
}
}
4. 复杂度分析
- 时间复杂度:O(V + E),需访问所有节点和边。
- 空间复杂度:O(V),队列最多存储一层节点。
5. 典型应用
- 无权图的最短路径(如迷宫最短步数)。
- 社交网络中的“六度分隔”理论。
- 检测图的连通性。
三、深度优先搜索(DFS)
1. 算法思想
DFS 沿一条路径尽可能深入探索,直到无法继续前进时回溯到上一个分叉点。
核心数据结构:栈(LIFO,递归隐式使用系统栈)。
2. 算法步骤
- 从起点出发,标记为已访问。
- 递归访问当前节点的未访问邻居,直到所有路径都被探索。
3. 代码实现(C语言)
// DFS 递归实现
void dfs(struct Graph* graph, int vertex) {
struct Node* adjList = graph->adjLists[vertex];
graph->visited[vertex] = 1;
printf("Visited %d\n", vertex);
while (adjList) {
int connectedVertex = adjList->vertex;
if (!graph->visited[connectedVertex]) {
dfs(graph, connectedVertex);
}
adjList = adjList->next;
}
}
4. 复杂度分析
- 时间复杂度:O(V + E),与 BFS 相同。
- 空间复杂度:O(V),递归栈深度最坏为节点数。
5. 典型应用
- 寻找所有可能的路径(如迷宫所有解法)。
- 拓扑排序(如课程安排依赖)。
- 检测图中的环。
四、BFS 与 DFS 对比
特性 | BFS | DFS |
---|---|---|
遍历顺序 | 层次遍历(由近及远) | 深度优先(一条路走到底) |
数据结构 | 队列 | 栈(递归或显式维护) |
最短路径 | 天然支持无权图最短路径 | 需要额外记录路径信息 |
空间复杂度 | 较高(存储一层节点) | 较低(递归栈深度) |
适用场景 | 最短路径、社交网络层级关系 | 全路径搜索、拓扑排序、连通性检测 |
五、经典例题解析
例题 1:200. 岛屿数量(BFS/DFS)
题目描述
给定一个由 '1'
(陆地)和 '0'
(水)组成的二维网格,计算岛屿的数量。
解题思路
遍历每个单元格,遇到陆地时启动 BFS/DFS,标记所有相连的陆地,统计启动次数即为岛屿数量。
代码片段(DFS)
void dfs_island(char** grid, int gridSize, int gridColSize, int i, int j) {
if (i < 0 || i >= gridSize || j < 0 || j >= gridColSize || grid[i][j] != '1')
return;
grid[i][j] = '0'; // 标记为已访问
dfs_island(grid, gridSize, gridColSize, i+1, j);
dfs_island(grid, gridSize, gridColSize, i-1, j);
dfs_island(grid, gridSize, gridColSize, i, j+1);
dfs_island(grid, gridSize, gridColSize, i, j-1);
}
int numIslands(char** grid, int gridSize, int* gridColSize) {
int count = 0;
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridColSize[0]; j++) {
if (grid[i][j] == '1') {
dfs_island(grid, gridSize, gridColSize[0], i, j);
count++;
}
}
}
return count;
}
例题 2:133. 克隆图(BFS)
题目描述
深度复制一个无向连通图的所有节点。
解题思路
使用 BFS 遍历原图,同时用哈希表存储原节点到克隆节点的映射。
代码片段
struct Node {
int val;
int numNeighbors;
struct Node** neighbors;
};
struct Node* cloneGraph(struct Node* s) {
if (!s) return NULL;
struct Node* clones[101] = {NULL}; // 假设节点值 <= 100
struct Node* queue[101];
int front = 0, rear = 0;
// 创建起始节点克隆
clones[s->val] = (struct Node*)malloc(sizeof(struct Node));
clones[s->val]->val = s->val;
clones[s->val]->numNeighbors = s->numNeighbors;
clones[s->val]->neighbors = (struct Node**)malloc(s->numNeighbors * sizeof(struct Node*));
queue[rear++] = s;
while (front < rear) {
struct Node* curr = queue[front++];
for (int i = 0; i < curr->numNeighbors; i++) {
struct Node* neighbor = curr->neighbors[i];
if (!clones[neighbor->val]) {
clones[neighbor->val] = (struct Node*)malloc(sizeof(struct Node));
clones[neighbor->val]->val = neighbor->val;
clones[neighbor->val]->numNeighbors = neighbor->numNeighbors;
clones[neighbor->val]->neighbors = (struct Node**)malloc(neighbor->numNeighbors * sizeof(struct Node*));
queue[rear++] = neighbor;
}
clones[curr->val]->neighbors[i] = clones[neighbor->val];
}
}
return clones[s->val];
}
例题 3:797. 所有可能路径(DFS)
题目描述
给定有向无环图(DAG),找出从节点 0
到节点 n-1
的所有路径。
解题思路
使用 DFS 回溯,递归记录路径。
代码片段
void dfs_paths(int** graph, int graphSize, int* graphColSize,
int* returnSize, int** returnColumnSizes,
int** res, int* path, int pathSize, int node) {
path[pathSize++] = node;
if (node == graphSize - 1) {
res[*returnSize] = (int*)malloc(pathSize * sizeof(int));
memcpy(res[*returnSize], path, pathSize * sizeof(int));
(*returnColumnSizes)[*returnSize] = pathSize;
(*returnSize)++;
return;
}
for (int i = 0; i < graphColSize[node]; i++) {
dfs_paths(graph, graphSize, graphColSize, returnSize, returnColumnSizes,
res, path, pathSize, graph[node][i]);
}
}
int** allPathsSourceTarget(int** graph, int graphSize, int* graphColSize,
int* returnSize, int** returnColumnSizes) {
int** res = (int**)malloc(1000 * sizeof(int*));
*returnColumnSizes = (int*)malloc(1000 * sizeof(int));
int* path = (int*)malloc(graphSize * sizeof(int));
*returnSize = 0;
dfs_paths(graph, graphSize, graphColSize, returnSize, returnColumnSizes,
res, path, 0, 0);
free(path);
return res;
}
六、总结
- BFS 适用场景:最短路径、层级遍历、最小步数问题。
- DFS 适用场景:全路径搜索、连通性检测、回溯问题。
- 关键选择依据:是否需要最短路径,或对空间复杂度有特殊要求。