简介这份C数据结构课设资源以“老鼠走迷宫”为实战案例面向正在学习数据结构与算法、需要完成课程设计或想通过小项目巩固编程能力的学生。项目将迷宫抽象为二维数组与图结构围绕深度优先搜索实现随机迷宫生成、路径探索与回溯并配有可替换的视觉素材帮助理解搜索算法与数据结构设计。压缩包共27个文件约148.59MB包含cpp与h源码、sln与vcxproj工程文件、exe可执行程序、mp4演示与使用说明视频、png/jpg素材及txt说明文档覆盖从源码到运行演示的完整链路。目前已有1390人学习下载。读者可获得可直接编译运行的完整课设方案、随机迷宫生成与DFS搜索的实现思路、素材替换与自定义迷宫参数的参考以及调试排错所需的工程配置与日志文件适合作为课程设计参考或算法入门练习。1. 从一份迷宫课设说起为什么“老鼠走迷宫”是数据结构最值得手撕的案例如果你正在搜“数据结构课设 老鼠走迷宫”大概率是两种情况要么课设题目已经发下来要求用 C 实现一个能自动寻路的迷宫程序要么你手里已经拿到了一份现成源码但打开一看全是模板和指针不知道从哪改起。这个资源就是围绕“老鼠走迷宫”这个经典课设做的完整实现核心用 C 写涉及栈、队列、递归回溯、图的遍历这几块数据结构主干知识。它解决的问题很具体给定一个二维迷宫矩阵让程序从入口走到出口并且把路径打印出来。适合两类人——刚学完栈和队列、需要拿一个能跑的项目交作业的学生以及想用一个小案例把 DFS/BFS 讲清楚的教学者。我见过太多人把课设做成“能跑就行”结果答辩时被问一句“为什么用栈不用队列”就卡住。这份东西的价值不在于代码多长而在于它把两种寻路策略放在同一个迷宫上跑差异一眼就能看出来。2. 迷宫的数据表示与两种寻路策略栈式回溯和队列式广搜到底怎么选2.1 迷宫矩阵的存储结构与坐标约定拿到任何迷宫题第一步不是写寻路而是把迷宫存成程序能操作的结构。常见做法是用一个二维整型数组maze[N][M]其中 0 表示通路1 表示墙。入口固定在(1,1)出口固定在(N-2,M-2)外围加一圈墙防止越界。这个约定不是随便定的它直接决定了后面边界判断写不写if嵌套。// 迷宫定义0 通路1 墙外围一圈墙 const int N 10; const int M 10; int maze[N][M] { {1,1,1,1,1,1,1,1,1,1}, {1,0,0,1,0,0,0,1,0,1}, {1,0,1,0,0,1,0,1,0,1}, {1,0,0,0,1,1,0,0,0,1}, {1,1,0,1,0,0,0,1,0,1}, {1,0,0,0,0,1,0,0,0,1}, {1,0,1,1,0,0,1,0,1,1}, {1,0,0,0,0,1,0,0,0,1}, {1,1,0,1,0,0,0,1,0,1}, {1,1,1,1,1,1,1,1,1,1} };这段数组就是整个程序的“地图”。参数说明N和M控制迷宫规模改大之后寻路时间会明显上升尤其是 DFS 在死胡同多的时候。外围一圈 1 是血泪经验——不加的话每次判断上下左右都要检查x-1 0代码里全是条件分支容易漏。加了墙之后只需要判断下一个格子是不是 0 就行。2.2 栈式 DFS用回溯找一条路径栈式寻路的思路是从入口出发每走一步就把当前位置压栈然后按“上右下左”的顺序试探下一个格子。如果四个方向都走不通就弹栈回到上一步换个方向继续试。这就是经典的回溯法用std::stack或者自己写一个数组栈都行。#include stack #include vector using namespace std; struct Pos { int x, y; }; bool dfsStack(int maze[N][M], Pos start, Pos end, vectorPos path) { stackPos st; bool visited[N][M] {false}; // 记录每个位置的前驱用于最后还原路径 Pos parent[N][M]; st.push(start); visited[start.x][start.y] true; int dx[4] {-1, 0, 1, 0}; // 上右下左 int dy[4] {0, 1, 0, -1}; while (!st.empty()) { Pos cur st.top(); if (cur.x end.x cur.y end.y) { // 从终点回溯到起点还原路径 Pos p cur; while (!(p.x start.x p.y start.y)) { path.push_back(p); p parent[p.x][p.y]; } path.push_back(start); reverse(path.begin(), path.end()); return true; } bool moved false; for (int i 0; i 4; i) { int nx cur.x dx[i]; int ny cur.y dy[i]; if (maze[nx][ny] 0 !visited[nx][ny]) { visited[nx][ny] true; parent[nx][ny] cur; st.push({nx, ny}); moved true; break; // 找到一个方向就深入不继续试其他方向 } } if (!moved) st.pop(); // 四个方向都不通回溯 } return false; }逻辑说明visited数组防止走回头路parent记录每个格子的来源找到终点后从终点倒推回起点再翻转得到的就是从入口到出口的完整路径。参数上方向数组dx/dy的顺序会影响找到的路径形状——先试上再试右路径会偏向左上绕。如果想让路径更“贴边”可以调整这个顺序。注意break那行找到一个可走方向就立刻深入这是 DFS 的典型写法不 break 就变成把四个方向全压栈路径会变得很怪。2.3 队列式 BFS找最短路径BFS 和 DFS 最大的区别在于BFS 用队列每次把当前格子所有能走的邻居都入队一层一层往外扩。这样第一次到达终点时走过的步数一定是最少的。如果课设要求“最短路径”DFS 交上去就是翻车现场。#include queue bool bfsQueue(int maze[N][M], Pos start, Pos end, vectorPos path) { queuePos q; bool visited[N][M] {false}; Pos parent[N][M]; q.push(start); visited[start.x][start.y] true; int dx[4] {-1, 0, 1, 0}; int dy[4] {0, 1, 0, -1}; while (!q.empty()) { Pos cur q.front(); q.pop(); if (cur.x end.x cur.y end.y) { Pos p cur; while (!(p.x start.x p.y start.y)) { path.push_back(p); p parent[p.x][p.y]; } path.push_back(start); reverse(path.begin(), path.end()); return true; } for (int i 0; i 4; i) { int nx cur.x dx[i]; int ny cur.y dy[i]; if (maze[nx][ny] 0 !visited[nx][ny]) { visited[nx][ny] true; parent[nx][ny] cur; q.push({nx, ny}); } } } return false; }这段和 DFS 的骨架几乎一样区别在容器从stack换成queue以及找到邻居后不 break而是全部入队。参数说明BFS 的visited必须在入队时就标记不能等出队再标否则同一个格子会被多个邻居重复入队队列爆炸。这是新手最容易踩的坑之一。2.4 两种策略的对比与选型依据对比项栈式 DFS队列式 BFS数据结构stackqueue路径性质任意可行路径最短路径空间开销与路径长度相关与层数相关最坏更大死胡同表现回溯多但内存稳不会回溯但队列可能很长课设适用要求“找一条路”要求“最短路径”选型建议很直接看题目要求。如果只说“走出迷宫”DFS 代码更短如果写了“最短”必须 BFS。我一般会两个都实现答辩时把两条路径打印出来对比老师一眼就能看出你理解了两者的本质差异。3. 从源码到可运行程序编译、输入输出与路径可视化3.1 工程文件组织与编译命令一份能交的课设不能只有一个main.cpp。常见做法是拆成三个文件maze.h放迷宫数据和结构体声明solver.cpp放 DFS/BFS 实现main.cpp负责读入和打印。这样答辩时老师翻代码也舒服。# 编译命令g 直接多文件编译 g -stdc11 -O2 main.cpp solver.cpp -o maze # 运行 ./maze参数说明-stdc11是因为用到了struct初始化列表和auto之类的特性-O2在迷宫规模到 100x100 以上时能明显减少 BFS 的运行时间。如果用的是旧版编译器把-stdc11去掉可能会报错这是常见环境问题。3.2 迷宫输入格式与边界处理硬编码迷宫只能演示真正交课设一般要求从文件读。常见格式是第一行两个整数 N M接下来 N 行每行 M 个字符#表示墙.表示路。#include fstream #include iostream bool loadMaze(const string filename, vectorvectorchar grid, int n, int m) { ifstream fin(filename); if (!fin) return false; fin n m; grid.assign(n, vectorchar(m)); for (int i 0; i n; i) for (int j 0; j m; j) fin grid[i][j]; return true; }逻辑说明用vectorvectorchar而不是固定数组是为了支持任意尺寸迷宫。fin grid[i][j]会自动跳过空白符所以#和.之间有没有空格都能读。注意如果文件里迷宫行末尾有换行符会跳过不会读进grid这是 C 流输入的特性不用额外处理。3.3 路径打印与可视化输出找到路径后最直观的展示方式是把路径上的格子标成*再打印整个迷宫。void printMazeWithPath(vectorvectorchar grid, const vectorPos path) { vectorvectorchar out grid; for (auto p : path) { if (out[p.x][p.y] .) out[p.x][p.y] *; } for (int i 0; i out.size(); i) { for (int j 0; j out[i].size(); j) cout out[i][j]; cout endl; } }参数说明path是从起点到终点的有序坐标序列遍历时只把.改成*不覆盖起点和终点标记。如果迷宫用 0/1 存储把.换成0即可。输出时每行末尾不加空格方便直接重定向到文件对比。4. 避坑与排查课设答辩前必须自己先跑一遍的五个问题4.1 现象程序能跑但路径穿墙原因边界判断写成了maze[nx][ny] ! 1而迷宫外围没有加墙导致nx或ny越界后读到了数组外的内存恰好不是 1 就走进去了。解决要么外围加一圈 1要么在判断前加nx 0 nx N ny 0 ny M。我一般直接加墙省事。4.2 现象BFS 找到的路径不是最短原因visited标记时机错了在出队时才标记导致同一个格子被多次入队先入队的路径被后入队的覆盖。解决入队时立刻标记visited[nx][ny] true出队时不再判断。这个坑在课设里出现频率极高答辩时被问到“你怎么保证最短”就露馅了。4.3 现象DFS 递归版本栈溢出原因迷宫规模大且死胡同多递归深度超过默认栈大小。解决改成显式栈的迭代版本或者把迷宫规模控制在 50x50 以内。如果题目强制递归可以在编译时加-Wl,-stack_size调大栈但更稳妥的是迭代。4.4 现象路径打印出来是反的原因从终点用parent回溯到起点后path里存的是终点到起点的顺序忘了reverse。解决在返回前加reverse(path.begin(), path.end())。这个错误不会导致程序崩溃但输出一看就错属于“玄学”级别的低级失误。4.5 现象换一个迷宫就找不到路径原因入口或出口坐标写死了新迷宫的起点不在(1,1)。解决把起点和终点作为参数传入或者在读入迷宫后扫描S和E标记的位置。常见做法是约定S为入口、E为出口读入时记录坐标。5. 进阶技巧把迷宫寻路改成可交互演示与性能对比5.1 用命令行参数切换 DFS/BFS交课设时如果能让老师自己选算法印象分会高不少。加一个简单的参数解析就行。int main(int argc, char* argv[]) { string mode bfs; // 默认 BFS if (argc 1) mode argv[1]; vectorPos path; bool found false; if (mode dfs) found dfsStack(maze, {1,1}, {N-2,M-2}, path); else found bfsQueue(maze, {1,1}, {N-2,M-2}, path); if (found) printMazeWithPath(grid, path); else cout No path found. endl; return 0; }参数说明argc是参数个数argv[1]是第一个参数。运行./maze dfs走 DFS./maze或./maze bfs走 BFS。这样演示时不用重新编译切换成本低。5.2 统计步数与访问节点数光打印路径还不够把两种算法的访问节点数和路径长度打出来对比更直观。指标DFS 示例值BFS 示例值路径长度1814访问节点数4763是否最短否是在代码里加一个计数器每次出队或弹栈时count最后输出。这个数据在答辩时非常有用能直接说明“BFS 访问节点多但路径短DFS 访问少但路径绕”。5.3 一个我常犯的错误早期写这个课设时我为了省事把visited和parent都开成全局数组结果测试多个迷宫时忘了重置第二个迷宫直接输出第一个的结果。从那以后我每次写寻路都强制把状态封装进函数局部变量或者显式memset清零。这个习惯后来在做更复杂的图算法时救了我很多次。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站