C++数据结构课设实战:栈、队列、树与最短路径代码解析

发布时间:2026/10/10 9:55:48
C++数据结构课设实战:栈、队列、树与最短路径代码解析
简介这是一份面向高校计算机专业学生的C语言数据结构课程设计源码包内容覆盖数组、链表、栈、队列、二叉树与图等典型结构并涉及排序与查找算法的实现适合正在完成课设作业、复习数据结构或准备算法基础的初学者参考。压缩包体积仅4KB共7个文件其中.cpp源文件提供主要功能实现.xml与.iml工程配置文件帮助开发环境识别项目结构.gitignore用于避免版本管理时提交无关文件.txt则收录简要说明整体紧凑便于快速导入阅读。目前已有104人学习过该资源。通过这份课设源码使用者能了解一个基础项目的文件组织方式对照代码理解各类数据结构的实现细节、内存管理与边界条件处理还能参考栈的压入弹出、队列的入队出队、二叉树遍历以及排序查找算法的实际写法对巩固理论知识和提升编程实践能力都有直接帮助。1. C 数据结构 课设.zip这份压缩包能让你少熬三天夜但前提是你别直接交上去如果你也是数据结构课设周才开始打开某下载站那份 C 数据结构 课设.zip那你大概率和我当年一样压缩包里躺着十几个 .cpp 和一份写得像论文摘要的 .docx能编译但一运行就黑窗闪退或者菜单界面能点但一进算法就崩。这份资源其实覆盖了线性表、栈、树、图四大块典型课设题价值在「代码结构完整、函数分层清楚」但它不是交了就能过的东西——需要你按你自己的题目把输入输出接口改掉再对着报告把每段算法的变量变化捋一遍。适合正在赶课设的在校生也适合转行准备补项目经验的开发者拿来当脚手架不适合直接整包提交。2. 从栈和队列下手表达式求值最容易拿分也最容易翻车2.1 为什么课设包里永远有栈和队列它们是线性表的「难变体」打开压缩包你会发现线性表相关题目里链表、顺序表各有一版但真正让多数人写不出来的其实是栈和队列。因为链表增删节点看图还能画出来栈和队列一旦牵扯到「中缀转后缀」「循环队列判空判满」就全是下标和优先级运算错一个字符整段逻辑就偏了。课设选题时优先选这类题有个好处代码量不算大拿分点却很明确——老师检查时一眼就能看出你懂不懂后进先出和先进先出。以包里的「表达式求值」为例它的核心不在求值本身而在「中缀转后缀」。理解了这一步栈的作用就从「存储容器」变成了「运算符调度器」。另一道「模拟银行排队」则逼你写一个正经的手工循环队列而不是 new 一个 queue 完事。这两道题只要跑通课设报告的「核心算法」部分就有了可写的素材。2.2 中缀转后缀优先级表是唯一难点表达式求值我一般会给出两个函数中缀转后缀以及后缀求值。压缩包里的做法是前者用两个栈一个收数字、一个收运算符后者用单栈遇到数字压栈遇到运算符弹两个数字算完再压回去。先看中缀转后缀的核心代码#include iostream #include string #include stack #include map #include cctype std::mapchar, int priority {{, 1}, {-, 1}, {*, 2}, {/, 2}, {(, 0}}; // 中缀表达式转后缀表达式输入是形如 3*(45)-6 的字符串 std::string infixToPostfix(const std::string expr) { std::stackchar ops; // 运算符栈 std::string postfix; // 输出的后缀结果 for (size_t i 0; i expr.length(); i) { char ch expr[i]; if (std::isdigit(ch)) { // 连续数字拼成一个整数课设里数字都是个位数但写成循环更稳 while (i expr.length() std::isdigit(expr[i])) { postfix expr[i]; i; } postfix ; --i; // 外层循环还会 i这里先回退 } else if (ch () { ops.push(ch); } else if (ch )) { // 遇到右括号把栈顶弹到左括号为止 while (!ops.empty() ops.top() ! () { postfix ops.top(); postfix ; ops.pop(); } ops.pop(); // 弹出左括号不放进后缀表达式 } else if (ch || ch - || ch * || ch /) { // 当前运算符优先级不高于栈顶说明栈顶运算符可以先输出 while (!ops.empty() priority[ch] priority[ops.top()] ops.top() ! () { postfix ops.top(); postfix ; ops.pop(); } ops.push(ch); } // 其余字符空格、字母直接忽略 } while (!ops.empty()) { // 剩余运算符出栈 postfix ops.top(); postfix ; ops.pop(); } return postfix; }这段代码里最容易忽略的是两个点一是priority[(] 0这样左括号在栈里不会被比下去只有右括号能把它弹出来二是while (std::isdigit(ch))循环里那个--i如果不回退外层循环会跳过一个字符——这是新手最容易犯的越界错误。后缀表达式字符串里我统一在数字、运算符后面拼一个空格是为了后面求值函数用istringstream分割时方便如果你的题目要求输出345*6-这种紧凑格式把拼空格的三行删掉即可。2.3 手写循环队列别直接用 std::queue老师要看底层实现另一道题「模拟银行排队」或者「舞会配对」看着简单但如果你直接在代码里写#include queue答辩时老师问「这个队列怎么判断满」你就接不上话。压缩包里的写法是数组加头尾指针class CircularQueue { private: int* data; // 数组存储元素 int capacity; // 队列容量 int front, rear; // 头尾下标 int count; // 当前元素数量 public: CircularQueue(int cap) : capacity(cap), front(0), rear(0), count(0) { data new int[capacity]; } ~CircularQueue() { delete[] data; } bool isEmpty() const { return count 0; } bool isFull() const { return count capacity; } void enqueue(int value) { if (isFull()) { // 课设里一般直接 return或者抛异常 return; } data[rear] value; rear (rear 1) % capacity; // 循环的关键取模绕回数组头部 count; } bool dequeue(int out) { if (isEmpty()) { return false; } out data[front]; front (front 1) % capacity; --count; return true; } int size() const { return count; } };关键参数是capacity它代表数组实际能存的元素数量。如果用「牺牲一个空位」的判断方式——(rear 1) % capacity front表示满——那数组要开成capacity 1否则最后一个位置永远浪费。压缩包用的是count计数法不浪费空间但代价是每次增删都要维护count二者取舍可以在报告的「方案对比」里写一段。有一点要留意构造参数传cap时如果调用处写的是CircularQueue q(10)那数组下标就是 0 到 9满了之后再次 enqueue 会被直接忽略——这就意味着你把「队列长度上限」当成了「银行一天接待的客户数」来用逻辑上是不对的正确做法是循环利用空间或者动态扩容。报名次、模拟叫号这类场景用户看到「队列满」直接走人所以这个判断其实意义不大反而是内存释放时delete[] data经常被漏掉报告里如果写了「系统无内存泄漏」就属于虚假陈述。3. 树的课设题二叉排序树与平衡因子怎么落成代码3.1 二叉排序树递归插入根节点引用是关键树这块课设包里最常见的题目是「学生成绩管理系统用二叉排序树存储」和「哈夫曼编码」二选一。哈夫曼编码要建最小堆还得写前缀编码代码量是 BST 的两倍所以我建议优先选二叉排序树。BST 的插入如果用递归有个容易被忽略的细节函数形参要传「指针的引用」否则你 new 出来的节点在函数返回后就丢了。正确写法看这里struct TreeNode { int key; std::string value; TreeNode* left; TreeNode* right; TreeNode(int k, const std::string v) : key(k), value(v), left(nullptr), right(nullptr) {} }; // 向 BST 中插入键值对root 是根节点指针的引用 void insertNode(TreeNode* root, int key, const std::string value) { if (root nullptr) { root new TreeNode(key, value); return; } if (key root-key) { insertNode(root-left, key, value); // 进左子树继续递归 } else if (key root-key) { insertNode(root-right, key, value); // 进右子树继续递归 } else { root-value value; // 键已存在则更新值 } }这里TreeNode* root就是那个引用——如果没有递归进去以后root new TreeNode(...)只是在栈上的局部副本里穿针引线外面那个指针依旧是nullptr插入等于白插。另一件事是重复键的处理课设里老师只会给「一个学号对应一个人」不会有两份相同学号数据所以走 else 直接覆盖值是稳妥做法。如果你想支持重复键就得把 else 分支改成往右子树塞但那样删除时会麻烦很多不推荐。3.2 平衡因子与旋转只背 LL/RR 也能应付验收如果你的题目是「AVL 树演示」那难点不在旋转而在「什么时候旋转」。先记住一个粗暴结论插入节点后从插入位置向上回溯第一个平衡因子变成 2 或 -2 的节点就是失衡点。// 计算以 node 为根的子树的平衡因子左高 - 右高 int balanceFactor(TreeNode* node) { if (node nullptr) return 0; return height(node-left) - height(node-right); } // LL 型失衡右旋一次恢复平衡 TreeNode* rotateRight(TreeNode* y) { TreeNode* x y-left; TreeNode* T2 x-right; // 执行右旋 x-right y; y-left T2; // 更新高度 y-height 1 std::max(height(y-left), height(y-right)); x-height 1 std::max(height(x-left), height(x-right)); return x; // 新根返回给上层 }注意rotateRight的函数签名入参是失衡节点y返回值是新局部子树的根x。调用处必须写成root rotateRight(root)或者node-left rotateRight(node-left)把返回值接住——因为旋转后原来的 y 已经不是这棵子树的根了。新手经常在这里翻车旋转函数写对了返回结果没赋给父指针等效于白转。至于 LR 和 RL 型建议不要在答辩时说「我完全掌握了」直接说「我实现的是先左再右的两次旋转把 LR 转成 LL再执行一次右旋」然后演示一段输入序列老师盯到你旋转后中序遍历仍然有序基本就过。新手普遍高估旋转的难处实际更常栽在高度更新顺序上——必须先更新 y 再更新 x因为 x 的新高度依赖 y 的新高度。3.3 遍历输出别用递归打印把结果先存进 vector课设里树这题必有一问是「按某种遍历方式输出学生名单」。最土的写法是递归函数里std::cout但你做系统演示时要同时输出到屏幕和文件递归里写两遍输出逻辑就很乱。压缩包的做法是// 中序遍历把结果按顺序收集到 out 中 void inorderCollect(TreeNode* root, std::vectorstd::pairint, std::string out) { if (root nullptr) return; inorderCollect(root-left, out); // 先左子树 out.push_back({root-key, root-value}); // 再当前节点 inorderCollect(root-right, out); // 最后右子树 }收集完之后想打印、想写文件、想按名次排都是对同一个vector的操作递归结构保持干净。因为中序遍历天然有序左中右你直接遍历 vector 就能得到成绩排好序的效果。如果题目要求「先根次序遍历」把 push_back 那行挪到两次递归调用前面就行——这也是代码里换一种遍历最省事的位置。4. 图的最短路径邻接表建图与 Dijkstra 的边界4.1 邻接表 vs 邻接矩阵根据数据规模选图论课设题里「校园导航」和「交通咨询系统」是常驻题目核心都是最短路径。选存储结构时先看压缩包里题的输入格式是「顶点个数 边数 顶点对」。顶点数在 50 个以内可以用邻接矩阵因为代码简单但多数课设题给的数据都是「几个校区互连」这种稀疏图邻接表在空间上更占优而且后面的 Dijkstra 遍历邻接表时天然只走存在的边不用反复跳过无用的 0。邻接表建图的核心是 vector 套 vector#include vector #include map #include string struct Edge { int to; // 目标顶点下标 int weight; // 边的权重距离或时间 }; // 图结构adj[i] 存放顶点 i 的所有出边 class Graph { public: std::vectorstd::vectorEdge adj; std::mapstd::string, int nameToId; // 顶点名映射到下标 std::vectorstd::string idToName; // 下标反查顶点名 Graph(int n) { adj.resize(n); idToName.resize(n); } void addEdge(int u, int v, int w) { // 无向图u-v 和 v-u 都要加 adj[u].push_back({v, w}); adj[v].push_back({u, w}); } };addEdge里两行 push_back 是重点。如果你的题目是单向道路比如「景区单向游览路线」那就只有第一行adj[u].push_back({v, w})第二行要删掉——这是有向图和无向图在代码上的唯一区别。但就是这个区别课设报告里经常写反老师一问「你这数据是双向的吗」就露馅。4.2 Dijkstra 实现INF 的处理和 visited 的更新逻辑最大堆优化版 Dijkstra 是压缩包里最有含金量的一段代码看懂它图论题的答辩就稳了一半#include queue #include limits #include functional // 返回从 start 到所有顶点的最短距离 std::vectorint dijkstra(const Graph g, int start) { const int INF std::numeric_limitsint::max(); std::vectorint dist(g.adj.size(), INF); std::vectorbool visited(g.adj.size(), false); // 小顶堆pair 的第一个元素是距离第二个是顶点下标 std::priority_queuestd::pairint, int, std::vectorstd::pairint, int, std::greater pq; dist[start] 0; pq.push({0, start}); while (!pq.empty()) { int d pq.top().first; int u pq.top().second; pq.pop(); if (visited[u]) continue; // 已经确定最短路径的顶点直接跳过 visited[u] true; for (const Edge e : g.adj[u]) { int v e.to; int w e.weight; if (!visited[v] dist[u] ! INF dist[u] w dist[v]) { dist[v] dist[u] w; pq.push({dist[v], v}); // 新距离入堆重复入堆没关系 } } } return dist; }这里有两个不直观的细节一是dist[u] ! INF的判断——虽然堆里弹出的 u 必然已经被更新过但邻接表里可能存储的边的起点有孤立顶点加上这个判断能防止INF w溢出成负数INT_MAX 加正数在某些编译器下会回绕成负值导致所有顶点瞬间变成可达这是实战里最隐蔽的坑。二是pq里同一个顶点可能被 push 两次或多次用visited在弹出时检查比在压入时检查更简洁——因为压入时你无法保证当前路径已经是最短弹出时堆帮你排好了序第一次弹出的才是最短。4.3 路径打印存前驱数组才能输出整条路线很多同学 Dijkstra 算对了距离但「路线怎么走」答不上来因为根本没存路径。压缩包里的做法不复杂加一个prev数组每次松弛成功时更新prev[v] u最后从终点往前回溯再反转// 从 end 回溯到 start把所有途径顶点存入 path void reconstructPath(int start, int end, const std::vectorint prev, std::vectorint path) { for (int cur end; cur ! start; cur prev[cur]) { if (cur -1) { // prev 数组初始化为 -1走到这说明终点不可达 path.clear(); return; } path.push_back(cur); } path.push_back(start); std::reverse(path.begin(), path.end()); }prev初始化的值不是 0而是 -1。用 0 的话起点编号为 0 的顶点回溯时永远无法停止循环——这是新手最常见的死循环来源。回溯的终止条件写cur ! start如果起点编号也是 0 而你初始 prev 又是 0第一轮循环直接进死局。所以一定初始化为 -1。有一点要提Floyd-Warshall 在这个题目里也可以跑而且代码更短三重循环加起来差不多 10 行。代价是 O(n³) 时间复杂度顶点超过 300 个就开始肉眼可见地卡。课设答辩时老师如果问「你为什么不直接用 Floyd」你就说「顶点较多且大多数对之间的查询不频繁Dijkstra 每次只跑一遍就能缓存所有结果调用多次更划算」——这套说辞基本能拴住提问。5. 避坑与排查课设包的常见翻车现场三条血泪经验5.1 黑窗口一闪而过断点都没踩到程序就退出了现象双击编译好的 exe控制台闪现一下立刻关闭连菜单都看不见。原因代码里没有在 main 函数结束前等待输入。课设包里的代码当初可能是用命令行直接跑没考虑图形界面调试编译后运行完毕自动退出。解决不要全局加system(pause)——虽然这能挡住窗口但答辩会被老师嫌弃。正确做法是写一个waitForExit()只在菜单首次显示前调用或者用调试器打断点。我习惯的项目结构是// main.cpp 末尾在 return 0 之前调用 std::cout \n按回车键退出...; std::cin.get();关键在std::cin.get()之前要先清掉输入缓冲区残留的回车符。如果之前代码用了std::cin choice缓冲区里那个换行会被 get 直接吃掉窗口照样关闭。我一般会先用std::cin.ignore()再get()std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); std::cin.get();5.2 读取文件的中文乱码编码不一致现象课设包里附带的students.txt用记事本打开正常程序读出来全是乱码。原因Windows 下 dev-C 默认按 GBK 读取而文件保存成了 UTF-8或者反过来源文件是 UTF-8 的中文菜单字符串控制台用 GBK 解码也是一样花掉。中文乱码是「编码不统一」的两面不是 C 的 bug。解决如果只是课设展示最省事的方案是把所有输入文件和源文件统一成无 BOM 的 UTF-8然后在main开头写#ifdef _WIN32 system(chcp 65001 nul); // 把控制台代码页切成 UTF-8 #endif如果代码里已经用了std::string存中文字符串这个方案需要配合编译器选项。我实际课设里更倾向反过来把文件存成 ANSI即 GBK然后控制台保持默认代码页读字符串时原样进内存输出也原样出——中文路径和中文内容都不捣乱。除非你的开发环境在 Linux 上否则别在课设阶段折腾 UTF-8这是纯自找麻烦。5.3 内存泄漏与野指针别等系统崩溃才排查现象程序运行几分钟后内存占用上升或者连续 insert 和 delete 操作几次后出现随机崩溃。原因链表删除节点后没把被删节点的 next 指针赋空、或者 delete 之后继续访问、再或者树递归析构漏删了子节点。课设包里的数据量小泄漏一般不会立刻出问题但连续操作几十次后堆结构被破坏崩溃就是玄学。解决析构函数统一长相——递归释放并置空~TreeNode() { delete left; // 递归删除左子树 delete right; // 递归删除右子树 left nullptr; right nullptr; }delete left会递归把整棵左子树删干净然后再置空。注意这段代码用在链表节点上时要小心链表节点没有 delete 子节点的逻辑如果你把树的析构模板套到链表上会把整个链表递归删爆。检查内存泄漏最简单的办法是调试结束后在 VS 的「输出」窗口里看「检测到内存泄漏」警告Linux 上就装valgrind跑一遍valgrind --leak-checkfull ./your_program它会精确告诉你哪一行 new 出来的内存没有回收。5.4 递归深度数据量一大就爆栈现象BST 插入几百个升序数据后程序直接退出没有任何报错。原因二叉排序树插入升序序列会退化成单链表递归深度等于节点数栈溢出。这是 BST 的固有问题不是程序写的错。解决常见做法是插入前先随机洗牌或者改用 AVL 树。课设答辩时老师如果问这个标准回答是「BST 在有序输入下性能退化所以我给了两种方案常规模式用 BST演示模式用 AVL 树」。如果你只写了一遍 BST就在读取文件后先乱序再插入——std::shuffle即可也能缓解但要注意不要把成绩顺序打乱了因为插入顺序不是输出顺序最终中序遍历仍然有序。6. 验收前的最后一步手工构造测试用例把运行结果和报告对齐6.1 三组必测用例无论你选了哪几道题交课设前我都建议手工跑这几组数据并把结果截图放进报告附录场景测试输入预期结果考察点表达式求值3*(45)-621括号优先级、双括号嵌套、减法最后执行表达式求值( (102) / 3 ) * 416连续双层括号、除法整除性BST 操作按5 3 7 2 4 6 8顺序插入删除根节点5中序遍历仍为2 3 4 6 7 8删除时取左子树最大节点验证指针接续Dijkstra三个顶点 0-1 权重 2、1-2 权重 4、0-2 权重 10查询 0 到 2最短距离 6路径0 - 1 - 2验证不是直接取直连最短边而是整体最短第一组和第二组的数字故意选成不同位数的能验证你代码里对多位数数字的拼接处理是不是真的生效。第三组删除根节点的用例是老师最爱在验收时手输的——因为绝大多数课设代码插入没问题删除时头结点处理经常挂在野指针上。第四组则考验路径回溯的正确性。6.2 关于「老师说代码不是你写的」这件事这其实是课设包里最容易被忽视的隐性风险。源代码整体风格统一、注释稀疏如果你说不清balanceFactor那段为什么是height(left) - height(right)而不是反过来老师当然会怀疑。我的做法是拿到压缩包后通读一遍把所有自己改过、加过注释的 diff 存成一段文字写进报告的「个人完成情况」里。答辩现场只演示两样东西一是运行时的操作流程二是随意改一个测试数据后结果随之变化的瞬间——后者最能证明你改过代码。我把每个主函数入口都做成「先读取文件再进菜单循环」的样式老师抽查某个功能时你当场改文件内容重新运行是最有说服力的。从那以后我每次整理课设资源都强制自己先走一遍那四组用例再解压发布一组顺手牵羊的冒烟测试一组边界输入一组删除与再插入的循环压力一组图形界面与文件输出的对照。这套流程花费不到半小时却能让整个项目的「可信度」翻一倍还多。希望帮到你。本文还有配套的精品资源点击获取