
简介这份运动会分数统计实验报告面向数据结构与软件设计课程的初学者帮助读者理解如何用线性链表、结构体和模块化函数解决实际统计问题。资源包共1个docx文件约91KB内容为完整的实验报告文档涵盖实验目的、要求、数据定义、函数设计、测试结果与调试分析。报告以学院编号1n、男子项目m个、女子项目w个为背景详细讲解InitSCH初始化、input输入、mark算分、output输出四个核心函数的实现思路并给出str_int字符串转数字的辅助函数与完整C源代码。读者可从中掌握结构体封装、指针操作、递归处理错误输入、条件判断与循环设计等关键技能同时获得边界值测试用例n4,m3,w2和指针使用注意事项。目前已有390人学习适合需要完成同类课程设计或巩固链表应用的学习者参考。1. 运动会分数统计一份被低估的结构体与线性表实战样本如果你正在翻数据结构实验报告大概率会看到“运动会分数统计”这个题目。它听起来像教务处的活儿但真正拆进去会发现这是一份把结构体、线性表、分支结构、输入输出、错误处理全串起来的经典样本。项目正文里给出的是一份完整的 C 实现包含运动员信息录入、名次得分换算、学院团体总分统计和报表输出测试规模设定为 n4、m3、w2同时预留了 n≤20、m≤30、w≤20 的边界。它适合两类人一类是刚学完结构体、需要一份能跑通的完整案例来理解“数据怎么组织、分数怎么算”的新手另一类是回头复习线性表操作、想看看别人怎么写健壮性判断的熟手。这份资源的核心价值不在算法多高深而在于它把“输入—校验—算分—输出”这条链路完整地落到了代码里而且错误处理部分写得比多数实验报告都细。2. 数据建模结构体怎么定义才不给自己挖坑2.1 运动员与学院两张线性表的字段设计项目正文里定义了两个结构体student和college。前者描述运动员字段包括参赛项目号、姓名、性别、学院编号、名次、得分以及一个last用来记录当前表中最后一个元素的下标后者描述学院字段包括学院编号、女子团体总分、男子团体总分、学院总成绩同样带last。这种设计本质上是用数组模拟线性表而不是真正的链表。为什么不用链表因为实验要求里明确说了“按名次顺序输入”数据量有上限MAXSIZE100数组的随机访问反而更方便后续按学院遍历累加。常见做法是先确定最大容量再用一个整型游标last标记有效元素范围初始化时把last置为 -1 或 n-1后续所有循环都围绕last展开。这里有一个容易被忽略的点student里的studentname在正文第一版定义中是char thletename[MAXSIZE][20]到了附录源码里改成了string studentname[MAXSIZE]。这个改动很关键。用char二维数组时姓名长度被硬限制在 20 个字符以内而且输入时要用cin a2再逐字符拷贝换成string之后长度由标准库管理赋值直接写S1-studentname[i] a2代码简洁很多。如果你要复现建议直接采用string版本除非你的编译器或作业要求必须用 C 风格字符串。2.2 初始化函数 InitSCH 的参数与调用时机InitSCH接收一个college指针和学院数量 n把last设为 n-1然后循环把每个学院的编号设为 i1三个总分字段全部清零。这个函数必须在输入任何运动员数据之前调用否则团体总分累加时会读到未初始化的垃圾值。正文里有一句调试分析写得很实在“线性表 School 的各属性在用之前需要初始化。”这不是废话我见过太多人直接声明一个college S2;就开始调mark结果输出里团体总分是随机数。调用顺序在main里是固定的先读 n、m、w再InitSCH(S2, n)再input(S1, n, m, w)再mark(S1, S2, n, m, w)最后output(S1, S2)。这个顺序不能乱因为input只负责填运动员表mark才根据运动员表去更新学院表。如果你把mark提前学院表里还没有运动员数据累加结果全是零。2.3 用数组模拟线性表的边界处理MAXSIZE定义为 100input函数里有一句判断if(S1-last MAXSIZE-1)就提示“运动员人数已满无法继续输入”。注意这里比较的是last和MAXSIZE-1因为last是下标最大有效下标是 99。循环内部每输入一条记录i之后立刻S1-last i保证last始终指向最后一个有效元素。这种写法比“先全部输入再统一设置 last”更安全因为中途如果用户按#结束last已经是对的。提示如果你把 MAXSIZE 改大比如改成 1000记得同时检查input里那个i 100-1的硬编码判断。正文源码里有一处写的是if(i 100-1)而不是if(i MAXSIZE-1)这是一个隐藏的耦合点改容量时容易漏掉。3. 输入与校验递归重输和性别项目匹配的实战写法3.1 input 函数的参数传递与指针必要性input的签名是int input(student *S1, int n, int m, int w)。第一个参数必须是指针因为函数内部要修改S1-item[i]、S1-studentname[i]等字段如果传值所有修改都留在函数栈里回到main后运动员表还是空的。正文调试分析里专门写了一句“输入函数里传参时必须用指针引用类型因为需要带回数据。”这是血泪经验不是理论复述。n、m、w 三个参数用来做范围校验学院号不能超过 n项目号要在 1 到 mw 之间名次要在 1 到 n 之间。注意名次的上限用的是 n而不是 5。正文里写的是if(!(S1-num[i]0 S1-num[i]n))这其实有点奇怪——名次应该最多到 5因为只有前五名有分。但原文确实这么写了可能是把“参赛学院数”和“名次上限”混用了。如果你要修正可以改成5但那样就偏离了原始实验报告的设定。我的建议是保留原逻辑但在自己的代码里加注释说明这个边界可以收紧到 5。3.2 性别与项目号的匹配校验这是整个输入校验里最容易翻车的地方。比赛分男子项目编号 1m和女子项目编号 m1mw所以一个运动员的性别必须和项目号匹配男子项目只能有男性sex b女子项目只能有女性sex g。正文里的判断逻辑是if(S1-item[i] 0 S1-item[i] m S1-sex[i] ! b) { cout 您的输入有误项目号与性别不匹配请重新输入该项目 endl; goto a; } if(S1-item[i] m1 S1-item[i] mw S1-sex[i] ! g) { cout 您的输入有误项目号与性别不匹配请重新输入该项目 endl; goto a; }第一段判断项目号在男子项目范围内但性别不是b报错。第二段判断项目号在女子项目范围内但性别不是g报错。逻辑是对的但有一个边界漏洞如果项目号是 0 或者大于 mw这两个 if 都不会命中程序会继续往下走。正文里前面已经有一个if(S1-item[i] 0)的判断但那只拦住了 0没拦住大于 mw 的情况。常见做法是再加一条if(S1-item[i] 1 || S1-item[i] mw)就报错重输。3.3 用 goto 实现递归重输的利弊正文里用goto a跳回输入起点实现“检测到错误就重新输入整条记录”。调试分析里说“要在输入函数里再调输入函数用递归调用的方法实现重新输入”但源码实际用的是goto不是递归。goto在这里的好处是不会增加栈深度坏处是代码可读性差而且如果跳转标签和变量作用域处理不好容易出玄学问题。如果你要改成递归可以写一个inputOne函数专门负责单条记录的输入和校验校验失败就return inputOne(...)但要注意递归层数太深可能导致栈溢出。对于实验规模最多 100 条两种方式都能跑选你能讲清楚原理的那种。3.4 字符串转数字的 str_int 函数源码里有一个str_int函数把char*或string的首地址传进去逐字符判断是否为数字然后累加。这个函数的存在是因为输入时用了string a1接收项目号但结构体里item是int需要转换。正文里str_int的实现是int str_int(char *str) { int num, i; num *str - 0; i 1; while (str[i] isdigit(str[i])) num num * 10 str[i] - 0; return num; }逻辑说明先取第一个字符转成数字然后从第二个字符开始只要还是数字就继续乘 10 累加。参数说明str是 C 风格字符串指针调用时用a1[0]把string的首字符地址传进去。注意这个函数不处理负数也不处理空字符串如果输入的第一个字符不是数字num会得到一个负值或乱值。所以调用之前必须先做“首字符是否为数字”的校验正文里确实也做了。4. 算分与报表名次映射、团体累加和输出格式4.1 mark 函数的两层循环与得分规则mark函数做两件事第一层循环遍历所有运动员根据名次给point赋值第二层循环遍历所有学院再遍历所有运动员把同一学院的得分按性别累加到Tboy或Tgirl最后算Tall。得分规则是固定的第 1 名 7 分第 2 名 5 分第 3 名 3 分第 4 名 2 分第 5 名 1 分第 5 名之后 0 分。正文里用了一串if而不是switch效果一样但如果你要扩展名次范围switch更清晰。void mark(student *L1, college *L2, int n, int m, int w) { int i, j; for (i 0; i L1-last 1; i) { if (L1-num[i] 1) L1-point[i] 7; if (L1-num[i] 2) L1-point[i] 5; if (L1-num[i] 3) L1-point[i] 3; if (L1-num[i] 4) L1-point[i] 2; if (L1-num[i] 5) L1-point[i] 1; if (L1-num[i] 5) L1-point[i] 0; } for (i 0; i L2-last 1; i) { for (j 0; j L1-last 1; j) { if (L2-collegenum[i] L1-collegenum[j]) { if (L1-sex[j] b) { L2-Tboy[i] L1-point[j]; } if (L1-sex[j] g) { L2-Tgirl[i] L1-point[j]; } } } L2-Tall[i] L2-Tgirl[i] L2-Tboy[i]; } }参数说明L1是运动员表指针L2是学院表指针n、m、w 在函数体内其实没用到可以删掉但保留也不影响。逻辑说明外层学院循环里每处理完一个学院就立刻算Tall这样不需要单独再遍历一次。注意Tboy和Tgirl在InitSCH里已经清零所以这里可以直接。4.2 output 函数的报表格式与对齐问题output接收的是值传递的student L1和college L2不是指针。这意味着如果表很大拷贝开销会很明显。对于 MAXSIZE100 的实验规模值传递能跑但更好的做法是传const引用。输出格式上正文用了\t做列分隔每个学院先输出该学院所有运动员的明细再输出女子团体总分、男子团体总分、全体总分。这里有一个小 bug源码里有一行coutcoutendl;第i1号学院的成绩endl;多了一个coutendl;被包在字符串里实际运行时会原样打印出来。这是复制粘贴时留下的痕迹复现时记得删掉。注意如果你用string存姓名输出时cout L1.studentname[j]会按字符串输出如果用char[20]同样可以cout但遇到没有\0结尾的数组会越界。建议统一用string。4.3 测试用例 n4, m3, w2 的输入输出对照正文建议用 n4、m3、w2 做测试。这意味着学院编号 14男子项目 13女子项目 45。你可以构造一组数据学院 1 的张三参加项目 1 拿第 1 名得 7 分计入男团学院 1 的李四参加项目 4 拿第 2 名得 5 分计入女团学院 2 的王五参加项目 2 拿第 3 名得 3 分计入男团。输入时注意性别和项目号匹配项目 13 必须输b项目 45 必须输g。输出时检查学院 1 的男团是否为 7、女团是否为 5、总分为 12。如果对不上优先检查mark里的学院编号匹配条件是否写成了而不是这是 C 里最经典的翻车点。5. 避坑与排查五条血泪经验5.1 现象团体总分全是零。原因InitSCH 没调用或调用顺序错了。解决确保在 input 之前调用 InitSCH并且传入的 n 和后续循环用的 n 一致。5.2 现象输入正确数据却提示“项目号与性别不匹配”。原因性别字符用了大写B或G而判断条件只认小写b和g。解决在输入后统一转小写或者把判断条件改成toupper比较。5.3 现象程序在输入阶段直接崩溃。原因str_int收到空字符串或首字符非数字导致num为负值后续数组下标越界。解决在调用str_int之前先判断a1[0]是否为数字正文里已经有这个判断但如果你自己改代码容易漏掉。5.4 现象输出报表里某个学院的运动员明细重复出现。原因output里两层循环的学院编号匹配条件写反了或者L2.collegenum[i]和L1.collegenum[j]用了赋值。解决检查所有比较是否用了这是 C 里最隐蔽的坑之一。5.5 现象名次大于 5 的运动员得分不是 0。原因mark里只写了if(L1-num[i] 5) L1-point[i] 0;但如果名次是负数或者 0这个条件不命中point会保留上一次的垃圾值。解决在mark开头先把所有point清零或者把条件改成else分支。6. 进阶技巧把数组模拟线性表改成真正的链表如果你已经跑通了数组版本想进一步练手可以把student和college改成真正的链表节点。做法是每个节点包含数据域和next指针插入时动态new节点遍历时用while (p ! nullptr)。好处是容量不再受 MAXSIZE 限制坏处是错误处理更复杂因为你要管理内存释放。我一般会先写一个insertStudent函数接收头指针的指针student** head在尾部插入新节点。算分时遍历链表累加逻辑和数组版本一样。输出时同样遍历链表但要注意链表节点不是连续存储不能再用下标访问。验证方法用同一组测试数据分别跑数组版和链表版对比输出报表是否完全一致。如果一致说明你的链表插入和遍历逻辑是对的。如果不一致优先检查插入时有没有更新尾指针以及遍历时有没有漏掉最后一个节点。struct StudentNode { int item; string name; char sex; int collegenum; int num; int point; StudentNode* next; }; void insertStudent(StudentNode** head, StudentNode** tail, StudentNode* newNode) { newNode-next nullptr; if (*head nullptr) { *head newNode; *tail newNode; } else { (*tail)-next newNode; *tail newNode; } }参数说明head和tail都是二级指针因为插入时要修改头指针或尾指针本身。newNode是已经填好数据的节点指针。逻辑说明如果链表为空新节点既是头也是尾否则把尾节点的next指向新节点再更新尾指针。这个写法比每次从头遍历找尾部要高效得多。从那以后我每次写线性表相关的实验都会先把初始化、插入、遍历、释放这四个环节单独测一遍再串起来跑完整流程。希望帮到你。本文还有配套的精品资源点击获取