从零开始,用动画和代码彻底搞懂两种最核心的搜索算法
想象你在一个巨大的迷宫里,需要找到出口。你会怎么走?
在计算机的世界里,搜索就是系统地遍历图或树中的所有节点,找到你需要的东西——比如一条路径、一个目标,或者所有可能的方案。
在学习 DFS 和 BFS 之前,我们需要知道它们在什么上面跑:
A → [B, C]。Depth-First Search —— 一条路走到黑,碰壁再回头
DFS 的策略是:从起点出发,沿着一条路一直走,走到走不动了(没有未访问的邻居),就退回上一个路口,换一条路继续走。
点击「播放」看 DFS 如何遍历这个图。注意观察栈的变化和回溯过程!
1#include <iostream>
2#include <vector>
3using namespace std;
4
5vector<int> adj[100]; // 邻接表
6bool visited[100]; // 标记是否访问过
7
8void dfs(int u) {
9 visited[u] = true; // 1. 标记当前节点已访问
10 cout << u << " "; // 2. 处理当前节点(输出)
11
12 // 3. 遍历所有相邻节点
13 for (int i = 0; i < adj[u].size(); i++) {
14 int v = adj[u][i]; // v 是 u 的邻居
15 if (!visited[v]) { // 如果 v 还没访问过
16 dfs(v); // 4. 递归访问 v(深入!)
17 } // 5. dfs(v) 返回后,自动回溯到 u
18 }
19}
20
21// 调用:从节点 1 开始搜索
22dfs(1);
Breadth-First Search —— 一层一层扩展,像水波纹一样
BFS 的策略是:从起点出发,先访问所有距离为 1 的节点,再访问所有距离为 2 的节点,一层一层向外扩展,直到访问完所有节点。
同样的图,看看 BFS 的遍历顺序和 DFS 有什么不同!注意观察队列的变化。
1#include <iostream>
2#include <vector>
3#include <queue>
4using namespace std;
5
6vector<int> adj[100]; // 邻接表
7bool visited[100]; // 标记是否访问过
8
9void bfs(int start) {
10 queue<int> q;
11 q.push(start); // 1. 起点入队
12 visited[start] = true; // 2. 标记起点已访问
13
14 while (!q.empty()) { // 3. 队列不空就继续
15 int u = q.front(); // 4. 取出队首
16 q.pop(); // 5. 出队
17 cout << u << " "; // 6. 处理当前节点
18
19 // 7. 遍历所有相邻节点
20 for (int i = 0; i < adj[u].size(); i++) {
21 int v = adj[u][i];
22 if (!visited[v]) { // 8. 如果 v 没访问过
23 visited[v] = true; // 9. 标记已访问
24 q.push(v); // 10. 入队(排在后面)
25 }
26 }
27 }
28}
29
30// 调用:从节点 1 开始搜索
31bfs(1);
同样的迷宫,看看两种算法的搜索方式有什么不同
下面是一个 10×10 的迷宫。DFS 会深入探索、走死路再回头;BFS 会一层层扩展,直接找到最短路径。
切换算法,观察探索过程的区别,特别注意探索的格子数和找到的路径长度!
| 对比项 | 🟣 DFS 深度优先 | 🟢 BFS 广度优先 |
|---|---|---|
| 核心策略 | 一条路走到黑,碰壁再回头 | 一层一层扩展,先近后远 |
| 数据结构 | 栈(Stack)/ 递归 | 队列(Queue) |
| 顺序特点 | 先深后广 | 先近后远 |
| 最短路径 | ❌ 不保证最短 | ✅ 无权图中最短 |
| 时间复杂度 | O(V + E) | O(V + E) |
| 空间复杂度 | O(V)(递归深度) | O(V)(队列大小) |
| 适合问题 | 全排列、组合、连通块、走迷宫 | 最短路、最少步数、层序遍历 |
| 实现方式 | 递归(简洁好写) | while 循环 + 队列 |
| 比喻 | 🏰 探索洞穴 | 🌊 水波纹扩散 |
这些是 GESP 六级常考的搜索题型,一定要掌握!
思路:这是一道经典的 DFS 回溯题。想象有 n 个位置,每个位置放一个数字。用 DFS 逐个位置填数字,填完 n 个就输出一个排列。关键是回溯——填完一个排列后,要撤销选择,尝试其他数字。
#include <iostream>
using namespace std;
int n;
int path[10]; // 存当前排列
bool used[10]; // 标记数字是否用过
void dfs(int step) {
if (step == n) { // 排列完成,输出
for (int i = 0; i < n; i++)
cout << path[i] << " ";
cout << endl;
return;
}
// 尝试每个数字
for (int i = 1; i <= n; i++) {
if (!used[i]) {
path[step] = i; // ① 选择
used[i] = true;
dfs(step + 1); // ② 递归
used[i] = false; // ③ 撤销(回溯!)
}
}
}
int main() {
cin >> n;
dfs(0);
return 0;
}
used[i] = false 千万不能忘!这就是回溯的核心——撤销选择,让这个数字可以在其他分支中被使用。忘了这一步,只能输出一个排列。
思路:求最少步数 → 用 BFS!因为 BFS 一层一层扩展,第一次到达终点时的步数就是最少的。
用 dist[i][j] 记录从起点到 (i,j) 的距离,初始全设为 -1 表示未访问。起点距离设为 0,每次扩展时距离 +1。
#include <iostream>
#include <queue>
using namespace std;
int n, m;
char maze[100][100];
int dist[100][100]; // 距离数组,-1 表示未访问
int dx[] = {0, 0, 1, -1}; // 右 左 下 上
int dy[] = {1, -1, 0, 0};
int bfs(int sx, int sy, int ex, int ey) {
queue<pair<int,int>> q;
q.push({sx, sy});
dist[sx][sy] = 0;
while (!q.empty()) {
int x = q.front().first;
int y = q.front().second;
q.pop();
if (x == ex && y == ey)
return dist[x][y]; // 找到终点!
// 尝试四个方向
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
// 检查边界、墙、是否访问过
if (nx >= 0 && nx < n && ny >= 0 && ny < m
&& maze[nx][ny] != '#' && dist[nx][ny] == -1) {
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
}
return -1; // 无法到达
}
int main() {
cin >> n >> m;
int sx, sy, ex, ey;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> maze[i][j];
if (maze[i][j] == 'S') { sx = i; sy = j; }
if (maze[i][j] == 'E') { ex = i; ey = j; }
dist[i][j] = -1;
}
}
cout << bfs(sx, sy, ex, ey) << endl;
return 0;
}
dx[] 和 dy[] 是方向数组,表示四个方向:右、左、下、上。用一个 for 循环就能遍历四个方向,比写四次 if 简洁多了!这是搜索题的常用技巧。
思路:遍历整个网格,每发现一个没访问过的 '1',就找到了一个新岛屿。然后用 DFS 把这个岛屿上所有的 '1' 都标记为已访问(这叫感染/Flood Fill)。最后数一共找到了多少个新岛屿。
#include <iostream>
using namespace std;
int n, m;
char grid[100][100];
bool vis[100][100];
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
void dfs(int x, int y) {
vis[x][y] = true; // 标记为已访问
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
// 边界检查 + 是陆地 + 没访问过
if (nx >= 0 && nx < n && ny >= 0 && ny < m
&& grid[nx][ny] == '1' && !vis[nx][ny]) {
dfs(nx, ny); // 递归访问相连的陆地
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> grid[i][j];
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// 发现一个新岛屿!
if (grid[i][j] == '1' && !vis[i][j]) {
dfs(i, j); // 把整个岛屿标记
count++; // 岛屿数量 +1
}
}
}
cout << count << endl;
return 0;
}
来检验一下你的学习成果吧!
多练习,多画图,多模拟。搜索算法的关键不是背代码,而是理解「怎么走」和「为什么这么走」。
祝你 GESP 六级考试顺利!💪