☰
数据结构课设核心:用C语言实现迷宫求解的栈与队列本质
2026/10/6 13:03:58 网站建设 项目流程

简介:本资源是面向高校计算机专业本科生的数据结构课程设计实践项目,聚焦经典图搜索问题——老鼠走迷宫的C++完整实现,旨在帮助学习者深入理解栈、队列、图遍历等核心数据结构与DFS算法的实际应用。压缩包共27个文件,包含可直接运行的exe程序、Visual Studio工程(sln/vcxproj)、关键源码(cpp/h)、随机迷宫生成逻辑与路径可视化代码,以及2个教学演示mp4视频(含使用说明与素材替换操作),辅以txt文档说明和png/jpg资源素材;整体大小148.59MB,结构清晰,便于编译调试与二次开发。已有1389人学习下载,提供从迷宫生成、老鼠寻路到界面替换的全流程实现,特别适合课程设计参考、算法可视化教学及C++数据结构综合实训。

1. 为什么“老鼠走迷宫”不是玩具代码,而是数据结构课设的试金石?

你交上去的那份《老鼠走迷宫》课设,老师真正在看的,从来不是那只用*和#拼出来的老鼠能不能走到终点——而是在看你有没有把栈、队列、图遍历这些抽象结构,真正焊进具体问题的血肉里。我带过七届数据结构实验课,每年都有学生用硬编码写死路径、用全局变量暴力回溯、甚至把整个迷宫当字符串replace来“走”,结果调试三天跑不出一个正确解,最后靠截图拼接“伪运行”交差。这不是编程能力问题,是没吃透“结构决定行为”这个底层逻辑:用栈就是深度优先的试探与撤退,用队列就是广度优先的层序推进,用邻接表建图就是把二维坐标映射成可索引的节点关系。本篇不讲伪代码,不画流程图,只带你用 C 语言(课设最常用、最能暴露内存和指针细节的语言)从零实现一个可调试、可验证、可改参数、可测时间复杂度的迷宫求解器。重点落在:怎么选结构、为什么这么选、哪一行代码在动哪个数据结构、出错了看哪几行日志就能定位——这才是课设拿高分、面试被追问时能掰开揉碎讲清楚的硬功夫。


2. 迷宫建模:用二维数组打底,但绝不能只靠二维数组

迷宫本质是图,而图的存储方式直接决定算法效率和代码可读性。很多同学一上来就int maze[20][20]硬刚,后面所有逻辑都围着下标加减转,结果if (i+1 < N && maze[i+1][j] == 0)写满屏幕,边界判断漏一个就段错误。这不是代码量问题,是模型抽象层级太低。我们必须把“位置”从(i,j)升级为可封装、可比较、可入队/入栈的一等公民。

2.1 定义坐标结构体:让位置有身份,而不是数字对

typedef struct { int x; int y; } Position; // 重载等于判断(用于 visited 判重) int pos_equal(Position a, Position b) { return (a.x == b.x && a.y == b.y); } // 打印位置(调试必备) void print_pos(Position p) { printf("(%d,%d)", p.x, p.y); }

提示:别用#define POS(x,y) ((x)*100+(y))这种整数哈希——看似省事,但x=100,y=1和x=1,y=100会冲突,且无法直观调试。结构体虽多占几个字节,但语义清晰、调试友好、后续扩展(比如加步数、父节点指针)无缝。

2.2 迷宫数据结构:二维数组 + 元信息封装

#define MAX_SIZE 50 typedef struct { int grid[MAX_SIZE][MAX_SIZE]; // 0:通路, 1:墙, 2:起点, 3:终点 int rows; int cols; Position start; Position end; } Maze;

关键点在于:grid只存状态,start/end存逻辑角色。这样初始化时就能强制校验起点终点存在:

int init_maze_from_file(Maze* m, const char* filename) { FILE* f = fopen(filename, "r"); if (!f) return -1; fscanf(f, "%d %d", &m->rows, &m->cols); for (int i = 0; i < m->rows; i++) { for (int j = 0; j < m->cols; j++) { fscanf(f, "%d", &m->grid[i][j]); if (m->grid[i][j] == 2) m->start = (Position){i, j}; if (m->grid[i][j] == 3) m->end = (Position){i, j}; } } fclose(f); // 强制校验:起点终点必须存在 if (m->start.x == 0 && m->start.y == 0 && m->grid[0][0] != 2) { fprintf(stderr, "Error: Start position (2) not found in maze\n"); return -1; } if (m->end.x == 0 && m->end.y == 0 && m->grid[0][0] != 3) { fprintf(stderr, "Error: End position (3) not found in maze\n"); return -1; } return 0; }

这段代码的价值不在读文件,而在把业务约束(起点终点必须存在)提前到初始化阶段捕获。课设中常见“程序跑完没输出”,八成是起点没设对,但学生还在dfs()里打printf查原因——这就是模型没兜住业务规则的典型翻车。

2.3 四方向移动:用数组代替四个 if,避免手抖写错

// 顺序:上、右、下、左 —— 对应 DFS 的试探顺序 const int dx[4] = {-1, 0, 1, 0}; const int dy[4] = {0, 1, 0, -1}; // 检查新位置是否合法(越界、非墙、未访问) int is_valid_move(const Maze* m, Position next) { if (next.x < 0 || next.x >= m->rows || next.y < 0 || next.y >= m->cols) { return 0; // 越界 } if (m->grid[next.x][next.y] == 1) { return 0; // 是墙 } return 1; }

注意dx/dy数组顺序决定了 DFS 的路径偏好(先往上探),也决定了 BFS 的层序展开方向。这个数组就是你的算法“性格开关”——想让老鼠优先往右走?把0,1放第一位;想模拟真实鼠类习惯(贴边走)?把0,-1(左)和0,1(右)放前面。课设报告里写一句“通过调整方向数组顺序,可模拟不同寻路策略”,老师一眼看到你懂设计意图。


3. 栈 vs 队列:用两种结构实现同一迷宫,看清本质差异

课设要求常写“分别用栈和队列实现”,但很多同学复制粘贴改个函数名就交差。真正的价值在于:同一个迷宫,栈给出的是一条曲折但可能最短的路径(DFS),队列给出的是绝对最短但需更多内存的路径(BFS)。我们用同一套Maze结构,只换底层容器。

3.1 手写栈:理解 LIFO 如何驱动回溯

#define STACK_SIZE 1000 typedef struct { Position data[STACK_SIZE]; int top; } Stack; void stack_init(Stack* s) { s->top = -1; } int stack_push(Stack* s, Position p) { if (s->top >= STACK_SIZE - 1) return -1; s->data[++s->top] = p; return 0; } int stack_pop(Stack* s, Position* p) { if (s->top == -1) return -1; *p = s->data[s->top--]; return 0; } int stack_empty(Stack* s) { return s->top == -1; }

DFS 主循环核心:

int dfs_solve(Maze* m, Stack* path) { Stack stack; stack_init(&stack); stack_push(&stack, m->start); // visited 数组标记已探索位置(防环) int visited[MAX_SIZE][MAX_SIZE] = {0}; visited[m->start.x][m->start.y] = 1; while (!stack_empty(&stack)) { Position cur; stack_pop(&stack, &cur); // 找到终点 if (pos_equal(cur, m->end)) { // 将路径倒序存入 path(因为栈是后进先出) Stack temp; stack_init(&temp); stack_push(&temp, cur); while (!stack_empty(&stack)) { stack_pop(&stack, &cur); stack_push(&temp, cur); } // temp 中是正向路径,导出到 path *path = temp; // 简化处理,实际需深拷贝 return 1; } // 四方向试探 for (int i = 0; i < 4; i++) { Position next = {cur.x + dx[i], cur.y + dy[i]}; if (is_valid_move(m, next) && !visited[next.x][next.y]) { visited[next.x][next.y] = 1; stack_push(&stack, next); } } } return 0; // 无解 }

关键洞察:stack_pop取出的是最新压入的位置,所以它总在一条路径上钻到底(比如一直往右),撞墙才弹出一层,退回上一个岔路口再试下一个方向——这就是“深度优先”的物理实现。visited数组在这里是防重复探索,不是防环(迷宫本无环),但少了它就会无限循环。

3.2 手写队列:理解 FIFO 如何保证最短路径

#define QUEUE_SIZE 1000 typedef struct { Position data[QUEUE_SIZE]; int front; int rear; } Queue; void queue_init(Queue* q) { q->front = q->rear = 0; } int queue_enqueue(Queue* q, Position p) { if ((q->rear + 1) % QUEUE_SIZE == q->front) return -1; q->data[q->rear] = p; q->rear = (q->rear + 1) % QUEUE_SIZE; return 0; } int queue_dequeue(Queue* q, Position* p) { if (q->front == q->rear) return -1; *p = q->data[q->front]; q->front = (q->front + 1) % QUEUE_SIZE; return 0; } int queue_empty(Queue* q) { return q->front == q->rear; }

BFS 主循环核心:

int bfs_solve(Maze* m, Stack* path) { Queue queue; queue_init(&queue); queue_enqueue(&queue, m->start); // parent 数组记录路径(BFS 必须,用于回溯最短路径) Position parent[MAX_SIZE][MAX_SIZE]; memset(parent, -1, sizeof(parent)); // 初始化为 (-1,-1) parent[m->start.x][m->start.y] = m->start; // 起点父节点指向自己 int visited[MAX_SIZE][MAX_SIZE] = {0}; visited[m->start.x][m->start.y] = 1; while (!queue_empty(&queue)) { Position cur; queue_dequeue(&queue, &cur); if (pos_equal(cur, m->end)) { // 从终点反向构建路径 Stack temp; stack_init(&temp); Position p = cur; while (!pos_equal(p, m->start)) { stack_push(&temp, p); p = parent[p.x][p.y]; } stack_push(&temp, m->start); // 加入起点 // temp 是反向路径,需反转存入 path *path = temp; // 简化,实际需反转拷贝 return 1; } for (int i = 0; i < 4; i++) { Position next = {cur.x + dx[i], cur.y + dy[i]}; if (is_valid_move(m, next) && !visited[next.x][next.y]) { visited[next.x][next.y] = 1; parent[next.x][next.y] = cur; // 记录谁走到这里 queue_enqueue(&queue, next); } } } return 0; }

关键区别:queue_dequeue取出的是最早入队的位置,所以所有距离起点 1 步的位置先被处理,再处理所有距离 2 步的位置……天然按层展开。parent数组是 BFS 的灵魂——没有它,你只能知道“能走到”,但不知道“怎么走最短”。课设报告里画一张 BFS 层序展开图,比写一百行注释都有力。


4. 避坑:课设高频翻车现场与血泪修复方案

学生交上来的代码,80% 的问题集中在以下五个点。这些不是语法错误,而是对数据结构本质理解偏差导致的系统性缺陷,必须逐条击穿。

4.1 现象:DFS 找到路径但长度远超 BFS,甚至出现绕圈

原因:visited数组在 DFS 中被误用为“已走过路径”的标记,而非“已探索位置”的标记。典型错误是:在stack_push前不标记visited,导致同一位置被多次压栈,形成无效循环。
解决:visited必须在push之前设置。检查你的 DFS 循环里,is_valid_move后、stack_push前,是否有visited[next.x][next.y] = 1;。缺这一行,就是玄学绕路的根源。

4.2 现象:BFS 运行崩溃或路径为空,但迷宫明显可通

原因:parent数组未初始化,或memset(parent, -1, sizeof(parent))用错。C 语言中Position是结构体,-1不能直接赋给x/y成员,会导致parent[i][j].x = -1但parent[i][j].y是随机值,回溯时访问非法内存。
解决:用memset(parent, 0, sizeof(parent))清零,然后显式设置起点parent[start.x][start.y] = start;。或者更安全:用循环初始化for (int i=0; i<MAX_SIZE; i++) for (int j=0; j<MAX_SIZE; j++) parent[i][j] = (Position){-1,-1};。

4.3 现象:输入迷宫文件后程序直接退出,无任何提示

原因:fscanf读取rows/cols后,文件指针停在换行符,后续读grid时第一个fscanf读到换行符返回 0,导致grid[0][0]为 0,起点检测失败。
解决:在读完rows/cols后加fgetc(f)吸收换行符,或用fgets读整行再sscanf解析。课设环境文件格式简单,推荐fgetc(f):

fscanf(f, "%d %d", &m->rows, &m->cols); fgetc(f); // 吸收换行符

4.4 现象:路径打印出来坐标全为(0,0)或乱码

原因:路径栈Stack path在函数内定义,dfs_solve返回时栈对象生命周期结束,path.data指向的内存已被回收。学生常犯“返回局部数组”错误。
解决:路径栈必须由调用方分配并传入。修改函数签名:

int dfs_solve(Maze* m, Stack* path); // path 由 main 分配

并在main中:

Stack result_path; stack_init(&result_path); if (dfs_solve(&maze, &result_path)) { print_path(&result_path); }

4.5 现象:迷宫含多个出口,但程序只找到第一个

原因:算法逻辑中if (pos_equal(cur, m->end))一找到就return 1,但m->end是单点。若需求是找所有路径,必须移除该return,改为收集所有到达end的路径。
解决:课设明确要求“任一路径”则保留;若要求“所有路径”,需将visited改为int count[MAX_SIZE][MAX_SIZE]记录到达该点的路径数,并用递归 DFS(非栈模拟)实现。但课设通常不要求,此坑提醒你:读懂题目比写代码更重要。


5. 路径可视化与性能验证:让课设从“能跑”升级为“可证”

课设报告里光写“算法正确”是苍白的。老师想看到你用数据证明它真的正确、真的高效、真的可控。下面三个技巧,能把你的报告从 80 分拉到 95 分。

5.1 终端彩色路径渲染:一眼看出算法行为差异

纯文本迷宫难看出路径优劣。用 ANSI 转义序列给路径加色(Windows CMD 需启用虚拟终端):

void print_maze_with_path(const Maze* m, const Stack* path) { // 先提取路径坐标到集合(便于 O(1) 查询) int in_path[MAX_SIZE][MAX_SIZE] = {0}; Stack temp = *path; while (!stack_empty(&temp)) { Position p; stack_pop(&temp, &p); in_path[p.x][p.y] = 1; } for (int i = 0; i < m->rows; i++) { for (int j = 0; j < m->cols; j++) { if (in_path[i][j]) { if (pos_equal((Position){i,j}, m->start)) { printf("\033[1;32mS\033[0m"); // 绿色起点 } else if (pos_equal((Position){i,j}, m->end)) { printf("\033[1;31mE\033[0m"); // 红色终点 } else { printf("\033[1;34m*\033[0m"); // 蓝色路径 } } else { switch (m->grid[i][j]) { case 0: printf(" "); break; // 通路 case 1: printf("\033[1;37m#\033[0m"); break; // 白色墙 case 2: printf("\033[1;32mS\033[0m"); break; // 起点(未在路径中) case 3: printf("\033[1;31mE\033[0m"); break; // 终点(未在路径中) } } } printf("\n"); } }

注意:in_path数组必须在渲染前构建,否则stack_pop会破坏原路径栈。这是调试可视化的基本功——路径不是抽象概念,是屏幕上可触摸的坐标序列。

5.2 步数与时间统计:用数据说话,拒绝“我觉得很快”

课设常忽略性能验证。加两行代码,让报告有硬指标:

#include <time.h> clock_t start_time = clock(); int found = dfs_solve(&maze, &path); clock_t end_time = clock(); double cpu_time_used = ((double)(end_time - start_time)) / CLOCKS_PER_SEC; int steps = 0; Stack temp = path; while (!stack_empty(&temp)) { stack_pop(&temp, &cur); steps++; } printf("DFS: Found path in %.6f sec, %d steps\n", cpu_time_used, steps);

对比 BFS 的steps(必等于最短路径长度)和 DFS 的steps,就能定量说明:DFS 路径长但常更快(因早停),BFS 路径最短但耗时略长(因遍历全图)。这比写“BFS 时间复杂度 O(V+E)”有力十倍。

5.3 迷宫生成器:用随机算法造测试集,证明鲁棒性

手写迷宫易出错。写个简单递归分割法生成器,确保连通性:

void generate_maze(int grid[MAX_SIZE][MAX_SIZE], int r1, int c1, int r2, int c2) { if (r2 - r1 < 2 || c2 - c1 < 2) return; // 随机选一行一列挖通道 int r = r1 + rand() % (r2 - r1); int c = c1 + rand() % (c2 - c1); // 挖横道 for (int j = c1; j <= c2; j++) grid[r][j] = 0; // 挖竖道 for (int i = r1; i <= r2; i++) grid[i][c] = 0; // 递归四块 generate_maze(grid, r1, c1, r-1, c-1); generate_maze(grid, r1, c+1, r-1, c2); generate_maze(grid, r+1, c1, r2, c-1); generate_maze(grid, r+1, c+1, r2, c2); }

在main中:

int main() { srand(time(NULL)); Maze maze; // 生成 15x15 迷宫 for (int i = 0; i < 15; i++) for (int j = 0; j < 15; j++) maze.grid[i][j] = 1; // 全墙 generate_maze(maze.grid, 0, 0, 14, 14); maze.rows = maze.cols = 15; maze.start = (Position){0,0}; maze.end = (Position){14,14}; maze.grid[0][0] = 2; maze.grid[14][14] = 3; // 测试... }

有了生成器,你就能说:“本实现通过 100+ 随机迷宫验证,100% 找到路径”,而不是“我手写了 3 个迷宫,都过了”。

我带学生做课设时,总强调:数据结构不是背概念,是用结构去驯服问题。那只老鼠走的每一步,都在替你验证栈的 LIFO 是否可靠、队列的 FIFO 是否公平、visited数组是否真的挡住了无效探索。当你的 DFS 在 100x100 迷宫上 0.02 秒出解,BFS 用 0.05 秒给出最短路径,而你清楚每一毫秒花在哪——那一刻,数据结构才真正从课本跳进你的肌肉记忆。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询