一、BFS 核心定义
BFS(Breadth-First Search,广度优先搜索)
是一种图/网格/树的遍历算法,核心规则:
从起点出发,一层一层向外扩散遍历,先访问离起点最近的节点,再访问次近的节点
关键特性(必背!)
- 天然求解「无权图最短路径」(第一次到达目标节点的路径,就是最短路径)
- 遍历顺序:近 → 远(层序遍历)
- 必须用队列(Queue) 实现(先进先出,保证层序)
- 必须用访问标记数组(防止重复遍历/死循环)
二、BFS 必备核心组件
1. 数据结构:队列 queue
- 特性:FIFO(先进先出),严格保证「先入队的节点先遍历」
- C++ 头文件:
#include <queue> - 网格场景常用:
queue<pair<int, int>> q(存储坐标(x,y))
2. 方向数组 DIRS(网格题必用)
遍历上下左右四个方向,标准化写法:
cpp
// 上下左右
constexpr int DIRS[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
3. 访问标记数组 vis / 距离数组 dis
- 标记是否遍历过:
vector<vector<bool>> vis - 记录最短距离:
vector<vector<int>> dis(初始值-1代表未访问)
三、BFS 标准执行流程(万能步骤)
以二维网格为例:
- 初始化:起点入队 + 标记起点已访问/距离为0
- 循环遍历:队列不为空时,取出队首节点
- 扩散邻节点:遍历上下左右四个方向
- 合法性判断:不越界 + 未访问 + 符合条件
- 更新状态:标记访问/记录距离 + 入队
- 结束:队列为空,遍历完成
四、BFS 三大核心分类(覆盖所有刷题场景)
分类1:单源BFS(单个起点)
✅ 适用:单个起点的最短路径/遍历
❌ 效率低,适合简单场景
C++ 单源BFS模板(网格)
cpp
#include <vector>
#include <queue>
using namespace std;
constexpr int DIRS[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
// 单源BFS:从(start_x, start_y)出发,遍历网格
vector<vector<int>> bfs(vector<vector<int>>& grid, int start_x, int start_y) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dis(m, vector<int>(n, -1)); // 距离数组,-1=未访问
queue<pair<int, int>> q;
// 1. 初始化起点
dis[start_x][start_y] = 0;
q.push({start_x, start_y});
// 2. 循环遍历
while (!q.empty()) {
auto [x, y] = q.front(); q.pop(); // 取出队首
// 3. 遍历四个方向
for (auto [dx, dy] : DIRS) {
int nx = x + dx;
int ny = y + dy;
// 4. 合法性判断:不越界 + 未访问
if (nx >=0 && nx < m && ny >=0 && ny < n && dis[nx][ny] == -1) {
dis[nx][ny] = dis[x][y] + 1; // 更新距离
q.push({nx, ny}); // 入队
}
}
}
return dis;
}
分类2:多源BFS(多个起点同时入队)
✅ 适用:多个起点同时扩散 (陆地→海洋、病毒扩散、多源最短路径)
✅ 核心:把所有起点一次性入队 ,再统一层序遍历
✅ 效率:O(n²)(远优于单源BFS的O(n⁴))
C++ 多源BFS模板
cpp
#include <vector>
#include <queue>
#include <climits>
using namespace std;
constexpr int DIRS[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
class Solution {
public:
int maxDistance(vector<vector<int>>& grid) {
int n = grid.size();
queue<pair<int, int>> q;
vector<vector<int>> dis(n, vector<int>(n, -1));
// 1. 多源初始化:所有陆地(1)同时入队
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
q.push({i, j});
dis[i][j] = 0;
}
}
}
int maxDis = -1;
// 2. 统一BFS扩散
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
for (auto [dx, dy] : DIRS) {
int nx = x + dx;
int ny = y + dy;
if (nx >=0 && nx < n && ny >=0 && ny < n && dis[nx][ny] == -1) {
dis[nx][ny] = dis[x][y] + 1;
maxDis = max(maxDis, dis[nx][ny]);
q.push({nx, ny});
}
}
}
return maxDis;
}
};
分类3:层序BFS(记录层数)
✅ 适用:二叉树层序遍历、按层统计结果
C++ 层序BFS模板(二叉树)
cpp
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
// 层序遍历:按层输出节点
void levelOrder(TreeNode* root) {
if (!root) return;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int size = q.size(); // 关键:记录当前层节点数
// 遍历当前层所有节点
for (int i = 0; i < size; i++) {
TreeNode* node = q.front(); q.pop();
// 处理节点
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
}
五、BFS 高频应用场景
- 网格最短路径(迷宫、海洋陆地、地图分析)
- 多源扩散问题(病毒扩散、颜色填充、洪水蔓延)
- 二叉树层序遍历(求深度、按层输出)
- 无权图最短路径(社交网络、路径规划)
- 连通分量统计(岛屿数量、连通块个数)
六、BFS 易错点
1. 访问标记必须「入队时标记」,不是出队时
否则会导致重复入队、死循环、超时
2. 函数参数必须用「引用传递 &」
错误:vector<vector<int>> min_dis → 拷贝副本,修改无效
正确:vector<vector<int>>& min_dis → 直接修改原数组
3. 多源BFS必须「一次性入队所有起点」
不能逐个遍历起点BFS(效率极低,结果错误)
4. 边界判断顺序不能错
0 <= nx < m && 0 <= ny < n(先判断不越界,再访问数组)
5. 距离数组初始值必须统一
常用 -1 代表未访问,0 代表起点
七、BFS vs DFS
| 特性 | BFS 广度优先 | DFS 深度优先 |
|---|---|---|
| 遍历方式 | 一层一层扩散(近→远) | 一条路走到底(深→浅) |
| 数据结构 | 队列 queue | 栈 stack / 递归 |
| 最短路径 | ✅ 天然支持(无权图) | ❌ 不支持 |
| 内存占用 | 较大(层节点多) | 较小(递归栈) |
| 适用场景 | 最短路径、多源扩散 | 连通性、路径探索 |
BFS 核心口诀
- 队列存节点,一层一层扫
- 方向数组定上下,标记数组防重复
- 单源找单点,多源找全局
- 第一次到达 = 最短路径
- 引用传递别忘写,边界判断不能少
一、DFS 核心定义与本质
1. 通俗定义
一条路走到黑,不撞南墙不回头,撞墙就回溯,换条路继续走
- 从起点出发,优先往更深的节点探索
- 无法继续深入时,回溯到上一个未探索完分支的节点
- 重复上述过程,直到所有节点遍历完成
2. 底层本质
- 数据结构:栈(Stack)(先进后出,保证深度优先)
- 实现形式:递归(系统自动维护调用栈) 或 手动模拟栈(非递归)
- 核心思想:回溯 + 试探
3. 关键特性(必背)
- 不保证无权图最短路径(第一次到达≠最短)
- 天然适合连通性判断、路径枚举、回溯组合问题
- 递归实现代码极简,非递归可避免栈溢出
- 必须加访问标记,否则会无限递归/重复遍历
- 空间复杂度由搜索深度决定(远小于BFS的层节点数)
二、DFS 必备核心组件
1. 方向数组 DIRS(网格题通用)
和BFS完全一致,用于遍历上下左右/八方向:
cpp
// 四方向:上下左右
const int DIR4[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
// 八方向:四方向+斜角(岛屿周长、扫雷等题用)
const int DIR8[8][2] = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}};
2. 访问标记数组 vis
- 标记节点是否被遍历,防止重复递归、死循环
- 网格:
vector<vector<bool>> vis - 图:
vector<bool> vis - 也可原地修改原数组(节省空间,如把1改为0标记已访问)
3. 递归终止条件(边界条件)
DFS的灵魂,没有终止条件会栈溢出(Stack Overflow)
常见终止:
- 坐标越界
- 已访问过该节点
- 到达目标节点
- 不符合题目条件(如海洋不能走)
三、DFS 两种实现方式
方式1:递归 DFS(最常用,代码极简)
系统自动维护调用栈,无需手动管理栈结构,是刷题首选。
核心执行流程
- 判断终止条件,满足则直接返回
- 标记当前节点为已访问
- 遍历所有邻接节点(方向/子节点)
- 对合法邻接节点递归调用DFS
- (可选)回溯:取消当前节点标记(用于枚举所有路径)
方式2:非递归 DFS(手动模拟栈)
解决递归深度过深导致栈溢出 的问题(如网格1e4×1e4),用stack手动模拟系统调用栈。
核心执行流程
- 起点入栈,标记已访问
- 栈不为空时,取出栈顶节点
- 遍历所有邻接节点
- 合法未访问节点入栈,标记已访问
- 重复直到栈空
四、DFS 标准模板
模板1:网格递归 DFS(最常用,适配岛屿、地图、迷宫)
模板(计算每个连通块的大小):
cpp
#include <vector>
using namespace std;
constexpr int DIRS[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // 左右上下
// 返回网格图 grid 每个连通块的大小
// 时间复杂度 O(mn)
vector<int> dfsGrid(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
vector vis(m, vector<int8_t>(n));
// 返回当前连通块的大小
auto dfs = [&](this auto&& dfs, int i, int j) -> int {
vis[i][j] = true;
int size = 1;
for (auto [dx, dy] : DIRS) {
int x = i + dx, y = j + dy;
// 这里 grid[x][y] == '.' 根据题意修改
if (0 <= x && x < m && 0 <= y && y < n && grid[x][y] == '.' && !vis[x][y]) {
size += dfs(x, y);
}
}
return size;
};
vector<int> comp_size;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] != '.' || vis[i][j]) { // grid[i][j] != '.' 根据题意修改
continue;
}
int size = dfs(i, j);
comp_size.push_back(size);
}
}
return comp_size;
}
模板2:网格非递归 DFS(手动栈,防栈溢出)
cpp
#include <vector>
#include <stack>
using namespace std;
const int DIR4[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
class Solution {
public:
int numIslands(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
int cnt = 0;
stack<pair<int, int>> st;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == 1) {
cnt++;
// 起点入栈
st.push({i, j});
grid[i][j] = 0;
// 手动栈DFS
while(!st.empty()) {
auto [x, y] = st.top();
st.pop();
// 遍历四方向
for(auto& d : DIR4) {
int nx = x + d[0];
int ny = y + d[1];
if(nx >=0 && nx < m && ny >=0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 0;
st.push({nx, ny});
}
}
}
}
}
}
return cnt;
}
};
模板3:二叉树 DFS(前/中/后序遍历,均为DFS)
二叉树的所有深度遍历都是DFS,递归实现极简:
cpp
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<int> res;
// 前序DFS:根→左→右
void preDFS(TreeNode* root) {
if(!root) return;
res.push_back(root->val);
preDFS(root->left);
preDFS(root->right);
}
// 中序DFS:左→根→右
void inDFS(TreeNode* root) {
if(!root) return;
inDFS(root->left);
res.push_back(root->val);
inDFS(root->right);
}
// 后序DFS:左→右→根
void postDFS(TreeNode* root) {
if(!root) return;
postDFS(root->left);
postDFS(root->right);
res.push_back(root->val);
}
};
五、DFS 进阶核心知识点
1. 回溯(Backtracking)------ DFS 灵魂
回溯 = 递归深入 + 状态恢复
适用场景:全排列、子集、组合总和、迷宫所有路径
回溯核心代码逻辑
cpp
void dfs(参数) {
if(终止条件) {
保存结果;
return;
}
for(遍历所有选择) {
做选择; // 标记状态
dfs(下一层); // 递归深入
撤销选择; // 回溯!恢复状态(关键)
}
}
示例:全排列 DFS 回溯
cpp
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<bool> vis;
void dfs(vector<int>& nums) {
if(path.size() == nums.size()) {
res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); ++i) {
if(vis[i]) continue;
vis[i] = true;
path.push_back(nums[i]);
dfs(nums); // 深入
path.pop_back(); // 回溯撤销
vis[i] = false; // 回溯撤销
}
}
vector<vector<int>> permute(vector<int>& nums) {
vis.resize(nums.size(), false);
dfs(nums);
return res;
}
};
2. 记忆化 DFS(剪枝,避免重复计算)
对重复子问题缓存结果,解决DFS超时问题(如斐波那契、动态规划类搜索)
cpp
// 记忆化数组,缓存已计算结果
vector<vector<int>> memo;
int dfs(int x, int y) {
if(memo[x][y] != -1) return memo[x][y]; // 直接返回缓存
int res = 计算逻辑;
memo[x][y] = res; // 缓存结果
return res;
}
3. 多源 DFS
多个起点同时启动DFS,和多源BFS逻辑一致(如你之前的地图分析题)
- 遍历所有起点,对每个起点执行DFS
- 用全局数组记录最小距离/状态
4. 剪枝(Pruning)
提前排除不可能的分支,大幅减少递归次数,是DFS优化核心手段
- 可行性剪枝:不符合条件直接return
- 最优性剪枝:当前结果已差于最优解,直接return
六、DFS 时间&空间复杂度
1. 时间复杂度
- 网格/图:O(V+E)
V=节点数(网格m×n),E=边数(四方向网格每个节点4条边) - 回溯枚举:O(k×n!)(k为结果长度,n为元素数)
2. 空间复杂度
- 递归DFS:O(深度)(系统调用栈,二叉树为O(h),网格最坏O(mn))
- 非递归DFS:O(节点数)(手动栈存储)
- 回溯:O(路径长度)
七、DFS 高频易错点
1. 缺少递归终止条件 → 栈溢出
这是最致命错误,必须先判断越界、非法状态再递归。
2. 忘记标记访问 → 无限递归
网格题原地修改/vis数组必须加,否则重复遍历同一节点。
3. 回溯未恢复状态 → 结果错误
枚举路径类问题,递归返回后必须撤销选择,否则状态污染。
4. 递归深度过大 → 系统栈溢出
C++默认递归栈深度约1e4,超大网格必须用非递归DFS。
5. 边界判断顺序错误
必须先判断x >=0 && x < m,再访问数组,否则越界崩溃。
6. 混淆DFS与BFS适用场景
DFS不找最短路径,最短路径必须用BFS(你之前的1162题就是典型)。
八、DFS vs BFS 对比
| 维度 | DFS 深度优先搜索 | BFS 广度优先搜索 |
|---|---|---|
| 遍历方式 | 一条路走到底,回溯 | 一层一层扩散,层序遍历 |
| 数据结构 | 栈(递归/手动栈) | 队列 |
| 最短路径 | ❌ 不支持(无权图) | ✅ 天然支持(第一次到达即最短) |
| 空间占用 | 小(由深度决定) | 大(由最宽层节点数决定) |
| 代码实现 | 递归极简,非递归繁琐 | 代码统一,无栈溢出风险 |
| 适用场景 | 连通性、回溯枚举、路径所有方案 | 最短路径、多源扩散、层序遍历 |
| 遍历顺序 | 深→浅 | 近→远 |
| 典型题目 | 岛屿数量、全排列、子集、连通分量 | 地图分析、迷宫最短路径、层序遍历 |
DFS 万能口诀
- 递归栈,深到底,撞南墙,就回溯
- 终止条件放第一,越界非法全返回
- 访问标记不能忘,原地修改省空间
- 枚举路径要回溯,状态撤销是关键
- 最短路径别用DFS,BFS才是最优解
- 深度太大栈溢出,手动栈来救场