今日详解深度优先搜索(DFS)与广度优先搜索(BFS),从遍历逻辑到场景应用,结合大厂真题与动图演示,彻底理解两种搜索策略的本质差异。
一、算法核心思想对比
深度优先搜索(DFS)
-
遍历逻辑:类似“迷宫探索”,沿单一路径深入到底,回溯后继续探索其他分支
-
数据结构:递归栈或显式栈(后进先出)
-
核心特征:
-
可能陷入深层次分支(需设置终止条件)
-
空间复杂度低(仅存储单条路径)
-
适用于“探索所有可能性”的场景
-
广度优先搜索(BFS)
-
遍历逻辑:类似“水波扩散”,逐层扫描所有相邻节点
-
数据结构:队列(先进先出)
-
核心特征:
-
首次访问即得最短路径(无权图)
-
空间复杂度较高(存储整层节点)
-
适用于“寻找最近解”的场景
-
二、算法实现模板
DFS模板(递归 vs 显式栈)
// 递归实现(以二叉树为例)
void dfs(TreeNode* node) {
if (!node) return;
// 前序操作
dfs(node->left);
// 中序操作
dfs(node->right);
// 后序操作
}
// 显式栈实现(图遍历)
void dfsStack(vector<vector<int>>& graph, int start) {
stack<int> stk;
vector<bool> visited(graph.size(), false);
stk.push(start);
visited[start] = true;
while (!stk.empty()) {
int u = stk.top();
stk.pop();
for (int v : graph[u]) {
if (!visited[v]) {
visited[v] = true;
stk.push(v); // 注意压栈顺序与遍历顺序的关系
}
}
}
}
BFS模板(队列实现)
void bfs(vector<vector<int>>& graph, int start) {
queue<int> q;
vector<bool> visited(graph.size(), false);
q.push(start);
visited[start] = true;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) { // 层级遍历关键
int u = q.front();
q.pop();
for (int v : graph[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
}
三、经典问题实战
例题1:二叉树层序遍历(BFS应用)
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (!root) return res;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int size = q.size();
vector<int> level;
for (int i=0; i<size; ++i) {
auto node = q.front(); q.pop();
level.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
res.push_back(level);
}
return res;
}
例题2:岛屿数量(DFS应用)
void dfs(vector<vector<char>>& grid, int i, int j) {
if (i<0 || i>=grid.size() || j<0 || j>=grid[0].size() || grid[i][j] != '1')
return;
grid[i][j] = '0'; // 标记为已访问
dfs(grid, i+1, j);
dfs(grid, i-1, j);
dfs(grid, i, j+1);
dfs(grid, i, j-1);
}
int numIslands(vector<vector<char>>& grid) {
int count = 0;
for (int i=0; i<grid.size(); ++i) {
for (int j=0; j<grid[0].size(); ++j) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
++count;
}
}
}
return count;
}
四、复杂度与适用场景对比
维度 | DFS | BFS |
---|---|---|
时间复杂度 | O(V + E) | O(V + E) |
空间复杂度 | O(h)(h为递归深度) | O(w)(w为最大层宽) |
最短路径 | 不保证(除非记录路径) | 天然保证(无权图) |
适用场景 | 连通性检测、回溯问题 | 层序处理、最短路径问题 |
内存风险 | 递归深度过大可能栈溢出 | 层级过宽可能内存爆炸 |
五、大厂真题解析
真题1:迷宫最短路径
题目描述:
给定N×M网格迷宫(0可走,1为墙),求从起点到终点的最短步数。
BFS解法关键点:
-
使用队列记录坐标与步数
-
方向数组简化代码
-
提前终止条件(到达终点)
int shortestPath(vector<vector<int>>& maze, vector<int>& start, vector<int>& end) {
vector<vector<int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
queue<pair<int, int>> q;
q.push({start[0], start[1]});
maze[start[0]][start[1]] = 1; // 标记为已访问
int steps = 0;
while (!q.empty()) {
int size = q.size();
while (size--) {
auto [x, y] = q.front(); q.pop();
if (x == end[0] && y == end[1]) return steps;
for (auto& d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx>=0 && nx<maze.size() && ny>=0 && ny<maze[0].size()
&& maze[nx][ny] == 0) {
maze[nx][ny] = 1;
q.push({nx, ny});
}
}
}
++steps;
}
return -1;
}
真题2:括号生成
DFS解法核心:
-
记录剩余左右括号数量
-
剪枝条件(右括号剩余数 < 左括号时无效)
void generate(vector<string>& res, string path, int left, int right) {
if (left == 0 && right == 0) {
res.push_back(path);
return;
}
if (left > 0) generate(res, path + "(", left-1, right);
if (right > left) generate(res, path + ")", left, right-1);
}
六、常见误区与优化技巧
-
DFS栈溢出:递归深度过大时改用显式栈或迭代加深
-
BFS状态爆炸:使用双向BFS或A*启发式搜索优化
-
重复访问判断:必须在入队/入栈时标记访问(而非出队时)
-
剪枝策略:根据问题特性提前终止无效分支(如超过当前最优解)
-
状态压缩:使用位运算减少内存消耗(如N皇后问题)
LeetCode真题练习:
-
200. 岛屿数量(DFS)
-
102. 二叉树的层序遍历(BFS)