C语言数据结构课程设计:迷宫生成与BFS寻路完整实现

发布时间:2026/10/8 9:01:15
C语言数据结构课程设计:迷宫生成与BFS寻路完整实现 简介适合高校学生及C语言初学者的C语言数据结构课程设计项目——老鼠走迷宫游戏升级版包含完整源代码与迷宫文件。核心玩法为用键盘方向键操控老鼠在规定时间内抵达右下角粮仓系统会正确判定成功或失败同时支持实时编辑迷宫可将墙变为路或将路变为墙并能够找出走出迷宫的所有路径以及最短路径覆盖了图遍历、深度优先/广度优先搜索、路径记录等数据结构关键知识点。压缩包约50KB共5个文件含1个可直接运行的exe、1个cpp源码文件以及3个txt文本文件其中txt可用于存放迷宫地图数据、地图说明或实验报告结构清晰方便对照学习。该资源已有3889人学习下载适合作为数据结构课程设计参考或迷宫类小游戏的入门实战范例尤其能帮助读者理解搜索算法在实际游戏逻辑中的落地方式也可借此学习如何设计交互式游戏界面。1. 老鼠走迷宫游戏C语言数据结构课程设计怎么才能拿得出手很多同学的课程设计上交形式是这样的控制台打印一张静态迷宫图再加上一个闪烁的光标就算“走迷宫”了。答辩老师一问“老鼠怎么自己找路”就答不上来。这份“老鼠走迷宫游戏升级版”不是那种货色它是一个把迷宫生成、自动寻路、关卡文件读取全部串起来的完整工程核心是 C 语言加数据结构里的栈、队列和二维数组。你下载下来能直接编译运行看到老鼠沿着路线自己走出来。适合正在做课程设计的学生也适合想复习数据结构的在职开发者拆开看内部实现。更重要的是这份代码的模块划分很清晰每个函数干一件事改起来不伤人。2. 迷宫地图怎么存二维数组选型与栈式深搜生成2.1 为什么是二维数组而不是链表或图迷宫的本质是一张网格地图每个格子要么是墙要么是路。二维数组在物理上就是一块连续内存访问maze[x][y]的时间复杂度是 O(1)这在寻路算法里是最高频的操作。链表也能表示图但要从某个节点跳到相邻节点得靠指针遍历代码复杂度上去了不说调试时看内存也不直观。图结构更重适合表达带有权重的复杂关系而迷宫的路就是路、墙就是墙没有权重可言。所以这份代码选了int maze[ROWS][COLS]值0表示通路1表示墙壁值本身也充当了可视化符号。用二维数组还有一个隐藏好处打印和渲染极其方便。控制台逐行输出二维数组迷宫就直接显示出来了。用链表你还要写一个遍历器才能画出来用数组直接两个for循环搞定。数组的行列就是迷宫的行列跟直觉对应后期如果要改成图形界面把数组转成像素矩阵也是顺手的事。2.2 生成算法为什么用随机深搜而不用普里姆迷宫生成的主流算法有递归回溯随机深搜、普里姆算法、克鲁斯卡尔算法。课程设计场景里递归回溯是最好讲清楚的一个它天然用到了栈而且生成的迷宫是完美迷宫任意两点之间只有一条通路视觉上非常规整。普里姆虽然也能生成迷宫但它更依赖集合数据结构在 C 语言里得自己写并查集或者布尔数组讲起来绕。这道题的做法是典型的“隔墙挖路”从起点格子出发每次随机选一个方向如果隔着墙壁的目标格子还没有被访问过就把中间的墙打通然后把当前格子压栈跳到目标格子继续。走到死胡同就弹栈回退。这个过程用数组模拟栈完全避免递归调用栈溢出的风险也方便你在答辩时直接展示栈顶指针的变化。方向数组dx/dy是这套逻辑的精髓四个方向的偏移量先定死后面寻路和生成共用一份方向数据。2.3 核心生成代码从零挖一个 21×21 的迷宫#include stdio.h #include stdlib.h #include time.h #define ROWS 21 // 迷宫行数必须是奇数保证有边界墙 #define COLS 21 // 迷宫列数必须是奇数 int maze[ROWS][COLS]; // 四个方向的偏移量右、下、左、上 int dirs[4][2] {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 栈结构用来保存回退路径 int stackX[ROWS * COLS]; int stackY[ROWS * COLS]; int top -1; void push(int x, int y) { top; stackX[top] x; stackY[top] y; } void pop() { if (top 0) top--; } // 判断坐标是否在迷宫有效范围内 int inBounds(int x, int y) { return x 0 x ROWS - 1 y 0 y COLS - 1; } // 随机深搜生成迷宫 void generateMaze(int startX, int startY) { // 初始化全部格子为墙 for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { maze[i][j] 1; } } srand((unsigned)time(NULL)); maze[startX][startY] 0; // 起点挖成路 push(startX, startY); while (top 0) { int cx stackX[top]; int cy stackY[top]; // 随机打乱方向避免生成结果偏向某个方向 for (int i 0; i 4; i) { int r rand() % 4; int tmpX dirs[r][0], tmpY dirs[r][1]; dirs[r][0] dirs[i][0]; dirs[r][1] dirs[i][1]; dirs[i][0] tmpX; dirs[i][1] tmpY; } int found 0; for (int i 0; i 4; i) { int nx cx dirs[i][0] * 2; // 跨一格目标是格子 int ny cy dirs[i][1] * 2; if (inBounds(nx, ny) maze[nx][ny] 1) { // 打通中间的墙 maze[cx dirs[i][0]][cy dirs[i][1]] 0; maze[nx][ny] 0; push(nx, ny); found 1; break; } } // 四个方向都走不通回退 if (!found) { pop(); } } }dirs数组是这套逻辑的地基生成和寻路都用它。注意生成时横纵坐标都乘以2这是为了隔一格挖一条路保证迷宫有完整的实体墙隔开。inBounds限制坐标在1到ROWS-2之间目的是让迷宫最外圈永远是墙体这样老鼠不会被走到地图外面。栈是数组模拟的top最初是-1压栈时先自加再赋值弹栈直接top--逻辑上不销毁数据只是丢弃访问权限这段代码在答辩时很值得讲——它体现了你懂队列和栈的差别。2.4 打印与可视化让迷宫直接显示在控制台生成完迷宫之后下一步是把它画出来。这份代码的做法是用循环逐行输出二维数组同时做符号映射墙输出#路输出空格起点和终点分别用S和E标记。控制台窗口默认宽度有限所以迷宫尺寸控制在 21×21 以内是合理的超过 40 列就会自动换行影响观看。void printMaze() { for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { if (i 1 j 1) { printf(S ); // 起点 } else if (i ROWS - 2 j COLS - 2) { printf(E ); // 终点 } else if (maze[i][j] 1) { printf(# ); // 墙 } else { printf( ); // 路 } } printf(\n); } }起点和终点是刚写死的坐标(1,1)和(ROWS-2, COLS-2)这在课程设计里完全够用。如果你想让入口出口可配置可以改成从外部传入参数或者直接读迷宫文件里的S和E字符。符号输出的逻辑和迷宫数据本身解耦了打印只是把数组翻译成字符不修改原始数据这样做的好处是寻路算法运行完再次调用printMaze()也不会被上一次的路径标记污染。3. 老鼠怎么走BFS最短路径与DFS回溯两条路线3.1 队列在迷宫寻路里扮演什么角色迷宫生成只是热身攻略重头戏在寻路。课程设计如果想拿高分必须让老鼠自己找路走到出口。寻路算法里最好讲、代码量也最可控的是广度优先搜索BFS它的核心数据结构是队列。BFS 的特点是逐层扩散老鼠从起点出发第一步能走到哪些格子第二步能在第一步基础上走到哪些新格子像水面波纹一样一层一层往外推。因为每一层都是离起点最近的未访问格子所以第一次到达终点时走过的路径一定是最短路径。这个特性和队列的先进先出严格对应每个格子出队时把它的上下左右四个邻居中未访问过的格子入队。队列保证先入队的格子先被扩展也就是层级严格的先来后到。二维数组配合一个结构体队列就能在当前迷宫上完整运行 BFS。用栈做 DFS 也能找到一条路径但不保证最短容易走出绕远路视觉上不好看答辩时也不好解释为什么老鼠不走直线。3.2 BFS寻路代码用队列记录每一步#define MAX_QUEUE ROWS * COLS // 队列节点保存坐标和步数 typedef struct { int x, y; int step; } Node; Node queue[MAX_QUEUE]; int head 0, tail 0; // 记录每个格子的前驱坐标用于回溯整条路径 int prevX[ROWS][COLS]; int prevY[ROWS][COLS]; // 方向数组沿用迷宫生成的 dirs extern int dirs[4][2]; // BFS寻路返回最短步数无法到达返回-1 int bfs(int startX, int startY, int endX, int endY) { // 初始化前驱数组 for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { prevX[i][j] -1; prevY[i][j] -1; } } head 0; tail 0; queue[tail].x startX; queue[tail].y startY; queue[tail].step 0; tail; maze[startX][startY] 2; // 标记为已访问避免重复入队 prevX[startX][startY] startX; prevY[startX][startY] startY; while (head tail) { int cx queue[head].x; int cy queue[head].y; int step queue[head].step; head; // 到达终点直接返回步数 if (cx endX cy endY) { return step; } // 遍历四个方向 for (int i 0; i 4; i) { int nx cx dirs[i][0]; int ny cy dirs[i][1]; // 越界或撞墙或已访问则跳过 if (nx 0 || nx ROWS || ny 0 || ny COLS) continue; if (maze[nx][ny] ! 0) continue; // 新格子入队 maze[nx][ny] 2; prevX[nx][ny] cx; prevY[nx][ny] cy; queue[tail].x nx; queue[tail].y ny; queue[tail].step step 1; tail; } } return -1; // 走到队列为空也没到终点 }step字段记录当前格子的最短步数终点格子出队时的step就是整条路径的长度。prevX和prevY是前驱数组用来在找到终点后反推完整路径。注意这里直接把maze[nx][ny]改成了2这是为了省掉一个visited数组但副作用是原始迷宫的路被污染了后面如果要打印迷宫得先重置回0。很多网上代码不处理这一步打印路径时整张图花的没法看这个坑我在后面避坑章节里细说。3.3 路径回溯从终点走回起点再反转BFS 结束以后prev数组里存的是每个格子是从哪个格子走过来的。要从终点回退到起点用一个循环不停往回跳// 从终点回溯路径存入 pathX/pathY返回路径长度 int pathX[MAX_QUEUE], pathY[MAX_QUEUE]; int pathLen 0; void buildPath(int startX, int startY, int endX, int endY) { int cx endX, cy endY; pathLen 0; while (!(cx startX cy startY)) { pathX[pathLen] cx; pathY[pathLen] cy; pathLen; int px prevX[cx][cy]; int py prevY[cx][cy]; cx px; cy py; } pathX[pathLen] startX; pathY[pathLen] startY; pathLen; // 反转数组使路径从起点开始 for (int i 0; i pathLen / 2; i) { int tmpX pathX[i]; int tmpY pathY[i]; pathX[i] pathX[pathLen - 1 - i]; pathY[i] pathY[pathLen - 1 - i]; pathX[pathLen - 1 - i] tmpX; pathY[pathLen - 1 - i] tmpY; } }回溯是寻路里最容易出错的环节因为路径是反的不反转的话打印出来是从终点往起点走。反转数组用了一个标准的两两交换循环前半段和后半段对应位置交换奇偶长度都成立。prev数组初始化为-1还有一个额外作用如果 BFS 返回-1说明迷宫无解这时候buildPath不应该被调用否则会死循环。这份代码在buildPath里没有显式检查路径是否存在所以我在自己用的时候会在调用前加判断这是你要留意的一个边界。3.4 对比DFS和贪心课设答辩选哪个更好深度优先搜索用栈实现代码量更少但它是一条道走到黑路径可能绕弯。贪心算法每次选距离终点最近的格子走速度快但贪心会陷入局部最优碰到死胡同就卡住。BFS 在八方向或四方向迷宫上的视觉效果最直观——你能看到路径一层层铺开最终连到出口而且“最短路径”这个结论一句话就能解释清楚。如果课设要求展示数据结构BFS 的队列和 DFS 的递归栈都值得展示但 BFS 队列的先进先出特性和最短路径直接挂钩讲起来最有说服力。4. 迷宫文件怎么读关卡管理器和文件缓冲区的坑4.1 迷宫文件格式怎么设计这份资源附带迷宫文件这意味着迷宫不是每次随机生成而是可以预先设计。文件格式设计成最简形式每行一串0和10是通路1是墙S是起点E是终点文件后缀名用.txt或者.dat都可以。加载的时候按行读取逐字符判断同时把坐标记录下来。格式不需要复杂课程设计的迷宫文件一般一屏能放下太复杂的自定义格式反而增加解析工作量。// maze1.txt 示例内容 // 111111111111111111111 // 1S000100000001000001 // 101110100101111011101 // 101001000100000010001 // 101011011111111010111 // 100010000000001010001 // 111110111110101011101 // 100000001010001000001 // 101111111010111011111 // 100001000000001000001 // 111101111111111101111 // 100001000000001000001 // 101110111110101111101 // 100000000000100000001 // 1111111111111111111114.2 文件加载代码从磁盘到内存#include stdio.h #include string.h #define MAX_LINE 256 // 从文件加载迷宫返回1成功0失败 int loadMazeFromFile(const char *filename) { FILE *fp fopen(filename, r); if (fp NULL) { perror(无法打开迷宫文件); return 0; } char line[MAX_LINE]; int row 0; while (fgets(line, sizeof(line), fp) ! NULL) { // 去掉末尾换行符 line[strcspn(line, \n)] \0; int len strlen(line); if (len 0) continue; // 跳过空行 for (int col 0; col len col COLS; col) { char ch line[col]; if (ch S) { maze[row][col] 0; startX row; startY col; } else if (ch E) { maze[row][col] 0; endX row; endY col; } else if (ch 1) { maze[row][col] 1; } else if (ch 0) { maze[row][col] 0; } // 其他字符忽略等待下次循环 } row; } fclose(fp); return 1; }fgets按行读取每行最长MAX_LINE个字符自动截断过长的行不会造成缓冲区溢出。strcspn(line, \n)是去掉行尾换行符的常用技巧比直接判断line[strlen(line)-1]安全得多因为最后一行不一定有换行符。row变量必须在上一个迷宫数据加载前初始化为0否则重复调用loadMazeFromFile会从上次的行号继续写直接破坏数组。4.3 文件缓冲区的经典问题文件加载有一个非常容易翻车的点如果迷宫文件是 Windows 下编辑的行尾是\r\nfgets读出来的行尾是多一个\r的。判断if (ch 1)还能过但如果最后数据是0之后跟一个\r你逐字符扫描数组就多出一个非法字符。主流做法是读完一行先做清洗把\r和\n都去掉// 清除行尾的 \r 和 \n line[strcspn(line, \r\n)] \0;另一个常见问题是文件缓冲区没有刷新。如果你在写迷宫文件时格式错了比如行列数不匹配加载结果就会出现错位。排查的时候先printf打印文件的总行数和每行长度对比ROWS和COLS宏定义基本上一次就能定位。这个排查思路比对着代码发呆快得多。4.4 关卡切换逻辑有了loadMazeFromFile关卡切换就变成换个文件名再次调用。官方资源里有多个迷宫文件主程序里通常用一个level变量控制switch (level) { case 1: loadMazeFromFile(maze1.txt); break; case 2: loadMazeFromFile(maze2.txt); break; default: printf(没有更多关卡了\n); break; }注意加载完新迷宫后BFS 寻路用的prevX、prevY数组必须重新初始化不然上一关的路径会残留在新地图上。这一点最容易忽略很多人在第二关看到的老鼠路径和墙重叠其实就是前驱数组没清。5. 避坑指南这份迷宫代码里最容易翻车的五个位置5.1 数组越界坐标加偏移后直接访问现象运行到一半程序崩溃或者输出乱码。原因寻路和生成时坐标加上dirs偏移后没有检查是否越界就访问maze[nx][ny]。虽然inBounds在生成时做了限制但 BFS 寻路里的越界检查写的是nx 0 || nx ROWS这类完整判断两处逻辑不一致一旦改动数组尺寸就可能漏掉边界。解决统一用一个isValid(x, y)函数内部同时判断x 0 x ROWS - 1 y 0 y COLS - 1生成和寻路都调它。不要在两处各写一套判断标准。5.2 打印路径时迷宫被污染现象BFS 跑完调用printMaze()输出迷宫里多了一堆数字2整个画面花了。原因BFS 里用maze[nx][ny] 2标记已访问确实省了一个visited数组但打印函数只处理墙和路没处理值为2的格子。更关键的是下一次运行 BFS 时原来的2不是0导致新的搜索直接跳过这些本应是路的格子。解决在printMaze判断里把2也当作路面处理同时在每次bfs调用开始时扫描整张地图把所有值等于2的格子重置为0。或者干脆把visited单独开一个二维数组不改动原始迷宫数据。5.3 fscanf读取后残留换行符现象用fscanf(fp, %d, maze[i][j])读取数字迷宫每次读到的数据都比预期错位一行。原因fscanf跳过空白字符但会留下换行符数字迷宫每行末尾的\n被下一次fscanf当作分隔符号处理但正好%d遇到换行也会跳过一般不会出错。真正出错的是混用了fscanf和fgetcfgetc读取到了残留的换行符把它当成墙或路直接写进迷宫。解决统一用fgets读整行再自己解析每个字符。这就是这份代码里loadMazeFromFile的做法。自己解析虽然多写几行代码但完全可控不会出现不明不白的换行残留。5.4 栈溢出与死循环现象迷宫尺寸改大到101×101程序运行时栈空间被瞬间用完或者生成迷宫时卡住不动。原因深搜生成迷宫时如果用递归函数递归深度等于迷宫格子数超过编译器默认栈大小就被打断。用数组模拟栈虽避免了递归栈但stackX、stackY数组大小写死成了ROWS * COLS如果ROWS、COLS被改大但数组大小没跟着改数组也会越界。解决把栈数组大小定为ROWS * COLS 1或者动态分配。另外生成迷宫时每个格子最多被访问一次用一个visited标记能防止重复入栈导致的死循环。如果你发现生成时间异常长多半是随机打乱方向的代码写成了固定顺序导致每次都优先走某个方向生成路径很长但最终能通看起来像死循环。5.5 历史路径残留导致寻路越来越慢现象同一回合内连续按两次空格第二次寻路明显卡顿。原因第一次 BFS 把地图上的格子标记成了2第二次 BFS 从起点开始扩散时几乎所有相邻格子都是2被当成已访问跳过。队列里的有效搜索范围只剩起点一格搜索很快结束但看起来老鼠不动了。如果迷宫中大量0被改成2还会直接污染文件加载的迷宫数据。解决每次调用bfs前写一个resetMazeState()函数把全图所有等于2的格子恢复成0。这个函数在课程设计里两行就能写完但它避免了八成以上的寻路逻辑翻车事故。从那以后我每次写迷宫代码都会强制走一遍“加载 → 重置状态 → 生成/寻路 → 打印”的流程确认每一步都不依赖上一步留下的副作用。6. 升级版还能加什么隐藏光标与实时行走动画打游戏不只为了通关老鼠怎么走过去才是最有观赏性的部分。原生 C 语言的printf每次输出一整片网格刷新率差还闪屏。升级版的做法是先把光标隐藏然后用 ANSI 转义序列控制光标位置每次只重绘老鼠当前所在的那一格既流畅又省资源。#include stdio.h // 隐藏控制台光标 void hideCursor() { printf(\033[?25l); } // 移动光标到指定行列 void gotoxy(int x, int y) { printf(\033[%d;%dH, y, x); } // 在指定坐标画一个老鼠符号 void drawMouse(int x, int y) { gotoxy(y * 2, x); // 每行两个字符宽度y乘2对齐 printf(鼠); }\033[?25l是 ANSI 转义序列让控制台光标消失。gotoxy里的%d;%dH表示把光标移到第y行第x列。注意行的坐标是y、列是x和二维数组的maze[x][y]正好是反的写的时候一恍惚就翻车。我通常先定义一个mapToScreen函数做坐标转换把迷宫数组的下标都转换到屏幕坐标再调用gotoxy这样寻路算法返回的路径坐标就能直接用了。动画主循环的做法是BFS 求完最短路径后遍历pathX和pathY数组每走一格用gotoxy清除当前格、在下一格画老鼠然后Sleep(100)暂停 100 毫秒。这个循环你要注意pathX从 0 到pathLen-1循环顺序千万不要反了否则老鼠倒着走答辩老师一眼就能看出来逻辑有问题。我自己的经验是在控制台做动画前先跑一遍纯文本路径输出确认路径坐标数组的顺序和数量都对再套光标控制代码。这样能把寻路逻辑和渲染逻辑分开调试哪一部分出问题一眼就能定位。希望这份笔记能把你在课程设计答辩前最慌的那段路缩短一点真正把它变成你顺手能改能讲的代码。本文还有配套的精品资源点击获取