ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据结构C++实训:作业完成情况管理程序从建模到性能优化

数据结构C++实训:作业完成情况管理程序从建模到性能优化 简介这份资源是面向计算机相关专业学生与C初学者的数据结构实训完整资料包围绕「作业完成情况管理程序」这一典型课程设计展开帮助读者把数组、链表、栈、队列、树等抽象数据结构落到可运行的C代码中。压缩包共11个文件约3.2MB包含cpp源码、可执行exe、工程配置cbp与layout、依赖depend等工程文件以及doc实训论文与实施计划书、pptx答辩汇报、txt数据文件和rar子模块覆盖从编码到答辩的全流程。目前已有655人学习下载。读者可借助源码理解类与对象、封装继承多态的实际用法通过论文与计划书梳理设计思路、算法选型与问题解决方案再结合答辩PPT把握项目重点与亮点适合作为课程设计参考、实训复盘或自学练手素材。1. 数据结构C实训作业完成情况管理程序到底在练什么很多人看到「作业完成情况管理程序」第一反应是不就是个增删改查吗有什么好实训的。真动手写一遍就知道这个题目的坑不在业务逻辑而在数据结构选型和 C 内存管理。它要求你把学生、作业、提交记录这三类实体组织起来支持按学号查、按作业查、按班级统计完成率还要能排序、能持久化。用数组硬写能跑但一学期几百人几十次作业查询和统计就会卡用链表写插入快但随机访问又拉胯。所以这个实训真正练的是面对一个具体管理场景你怎么在顺序表、链表、哈希表、二叉搜索树之间做取舍并用 C 把它落地。适合刚学完数据结构、想找一个完整项目把知识点串起来的人也适合准备课程设计但不知道从哪下手的人。下面按我实际做过的路径从建模到跑通再到排错一步步拆开讲。2. 先把数据模型定下来三类实体和它们的关系2.1 学生、作业、提交记录怎么抽象成结构体这个程序的核心不是「管理」两个字而是三张表之间的关系。学生是一张表作业是一张表提交记录是连接两者的多对多关系。很多新手一上来就写一个巨大的结构体把学生信息和提交信息揉在一起后面统计完成率时就会发现数据冗余到没法维护。我一般会拆成三个独立结构体用 ID 做外键关联#include string #include vector #include ctime struct Student { int id; // 学号唯一 std::string name; // 姓名 std::string className; // 班级用于按班统计 }; struct Assignment { int id; // 作业编号唯一 std::string title; // 作业标题 std::time_t deadline; // 截止时间 }; struct Submission { int studentId; // 外键指向 Student.id int assignmentId; // 外键指向 Assignment.id bool completed; // 是否完成 std::time_t submitTime; // 提交时间 };逻辑说明Student 和 Assignment 各自独立存储Submission 只存两个 ID 加状态。这样查某个学生所有作业时遍历 Submission 按 studentId 过滤即可统计某次作业完成率时按 assignmentId 过滤。参数上id 用 int 足够除非学号带字母那就换 string。className 单独存而不是从学号解析是因为班级命名规则各校不同硬解析容易翻车。提示deadline 和 submitTime 用 time_t 存时间戳比较和排序都方便展示时再格式化。2.2 用哪种容器存vector、list 还是 unordered_map结构体定完接下来是选容器。这是这个实训最值得琢磨的地方也是很多人直接抄答案却说不清为什么的地方。学生表我一般用std::vectorStudent因为学生数量相对固定主要操作是按学号查找和遍历统计。vector 内存连续遍历快配合按 id 排序后二分查找O(log n) 就能定位。作业表同样用 vector理由一样数量少。提交记录是重点。如果一学期 200 人、20 次作业就是 4000 条记录。用 vector 存每次查「某学生某作业是否完成」都要线性扫4000 条还能忍但统计完成率要反复扫就很浪费。我一般用std::unordered_map做索引key 用studentId * 1000 assignmentId这种组合键value 存 Submission 或直接存 bool#include unordered_map // 组合键学号 * 1000 作业号前提是作业号小于 1000 std::unordered_maplong long, Submission submissionIndex; long long makeKey(int studentId, int assignmentId) { return static_castlong long(studentId) * 1000 assignmentId; }逻辑说明组合键把二维关系压成一维哈希查找平均 O(1)。参数上乘 1000 是假设作业编号不超过 999如果作业可能上百改成乘 10000。这个技巧在「数据结构 王道408」里哈希表那章讲过但真正用起来要注意键冲突和溢出long long 是必须的int 在学号大时会溢出。注意unordered_map 不保证顺序如果需要按学号顺序输出统计结果最后要单独排序一次。3. 核心功能实现查询、统计、排序逐个落地3.1 按学号查完成情况的最小实现查询是最基础也最常用的功能。给定学号列出该生所有作业的完成状态。有了上面的索引实现很直接#include iostream void queryByStudent(int studentId, const std::vectorAssignment assignments, const std::unordered_maplong long, Submission index) { std::cout 学号 studentId 的作业完成情况\n; int done 0; for (const auto a : assignments) { long long key makeKey(studentId, a.id); auto it index.find(key); bool completed (it ! index.end() it-second.completed); if (completed) done; std::cout 作业 a.id a.title : (completed ? 已完成 : 未完成) \n; } std::cout 完成 done / assignments.size() \n; }逻辑说明遍历作业列表对每个作业用组合键去哈希表里查。找到且 completed 为 true 才算完成。参数上 assignments 传引用避免拷贝index 传 const 引用。这个函数的时间复杂度是 O(作业数)因为哈希查找是 O(1)。如果作业数也很大可以反过来用学生做索引但一般作业数远小于学生数这样写够用。3.2 统计班级完成率别用嵌套循环硬算统计是实训里最容易写丑的地方。新手常见写法是三层循环遍历班级、遍历学生、遍历作业然后去 vector 里线性找提交记录。200 人 20 次作业就是 4000 次线性查找每次平均扫 2000 条总共 800 万次比较跑一次要好几秒。用哈希索引后同样规模降到 4000 次 O(1) 查找毫秒级完成。#include map std::mapstd::string, double classCompletionRate( const std::vectorStudent students, const std::vectorAssignment assignments, const std::unordered_maplong long, Submission index) { std::mapstd::string, std::pairint, int stat; // 班级 - (完成数, 总数) for (const auto s : students) { for (const auto a : assignments) { auto rec stat[s.className]; rec.second; long long key makeKey(s.id, a.id); auto it index.find(key); if (it ! index.end() it-second.completed) { rec.first; } } } std::mapstd::string, double rate; for (const auto kv : stat) { rate[kv.first] kv.second.second 0 ? 0.0 : static_castdouble(kv.first) / kv.second.second; } return rate; }逻辑说明外层遍历学生和作业内层用哈希查找整体 O(学生数 × 作业数)。stat 用 map 按班级聚合因为班级名是字符串map 自动排序方便输出。参数上返回 double 表示完成率0 到 1 之间。这里有个细节如果某班级没有学生分母为 0要单独处理否则除零会得到 nan输出时看着像 bug。提示如果学生数上万可以把班级也做成索引先按班级分组再统计但一般课程规模用不上。3.3 按完成率排序sort 配 lambda 的写法统计完往往要排名比如找出完成率最低的班级重点提醒。C 的std::sort配 lambda 是最顺手的#include algorithm #include vector std::vectorstd::pairstd::string, double rankClasses( const std::mapstd::string, double rate) { std::vectorstd::pairstd::string, double vec(rate.begin(), rate.end()); std::sort(vec.begin(), vec.end(), [](const auto a, const auto b) { return a.second b.second; // 完成率高的排前面 }); return vec; }逻辑说明map 不能直接按 value 排序先拷进 vector 再 sort。lambda 里a.second b.second是降序想升序改成。参数上 pair 的 first 是班级名second 是完成率。这个写法在「数据结构排序算法」里对应的是比较排序平均 O(n log n)班级数量少性能无压力。4. 避坑与排查这几个问题我全踩过4.1 组合键溢出导致查询结果错乱现象学号 20230101 的学生查出来完成情况是别人的。原因组合键用 int 存studentId * 1000超过 int 上限约 21 亿20230101 乘 1000 直接溢出成负数不同学生算出同一个键。解决组合键一律用 long long或者干脆用std::pairint,int配自定义哈希。我现在的习惯是只要涉及乘法组合键先算一下最大值会不会超。4.2 文件读写时中文路径乱码现象程序在 VS Code 里跑正常一读作业数据.txt就报文件打不开。原因Windows 下控制台默认 GBK源码文件是 UTF-8字符串字面量编码不一致。解决源文件保存为 UTF-8 with BOM或者在代码里用宽字符最省事的办法是文件名全用英文展示时再映射成中文。这个坑在「vscode配置c/c环境」时特别常见。4.3 vector 遍历时删除元素导致迭代器失效现象删除某个学生后程序崩溃或跳过下一个学生。原因在 range-for 里调用erase会让当前迭代器失效。解决用it vec.erase(it)的写法或者先标记再统一删除。我一般用std::remove_if配 erase一行搞定students.erase( std::remove_if(students.begin(), students.end(), [](const Student s) { return s.id targetId; }), students.end());4.4 统计完成率时把未提交当成未完成现象完成率算出来偏低。原因索引里只存了已提交的记录没提交的作业在哈希表里找不到代码里it end()时默认当成未完成但如果逻辑写反把找不到当成完成就会偏高。解决明确约定「找不到即未完成」并在查询函数里用it ! index.end() it-second.completed双重判断别偷懒只判断一个条件。4.5 大量数据下 unordered_map 退化现象数据量到几万条后查询突然变慢。原因哈希函数质量差或负载因子过高冲突链变长。解决插入前index.reserve(n)预留空间或者自定义哈希函数。默认哈希对 long long 一般够用但 reserve 能明显减少 rehash 次数。5. 进阶技巧把程序改成能持久化和可测试的形态5.1 用文本文件做持久化格式要能容错程序跑完数据不能丢最简单的持久化是写文本文件。我一般用 CSV 风格每行一条记录字段用逗号分隔#include fstream #include sstream void saveSubmissions(const std::string path, const std::unordered_maplong long, Submission index) { std::ofstream out(path); if (!out) { std::cerr 无法写入 path \n; return; } for (const auto kv : index) { const auto s kv.second; out s.studentId , s.assignmentId , s.completed , s.submitTime \n; } } void loadSubmissions(const std::string path, std::unordered_maplong long, Submission index) { std::ifstream in(path); if (!in) return; // 文件不存在时静默跳过首次运行正常 std::string line; while (std::getline(in, line)) { std::istringstream iss(line); Submission s; char comma; if (iss s.studentId comma s.assignmentId comma s.completed comma s.submitTime) { index[makeKey(s.studentId, s.assignmentId)] s; } } }逻辑说明保存时遍历哈希表逐行写加载时逐行解析用 istringstream 按逗号切分。参数上 completed 是 bool流操作会写成 0 或 1读回来也正确。容错点在于加载时如果某行格式不对iss 会失败跳过该行而不是崩溃。这个设计让程序第一次运行没有数据文件也能正常启动。5.2 用断言和边界用例验证统计逻辑写完统计别急着交先造几组边界数据验证。我习惯写一个简单的自检函数#include cassert void selfTest() { std::vectorStudent students { {1, 张三, 一班}, {2, 李四, 一班}, {3, 王五, 二班} }; std::vectorAssignment assignments {{101, 作业一, 0}, {102, 作业二, 0}}; std::unordered_maplong long, Submission index; index[makeKey(1, 101)] {1, 101, true, 0}; index[makeKey(1, 102)] {1, 102, true, 0}; index[makeKey(2, 101)] {2, 101, true, 0}; auto rate classCompletionRate(students, assignments, index); assert(rate[一班] 0.74 rate[一班] 0.76); // 3/4 0.75 assert(rate[二班] 0.0); }逻辑说明一班两个学生共 4 条作业记录完成 3 条完成率 0.75二班一个学生 2 条全未完成0.0。用 assert 卡住范围而不是精确相等是因为浮点比较有误差。这个自检跑一遍统计逻辑对不对立刻见分晓比手动核对快得多。5.3 性能对比不同容器在万级数据下的实测差异我拿 5000 学生、20 次作业、10 万条提交记录做过一次对比同一台机器上存储方式单次查询耗时全量统计耗时vector 线性查找约 8 ms约 12 sunordered_map 索引约 0.02 ms约 30 msmap红黑树索引约 0.05 ms约 80 ms差距在统计场景下是几百倍。这也是为什么我一直强调别用嵌套循环硬算。unordered_map 比 map 快是因为哈希 O(1) 对树 O(log n)但 map 有序如果统计结果需要按 key 排序输出map 省一次排序。选哪个看你的输出需求。最后说个我自己的习惯每次写完一个数据结构实训我都会把核心容器的操作单独抽出来写几行测试确认增删改查的边界都对再往上叠业务逻辑。这个程序我前后改过三版第一版用 vector 硬扫第二版加哈希索引第三版才把持久化和自检补上。真正让代码从「能跑」到「敢交」的不是功能多而是你知道它在数据量涨十倍时哪里会先崩。希望帮到你。本文还有配套的精品资源点击获取
返回列表