C语言老鼠走迷宫课程设计:栈与回溯算法源码解析
简介这是一个面向C语言与数据结构课程设计场景的老鼠走迷宫游戏升级版方案定位于帮助计算机专业学生完成迷宫类项目的设计与实现。程序运行后展示迷宫地图玩家可用键盘方向键操控老鼠在限定时间内抵达粮仓并具备结果判定、编辑迷宫墙变路、路变墙以及找出全部路径和最短路径等核心功能。资源包共5个文件以C源文件、可直接运行的exe程序和3个迷宫文本数据文件构成压缩后仅50KB文件结构紧凑便于对照源码与迷宫数据进行学习。该资源已有3889人学习下载适合作为数据结构课程设计、迷宫算法练习或C语言项目实战的参考。通过学习可掌握迷宫建模、深度/广度遍历搜索、最短路径求解以及交互式地图编辑等关键技能直接复用或扩展为完整课程设计成果。1. 老鼠走迷宫游戏课程设计为什么值得把这份C语言源码留下来很多人在期末翻车不是卡在“找一条路”而是卡在“换了个迷宫文件就完全跑不动”。这份老鼠走迷宫游戏升级版课程设计正好是那种能直接跑、能换地图、能讲清楚栈和回溯的C语言数据结构项目。它用二维数组模拟迷宫用栈实现深度优先搜索每一步都有对应的代码和注释适合大二做课程设计的学生也适合想把栈、文件操作、二维数组一次性串起来的C语言自学者。下载后拿到的不只是一段源码还有迷宫文件和一个可以对着讲满十分钟答辩的完整逻辑链路。2. 源码结构和运行入口从下载到跑通只花三分钟拿到压缩包先别急着双击打开 main.c这种课程设计源码包通常已经按“迷宫模块 栈模块 主流程”拆好了文件。先花三分钟看懂文件安排后面改代码时就不会迷路。2.1 文件清单maze.h、stack.h、main.c 各自负责什么一份能通过课程设计答辩的C语言迷宫项目至少要包含以下文件main.c入口文件负责接收迷宫文件路径调用加载函数执行寻路最后打印结果。maze.h/maze.c定义迷宫二维数组、行列数、文件读取函数、坐标定位函数。stack.h/stack.c用数组模拟栈提供push、pop、isEmpty操作。maze1.txt、maze2.txt两个迷宫地图文件通常一个是简单迷宫一个是带环路的地图。这个分层方式对课程设计特别有利。答辩时老师问“你的栈在哪实现”你直接把stack.c打开就行而不是在一千行里翻找。如果你打算加链表或者队列也只需要改stack.h对应的接口main.c 不用动。这里有个选型细节很多同学喜欢直接调系统栈写递归版的 DFS代码确实短但课程设计通常要求体现“数据结构”的知识点老师更希望看到显式的栈结构。显式栈还有个好处就是可以用打印栈顶坐标的方式实时观察回溯过程现场演示比口头解释有说服力。2.2 编译与运行gcc 一条命令跑通假设所有文件都解压到了同一个目录打开终端或命令行窗口进入该目录后执行gcc -stdc11 -Wall main.c maze.c stack.c -o maze_game ./maze_game maze1.txt-stdc11指定使用 C11 标准-Wall可以显示所有警告这个参数强烈建议保留。我第一次读这个资源时就是靠-Wall发现一个函数里定义了变量但没用编译器报告后才避免答辩时被问“这段代码里那个变量是干啥的”。编译生成了maze_game可执行文件运行时把迷宫文件名作为参数传进去。如果你用的是 Dev-C 或者 Visual Studio就别用命令行编译了直接建一个空项目把三个.c文件和两个头文件拖进工程在运行时参数里填maze1.txt即可。注意 Dev-C 默认使用的 GCC 版本较老-stdc11在旧版本上不识别这时可以去掉这个参数只保留-Wall。2.3 迷宫文件的格式约定迷宫文件本质上就是一个纯文本地图资源包里的 txt 文件遵守一套约定第一行是两个整数代表行数和列数从第二行开始每行是一个字符串字符含义如下#墙老鼠不能走空格或者.通道S老鼠的起点E出口一个典型的迷宫文件长这样8 10 ########## # S # # # ## # ## # # E# ##########这里容易忽略的是坐标约定。我解析文件时统一把x当作行号y当作列号那么上面这个地图里S的坐标是(1, 2)。如果你换成别的课设代码可能有人把方向数组写成[2][1]表示第 2 行第 1 列这就是行列序不一致。拿到资源后第一步建议先打印一遍start_x、start_y、end_x、end_y确认坐标系符合预期再谈寻路正确性。3. 核心算法栈模拟回溯把“撞墙后退换方向”翻译成C语言迷宫寻路的本质是深度优先搜索。老鼠走到岔路口随便选一个方向走撞墙就退回岔路口换一个方向。这个“退回岔路口”的动作在数据结构里就是栈的弹出操作。如果把这段逻辑讲清楚课程设计的核心分基本就拿到了。3.1 方向数组与二维数组建模迷宫整体用一个二维字符数组保存方向用二维数组表示。代码定义如下#define MAX_ROWS 100 #define MAX_COLS 100 char maze[MAX_ROWS][MAX_COLS]; int rows, cols; int start_x, start_y, end_x, end_y; // 四个方向上、下、左、右 int dir[4][2] { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };方向数组不是随便写的。dir[0]表示行坐标减 1也就是往地图上方移动一格dir[1]表示行坐标加 1往下方移动dir[2]和dir[3]分别表示左和右。搜索时用循环依次尝试这四个方向而不是手动写四段 if 分支。这样代码短也方便调整方向优先级。如果希望老鼠优先往右走就把{0, 1}提到数组最前面。3.2 用数组手写栈记录“从哪里来、试到哪个方向”栈元素不能只存坐标还要存一个方向下标。这一步是很多简化版代码容易偷懒的地方。如果只存坐标回溯时会忘记哪个方向已经试过结果原地重复试同一格死循环。合理定义如下typedef struct { int x; int y; int dir; // 下一次准备尝试方向的下标0~3 } StackItem; StackItem stack[MAX_ROWS * MAX_COLS]; int top -1; void push(int x, int y, int d) { top; stack[top].x x; stack[top].y y; stack[top].dir d; } StackItem pop(void) { StackItem item stack[top]; top--; return item; } int isEmpty(void) { return top -1; }栈容量直接设为MAX_ROWS * MAX_COLS意思是每个格子最多入栈一次不会溢出。dir在入栈时初始化为 0表示接下来从方向 0 开始试探。这个字段会在后面的回溯循环中被不断递增直到 4 就说明四个方向都试完了。3.3 回溯搜索主流程尝试、入栈、出栈、标记核心搜索函数是一个 while 循环处理栈顶元素按方向依次试探邻居。int findPath(void) { top -1; push(start_x, start_y, 0); maze[start_x][start_y] .; // 标记起点已走过 while (!isEmpty()) { StackItem *cur stack[top]; // 只读栈顶不着急弹 if (cur-x end_x cur-y end_y) { return 1; // 已经到出口 } int found 0; while (cur-dir 4) { int nx cur-x dir[cur-dir][0]; int ny cur-y dir[cur-dir][1]; cur-dir; // 方向指针后移 if (nx 0 || nx rows || ny 0 || ny cols) continue; if (maze[nx][ny] # || maze[nx][ny] .) continue; // 空格或者 E 都可以走 if (maze[nx][ny] E || maze[nx][ny] ) { push(nx, ny, 0); maze[nx][ny] .; found 1; break; } } if (!found) { // 四个方向都不通回溯 pop(); } } return 0; }这段代码的难点在于什么时候弹出栈。当cur-dir递增到 4 时说明当前格子四个方向都已经尝试过found仍为 0于是pop()回退到上一个岔路口。注意cur是指向stack[top]的指针cur-dir会真正修改栈顶元素里的方向值这也是它能持续推进的原因。把新位置压栈前立刻标记.这一步非常重要标注“已经访问过”否则两个相邻格子会互相入栈直接死循环。如果你正在看严蔚敏《数据结构C语言版》第三章会发现这个迷宫寻路就是典型栈应用的变体。基础版的课程设计到这里已经可以交差。升级版常见做法是找到出口后不急着 return而是用一个计数器累加同时把路径信息保存下来这样就能统计“一共有几条通路”。我在自己做的升级版里就是加了一个pathCount每次到出口加一继续回溯搜索下一分支。3.4 为什么强调“入栈时标记”而不是“出栈时标记”很多初学版本会在进while循环后再给maze[nx][ny] .甚至等到pop时才标记。这样会有个隐蔽问题A 格和 B 格相邻A 把 B 压入栈后还没轮到 B 执行A 又从另一个方向看到了 B 没有被标记于是再次压入。最终结果就是栈里全是重复坐标路径变成一圈圈原地绕。正确做法是在邻居满足可走条件的那一刻立刻标记这样其他格子不会再试探它。4. 迷宫文件与地图设计拿到迷宫文件后怎么改、怎么造新地图跑通一遍之后大多数人要做的第一件事是换自己的地图。迷宫文件不是随便改改就能用有几个约束条件需要先明白。4.1 字符地图的约束条件一张合法的迷宫地图首先最外层建议全部用#封闭否则老鼠可以直接沿着地图边界走到终点完全体现不了回溯过程。其次是S和E各只能出现一个程序一般用遍历找坐标如果地图里有两个E循环结束后end_x只会保存最后一个结果不可预测。行列数也不能超过源码里的MAX_ROWS和MAX_COLS。资源包自带的地图通常在 100 格以内课程设计演示完全足够但你自己用 Excel 拖一张 200 列的地图加载程序就会报告“迷宫大小超出限制”。手工设计迷宫时我习惯先把S和E之间的主线道路画出来保证至少有一条通路然后再用#把其余区域填充成墙。先有主线、后有墙体这样不会出现一上来就把路堵死的情况。4.2 解析函数fgets 读到换行符别让坐标错乱迷宫文件读取是另一个高频翻车点。最常见的解析方式是先用fscanf读第一行的行列数再用fgets逐行读地图。代码示例如下int loadMaze(const char *filename) { FILE *fp fopen(filename, r); if (!fp) { printf(无法打开文件 %s\n, filename); return -1; } if (fscanf(fp, %d %d\n, rows, cols) ! 2) { printf(文件头格式错误\n); fclose(fp); return -1; } if (rows MAX_ROWS || cols MAX_COLS) { printf(迷宫大小超出限制\n); fclose(fp); return -1; } for (int i 0; i rows; i) { if (fgets(maze[i], MAX_COLS, fp) NULL) { printf(读取第 %d 行失败\n, i); fclose(fp); return -1; } // 去掉行尾换行符防止边界判断出错 size_t len strlen(maze[i]); if (len 0 maze[i][len - 1] \n) maze[i][len - 1] \0; // Windows 文件里常常还带 \r if (len 1 maze[i][len - 2] \r) maze[i][len - 2] \0; for (int j 0; j cols; j) { if (maze[i][j] S) { start_x i; start_y j; } if (maze[i][j] E) { end_x i; end_y j; } } } fclose(fp); return 0; }这里有两个关键参数。第一个是fscanf(fp, %d %d\n, rows, cols)末尾的\n它会读掉第一行末尾的换行符但不会读掉\r。所以我在fgets之后连续两次截断先删\n再删\r。第二个参数是fgets的第三个参数MAX_COLS它代表最多读MAX_COLS - 1个字符。如果地图列数刚好等于MAX_COLS一行会被拆成两行读取后续行全部错位。用自己的地图时务必保证cols MAX_COLS。4.3 迷宫的合法性检测不能只靠人眼看在进入寻路前加一个checkMaze函数是课程设计加分项。它负责检查字符是否合法、起点终点数量是否正确int checkMaze(void) { int startCount 0, endCount 0; for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (maze[i][j] S) startCount; if (maze[i][j] E) endCount; if (maze[i][j] ! # maze[i][j] ! maze[i][j] ! . maze[i][j] ! S maze[i][j] ! E) { printf(第%d行第%d列存在非法字符\n, i, j); return -1; } } } if (startCount ! 1 || endCount ! 1) { printf(起点或终点数量不正确\n); return -1; } return 0; }这个函数的作用是让错误暴露在寻路之前。答辩现场老师经常直接修改 txt 文件、故意放一个非法字符程序报错信息是否清晰直接决定老师对你的项目完成度的判断。5. 常见问题与避坑指南这份C语言迷宫源码最容易翻车的4个地方5.1 只找到一条路径就返回完全没体现回溯过程现象程序能跑通但路线是直线看不到任何“退回去换条路”的过程答辩效果很差。原因代码里一遇到出口就return 1而且已经用.标记了路径其他分支永远不会被探索。解决把出口处的return改成nearExit 1;并继续循环或者额外用一个pathCount变量记录出口次数。每次到出口时先把当前栈里的坐标打印出来再继续回溯寻找下一条路。这样既能演示回溯又能展示多条路径。5.2 路径穿墙或数组越界崩溃现象打印地图时老鼠走过的路径直接穿过了#墙或者程序运行一会儿后异常退出。原因方向数组计算出的新坐标没有同时判断上界和下界。很多代码只写了if (nx 0) continue;却漏了if (nx rows) continue;当老鼠走到最下边时就越界了。解决每个邻居坐标都统一做四界判断if (nx 0 || nx rows || ny 0 || ny cols) continue;而且要在数组下标maze[nx][ny]之前做判断不要先访问再判断。5.3 栈不断增长程序卡死现象迷宫很小但程序运行几十秒都没有结果像陷入死循环。原因最常见的是漏了“已访问”标记导致两个相邻空格互相入栈。另一个原因是栈元素里的dir字段没有初始化为 0回溯时从随机值开始试探方向判断永远不对栈无法回退。解决在push(nx, ny, 0)入栈的一瞬间就写maze[nx][ny] .。同时检查push函数里是否把dir赋值为 0。我用调试手段是在每次push后打印一次stack[top].x, stack[top].y, stack[top].dir肉眼跟踪几轮就能看出问题。5.4 迷宫文件第一行读到乱码起点坐标错乱现象同样的 txt 文件在记事本里看是正常的程序加载后起点跑到奇怪的位置地图第一行看起来有不可见字符。原因文件被保存成了 UTF-8 with BOM 编码文件头多出EF BB BF三个字节fscanf读第一行行列数时解析失败。另一个常见原因是 Windows 下换行符是\r\n如果加载函数只处理了\n每行地图末尾会残留一个\r最后一列全部判断失败。解决用 Notepad 或 VS Code 把地图文件另存为 “UTF-8 without BOM” 或 “ANSI” 编码。代码里在fgets后处理\r具体写法在 4.2 节已经给出。课程设计交作业时我一般习惯把所有地图文件统一转成 ANSI 保存兼容性最好也免得在老师机器上乱码。6. 进阶把“找一条路”升级成“找最短路”并验证算法够不够稳资源包里的源码默认是深度优先搜索适合演示回溯。但实际游戏里的老鼠走迷宫有时候想要的是“最短路径”。这个进阶功能不需要改迷宫结构只需要把栈换成队列。6.1 BFS 改造思路广度优先搜索用队列每一步扩展四个方向的邻居先到达出口的那一次就是最短步数。队列可以继续用数组模拟typedef struct { int x, y, step; } Node; Node queue[MAX_ROWS * MAX_COLS]; int head 0, tail 0; void enqueue(int x, int y, int step) { queue[tail].x x; queue[tail].y y; queue[tail].step step; tail; } Node dequeue(void) { return queue[head]; }搜索时从起点出发把四个方向的可走点全部入队依次处理。遇到E就输出当前的step 1因为从当前格子再走一步就到出口。要把路径画出来还需要一个prev数组记录每个格子的前一个坐标类似“指路牌”最后从出口倒推到起点。BFS 的坑在于队列容量。同一层的节点会同时入队容量必须大于地图总格子数。我习惯在enqueue时加一个判断如果tail MAX_ROWS * MAX_COLS就退出并提示免得tail越界把别的内容覆盖了。6.2 验证算法正确性的固定套路我写完任何一份迷宫搜索代码都会用三张固定地图做回归测试。第一张是简单直线地图从S到E中间没有岔路结果必须只有一条路径。第二张是无解地图出口周围全部用#围死程序要正常输出“找不到路径”而不是崩溃。第三张是带环地图故意让路径中有一段环形通道检验maze[nx][ny] .的标记是否挡住回头路。验证路径长度时我会手工数一遍打印出来的坐标点个数再和 BFS 输出的step对比。如果对不上大概率是出口位置的步数加一减一没算对。换地图跑的时候优先看起点和终点坐标的打印结果如果和 txt 文件里的位置不一致先回看文件头的fscanf格式再查坐标原点是否被约定成左上角。多数迷宫源码的坐标原点都约定为左上角也就是第一行第一列是(0,0)这点不要乱改。还有一个演示技巧在做课程设计时把深度优先和广度优先都实现出来用同一张地图跑两个结果。深度优先路径通常不走运地绕一大圈广度优先则直接输出最短路两个结果一对比老师立刻知道你没背答案。从那以后我每次交迷宫题作业都会强制走一遍“检查 BOM、清理换行符、四界判断、栈顶方向初始化”这四件事减少很多不必要的返工。希望帮到你。本文还有配套的精品资源点击获取