简介:这份运动会分数统计实验报告面向数据结构与软件设计课程的初学者,帮助读者理解如何用线性链表、结构体和模块化函数解决实际统计问题。资源包共1个docx文件,约91KB,内容为完整的实验报告文档,涵盖实验目的、要求、数据定义、函数设计、测试结果与调试分析。报告以学院编号1~n、男子项目m个、女子项目w个为背景,详细讲解InitSCH初始化、input输入、mark算分、output输出四个核心函数的实现思路,并给出str_int字符串转数字的辅助函数与完整C++源代码。读者可从中掌握结构体封装、指针操作、递归处理错误输入、条件判断与循环设计等关键技能,同时获得边界值测试用例(n=4,m=3,w=2)和指针使用注意事项。目前已有390人学习,适合需要完成同类课程设计或巩固链表应用的学习者参考。
1. 运动会分数统计:一份被低估的结构体与线性表实战样本
如果你正在翻数据结构实验报告,大概率会看到“运动会分数统计”这个题目。它听起来像教务处的活儿,但真正拆进去会发现,这是一份把结构体、线性表、分支结构、输入输出、错误处理全串起来的经典样本。项目正文里给出的是一份完整的 C++ 实现,包含运动员信息录入、名次得分换算、学院团体总分统计和报表输出,测试规模设定为 n=4、m=3、w=2,同时预留了 n≤20、m≤30、w≤20 的边界。它适合两类人:一类是刚学完结构体、需要一份能跑通的完整案例来理解“数据怎么组织、分数怎么算”的新手;另一类是回头复习线性表操作、想看看别人怎么写健壮性判断的熟手。这份资源的核心价值不在算法多高深,而在于它把“输入—校验—算分—输出”这条链路完整地落到了代码里,而且错误处理部分写得比多数实验报告都细。
2. 数据建模:结构体怎么定义才不给自己挖坑
2.1 运动员与学院两张线性表的字段设计
项目正文里定义了两个结构体:student和college。前者描述运动员,字段包括参赛项目号、姓名、性别、学院编号、名次、得分,以及一个last用来记录当前表中最后一个元素的下标;后者描述学院,字段包括学院编号、女子团体总分、男子团体总分、学院总成绩,同样带last。这种设计本质上是用数组模拟线性表,而不是真正的链表。为什么不用链表?因为实验要求里明确说了“按名次顺序输入”,数据量有上限(MAXSIZE=100),数组的随机访问反而更方便后续按学院遍历累加。常见做法是:先确定最大容量,再用一个整型游标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,然后循环把每个学院的编号设为 i+1,三个总分字段全部清零。这个函数必须在输入任何运动员数据之前调用,否则团体总分累加时会读到未初始化的垃圾值。正文里有一句调试分析写得很实在:“线性表 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定义为 100,input函数里有一句判断: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 到 m+w 之间,名次要在 1 到 n 之间。注意名次的上限用的是 n,而不是 5。正文里写的是if(!(S1->num[i]>0 && S1->num[i]<=n)),这其实有点奇怪——名次应该最多到 5,因为只有前五名有分。但原文确实这么写了,可能是把“参赛学院数”和“名次上限”混用了。如果你要修正,可以改成<=5,但那样就偏离了原始实验报告的设定。我的建议是:保留原逻辑,但在自己的代码里加注释说明这个边界可以收紧到 5。
3.2 性别与项目号的匹配校验
这是整个输入校验里最容易翻车的地方。比赛分男子项目(编号 1~m)和女子项目(编号 m+1~m+w),所以一个运动员的性别必须和项目号匹配:男子项目只能有男性(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] >= m+1 && S1->item[i] <= m+w && S1->sex[i] != 'g') { cout << "您的输入有误,项目号与性别不匹配,请重新输入该项目!" << endl; goto a; }第一段判断:项目号在男子项目范围内,但性别不是b,报错。第二段判断:项目号在女子项目范围内,但性别不是g,报错。逻辑是对的,但有一个边界漏洞:如果项目号是 0 或者大于 m+w,这两个 if 都不会命中,程序会继续往下走。正文里前面已经有一个if(S1->item[i] == 0)的判断,但那只拦住了 0,没拦住大于 m+w 的情况。常见做法是再加一条:if(S1->item[i] < 1 || S1->item[i] > m+w)就报错重输。
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,不是指针。这意味着如果表很大,拷贝开销会很明显。对于 MAXSIZE=100 的实验规模,值传递能跑,但更好的做法是传const引用。输出格式上,正文用了'\t'做列分隔,每个学院先输出该学院所有运动员的明细,再输出女子团体总分、男子团体总分、全体总分。这里有一个小 bug:源码里有一行cout<<"cout<<endl;第"<<i+1<<"号学院的成绩"<<endl;,多了一个cout<<endl;被包在字符串里,实际运行时会原样打印出来。这是复制粘贴时留下的痕迹,复现时记得删掉。
注意:如果你用
string存姓名,输出时cout << L1.studentname[j]会按字符串输出;如果用char[20],同样可以cout,但遇到没有\0结尾的数组会越界。建议统一用string。
4.3 测试用例 n=4, m=3, w=2 的输入输出对照
正文建议用 n=4、m=3、w=2 做测试。这意味着学院编号 1~4,男子项目 1~3,女子项目 4~5。你可以构造一组数据:学院 1 的张三参加项目 1 拿第 1 名,得 7 分,计入男团;学院 1 的李四参加项目 4 拿第 2 名,得 5 分,计入女团;学院 2 的王五参加项目 2 拿第 3 名,得 3 分,计入男团。输入时注意性别和项目号匹配:项目 1~3 必须输b,项目 4~5 必须输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指向新节点,再更新尾指针。这个写法比每次从头遍历找尾部要高效得多。
从那以后我每次写线性表相关的实验,都会先把初始化、插入、遍历、释放这四个环节单独测一遍,再串起来跑完整流程。希望帮到你。
本文还有配套的精品资源,点击获取