C/C++ 数据结构(九)深搜广搜

本篇 核心知识 :迷宫寻路、深度优先搜索 (DFS)、广度优先搜索 (BFS)、栈与队列在搜索中的应用、路径记录、迷宫场景拓展(拾取所有金币)、算法时空复杂度对比


一、迷宫寻路算法总述

概念

迷宫寻路是图 / 网格结构下的经典搜索问题,将迷宫抽象为二维网格 :网格节点代表位置,相邻格子代表可通行路径。主流实现为深度优先搜索 (DFS)广度优先搜索 (BFS),也是面试、游戏开发高频考点。

特性
  1. 迷宫常用二维数组表示,约定标识:0=墙壁1=可通行区域2=已走过路径3=入口4=出口

  2. 移动方向默认四个:上、下、左、右

  3. 两类算法底层分别依托栈 (DFS)、** 队列 (BFS)** 实现;

  4. 选型依据:结合时间复杂度、空间复杂度、是否需要最优路径综合判断。

拓展

游戏地图导航、机器人路径规划、网格类 AI 行为均基于这两类搜索算法延伸,进阶有 A * 启发式寻路算法。


二、深度优先搜索 DFS(深度优先)

概念

遇到分支时优先沿着单一方向一直走 ,走到无路可走后,回溯到上一个分支点,选择其他方向继续探索,俗称 "一条路走到黑"。底层依托栈 (Stack) 实现路径回溯。

核心特性

  1. 执行逻辑:单路径优先,回溯探索其他分支;

  2. 时空特点:时间效率高,找到目标即可终止;但最坏情况会遍历全部区域,空间开销取决于递归 / 栈的深度;

  3. 路径特点:不一定能得到最短最优路径

  4. 适用场景:仅要求找到出口、遍历所有可达区域(拾取金币)。

关键规则

  1. 移动前必须做数组越界判断(行、列下标不能小于 0、不能超出网格范围);

  2. 走过的格子标记为2,避免重复行走、死循环;

  3. 检测到出口4则终止搜索。

代码示例(DFS 迷宫行走演示,栈实现)

复制代码
#include <iostream>
#include <stack>
#include <Windows.h>
using namespace std;
​
// 迷宫网格
const int ROW = 10;
const int COL = 10;
int maze[ROW][COL] = {
    {0,0,0,0,0,0,0,0,0,0},
    {0,1,1,0,1,1,1,0,1,0},
    {0,3,1,0,1,0,1,0,1,0}, // 3 入口
    {0,1,1,1,1,0,1,1,1,0},
    {0,0,0,0,1,0,0,0,1,0},
    {0,1,1,1,1,1,1,0,1,0},
    {0,1,0,0,0,0,1,0,1,0},
    {0,1,1,1,1,1,1,1,1,0},
    {0,1,0,0,0,0,0,0,4,0}, // 4 出口
    {0,0,0,0,0,0,0,0,0,0}
};
​
// 坐标结构体,存储行、列
struct Pos{
    int x; // 行
    int y; // 列
    Pos(int x_, int y_) : x(x_), y(y_) {}
};
​
// 四个移动方向:上、下、左、右
int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
​
void DFS_Show(){
    stack<Pos> st;
    // 找到入口坐标
    int startX = 0, startY = 0;
    for(int i = 0; i < ROW; i++)
        for(int j = 0; j < COL; j++)
            if(maze[i][j] == 3)
            {
                startX = i;
                startY = j;
                break;
            }
    st.push(Pos(startX, startY));
​
    while(!st.empty()){
        Pos cur = st.top();
        st.pop();
​
        // 到达出口,结束搜索
        if(maze[cur.x][cur.y] == 4){
            cout << "已到达出口!" << endl;
            break;
        }
        // 标记为已走过
        maze[cur.x][cur.y] = 2;
​
        // 遍历四个方向
        for(int d = 0; d < 4; d++){
            int nx = cur.x + dir[d][0];
            int ny = cur.y + dir[d][1];
            // 越界判断 + 可通行判断
            if(nx >= 0 && nx < ROW && ny >=0 && ny < COL && maze[nx][ny] == 1){
                st.push(Pos(nx, ny));
            }
        }
​
        // 刷新迷宫视图,模拟行走
        system("cls");
        for(int i = 0; i < ROW; i++){
            for(int j = 0; j < COL; j++){
                if(maze[i][j] == 0) cout << "墙";    // 墙壁
                else if(maze[i][j] == 1) cout << "  "; // 通路
                else if(maze[i][j] == 2) cout << "路"; // 已走
                else if(maze[i][j] == 3) cout << "入 "; // 入口
                else if(maze[i][j] == 4) cout << "出 "; // 出口
            }
            cout << endl;
        }
        Sleep(200); // 延时200ms
    }
}
​
int main(){
    DFS_Show();
    return 0;
}

拓展

DFS 也可使用递归 实现,递归本质就是函数调用栈;但迷宫规模极大时,递归深度过大会造成栈溢出,工程中优先使用显式栈实现。


三、广度优先搜索 BFS(广度优先)

概念

遇到分支时分层同步探索 ,将当前位置所有相邻可走节点全部纳入搜索范围,逐层向外扩散,如同水流漫延。底层依托队列 (Queue) 实现。

核心特性

  1. 执行逻辑:一层一层遍历,同级分支同步探索;

  2. 时空特点:遍历层数多,时间开销更大;但每层节点数量可控;

  3. 路径特点:一定能找到最短 / 最优路径(面试、项目求最优路径首选);

  4. 适用场景:要求最短路径、最短步数的寻路问题。

代码示例(BFS 迷宫行走演示)

复制代码
#include <iostream>
#include <queue>
#include <Windows.h>
using namespace std;
​
const int ROW = 10;
const int COL = 10;
int maze[ROW][COL] = {
    {0,0,0,0,0,0,0,0,0,0},
    {0,1,1,0,1,1,1,0,1,0},
    {0,3,1,0,1,0,1,0,1,0},
    {0,1,1,1,1,0,1,1,1,0},
    {0,0,0,0,1,0,0,0,1,0},
    {0,1,1,1,1,1,1,0,1,0},
    {0,1,0,0,0,0,1,0,1,0},
    {0,1,1,1,1,1,1,1,1,0},
    {0,1,0,0,0,0,0,0,4,0},
    {0,0,0,0,0,0,0,0,0,0}
};
​
struct Pos{
    int x, y;
    Pos(int x_, int y_) : x(x_), y(y_) {}
};
int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
​
void BFS_Show(){
    queue<Pos> q;
    int startX = 0, startY = 0;
    for(int i = 0; i < ROW; i++)
        for(int j = 0; j < COL; j++)
            if(maze[i][j] == 3){
                startX = i;
                startY = j;
                break;
            }
    q.push(Pos(startX, startY));
​
    while(!q.empty()){
        Pos cur = q.front();
        q.pop();
​
        if(maze[cur.x][cur.y] == 4){
            cout << "已到达出口!" << endl;
            break;
        }
        maze[cur.x][cur.y] = 2;
​
        // 四个方向入队
        for(int d = 0; d < 4; d++){
            int nx = cur.x + dir[d][0];
            int ny = cur.y + dir[d][1];
            if(nx >= 0 && nx < ROW && ny >=0 && ny < COL && maze[nx][ny] == 1){
                q.push(Pos(nx, ny));
            }
        }
​
        system("cls");
        for(int i = 0; i < ROW; i++){
            for(int j = 0; j < COL; j++){
                if(maze[i][j] == 0) cout << "墙";
                else if(maze[i][j] == 1) cout << "  ";
                else if(maze[i][j] == 2) cout << "路";
                else if(maze[i][j] == 3) cout << "入 ";
                else if(maze[i][j] == 4) cout << "出 ";
            }
            cout << endl;
        }
        Sleep(200);
    }
}
​
int main(){
    BFS_Show();
    return 0;
}

拓展

BFS 天然适配最短路径问题,是地图导航、游戏自动寻路的基础方案。


四、DFS 与 BFS 核心对比

对比维度 深度优先 DFS 广度优先 BFS
底层容器 栈(Stack) 队列(Queue)
搜索逻辑 单方向走到底,再回溯 分层扩散,同级同步遍历
最优路径 无法保证最短路径 一定得到最短路径
时间效率 较高,找到目标即终止 相对较低,分层遍历
空间开销 依赖栈深度,极端情况较大 依赖每层节点数
典型场景 遍历全区域、只求可达 求最短路径、最短步数

五、路径记录功能实现

概念

基础寻路仅模拟行走过程,实际项目需要输出完整行走坐标序列。主流两种实现方案。

方案 1:记录父节点(推荐)

概念

额外创建和迷宫同大小的二维数组,存储每个位置的前驱坐标(从哪一个格子走过来)。抵达出口后,从出口反向回溯到入口,拼接出完整路径。

特性
  1. 每个节点的父节点唯一,回溯逻辑简单;

  2. 空间换时间,查询路径效率极高;

  3. DFS/BFS 均可搭配使用。

代码片段(父节点定义)
复制代码
// 父节点数组,记录每个位置的来源坐标
Pos pre[ROW][COL];
// 初始化所有父节点为空
for(int i = 0; i < ROW; i++)
    for(int j = 0; j < COL; j++)
        pre[i] = Pos(-1,-1);
​
// 入队/入栈时,记录前驱
pre[nx] = cur;

方案 2:路径容器回溯

概念

使用vector动态数组实时存储每一步坐标,遇到死分支时删除无效路径。

特性
  1. 无需额外父节点数组;

  2. 分支较多时,频繁增删元素,时间开销更大


六、迷宫拓展场景:拾取所有金币

概念

迷宫无固定入口、出口,目标是遍历所有可通行区域、拾取全部金币,直到所有路径走完。

特性
  1. 终止条件:栈 / 队列为空(所有可走区域全部遍历完毕);

  2. 逻辑改造:删除 "到达出口终止" 的判断,仅保留越界、重复行走判断;

  3. 本质:对整个连通区域做全遍历

代码核心改造点

复制代码
// 原出口判断代码直接移除
// 循环终止条件:容器为空
while(!st.empty())
{
    Pos cur = st.top();
    st.pop();
    maze[cur.x][cur.y] = 2; // 标记已遍历
    // 遍历四个方向...
}
cout << "所有金币拾取完成!" << endl;

七、补充知识点与拓展

1. 方向数组优化

概念

将上下左右四个偏移量统一存入二维数组dir[4][2],替代重复的方向判断代码,代码更简洁、易维护。

复制代码
// 上(-1,0) 下(1,0) 左(0,-1) 右(0,1)
int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};

2. 易出错三大情况

  1. 数组越界:操作数组时,下标必须校验范围下标 >= 0 && 下标 < 数组长度,越界会导致程序崩溃、内存错乱。

  2. 数据溢出:每种整型(char / short / int / long)都有固定取值范围 ,当赋值 / 计算结果超出该类型可表示的最大 / 最小值时,就发生数据溢出

  3. 整形提升:当表达式中出现 不同长度整型混合运算 、或 char/short 参与运算时:编译器会自动把短整型(char、short)提升为 int ,再进行计算,这个过程就叫整型提升

3. 进阶算法 A* 寻路

概念

启发式搜索算法,在 BFS 基础上增加估价函数,优先朝着目标方向搜索,大幅减少无效遍历。

特性
  1. 结合当前位置、目标位置计算预估代价;

  2. 效率远高于普通 BFS,是商业游戏、导航软件主流寻路方案;

  3. 后续课程重点讲解。

4. 游戏应用延伸

  1. 2D 游戏:基于网格 + 四方向 / 八方向移动,搭配寻路算法实现 NPC 巡逻、玩家自动行走;

  2. 3D 游戏:基于向量计算位置、角度,结合寻路 + 骨骼动画实现角色转向、移动;

  3. 通用思路:算法负责计算路径,引擎负责渲染动画与画面。

5. 容器选型总结

深度遍历、回溯场景 → 优先使用

分层遍历、求最短路径 → 优先使用 队列

存储不定长路径序列 → 优先使用 vector