ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言实现运动会分数统计:链表与排序的课设全解析

2026/9/19 23:47:30 拓冰建站 浏览量
C语言实现运动会分数统计:链表与排序的课设全解析 简介这是一份数据结构课程设计《运动会分数统计系统》C语言版的完整设计文档面向计算机相关专业学生适合用于课程设计参考、实验报告撰写或期末复习。文档从需求分析、功能模块划分、数据要求与性能要求出发详细叙述系统开发工具选择、结构体与数组链表设计、用户界面及输入输出处理并重点讲解了快速排序与冒泡排序在成绩排名中的应用。正文还包括程序测试与调试、系统性能分析、性能瓶颈识别与改进措施以及项目总结与未来展望能帮助读者理解从设计到上线的完整流程和常见排错思路。资源包内含1个docx文档大小仅52KB但目录结构完整章节划分清晰从绪论到参考文献一应俱全可直接作为课程设计文档模板使用。已有221人在CSDN学习下载。1. 一个运动会分数统计系统为什么值得拆开看数据结构课程设计里运动会分数统计是出现频率最高的题目之一但多数提交版本只是把菜单和 printf 拼在一起能跑通演示就交差。这份基于 C 语言实现的课程设计文档表面是「n 个学校、m 个男子项目、w 个女子项目、前三名按 5/3/2 计分」的简单需求实际上把结构体数组、链表、排序、文件持久化和菜单驱动程序设计全串起来了。对正在做课设的本科生来说它有可直接复现的代码骨架对已经工作的开发者反而值得看看在链表和数组之间做取舍时C 语言课程里那些「时间复杂度」结论在真实小规模数据下是怎么被打破的。本篇按数据建模、模块实现、排序选型、文件读写和调试五个层面拆解这份源码每一步都给出可运行的代码和参数说明。2. 需求拆解与存储建模链表选型背后的数据结构权衡2.1 题目约束与数据流分析先明确输入边界n 10 个学校m 和 w 分别表示男子、女子项目数且 m、w 20项目编号规则是男子 1..m、女子 m1..mw。输入数据是「哪个学校在哪个项目拿了第几名」输出需求则拆成八个功能点录入成绩、统计总分、按编号排序、按总分排序、按男团总分排序、按女团总分排序、按学校编号查项目、按项目编号查名次学校。整个系统的数据流可以简化为两条链一条从「项目」出发指向在该项目上获奖的学校链表另一条从「学校」出发指向该学校所有获奖项目的链表。这两条链共享同一份录入数据但组织方向相反正好对应查询的两种维度。数据结构课程里常说的「以空间换时间」「双向索引」在这份代码里就是以两个全局结构体指针g1、g2的形式落地的。2.2 双链表结构项目表与学校成绩表的组织方式代码里定义了ALLitems和ALLNode两个顶层结构体分别挂项目链表和学校链表typedef struct Schools { int school; // 学校编号取值 1..n int record; // 该项目下该学校获得的分数 struct Schools *next; // 指向下一个获奖学校节点 } Schools; typedef struct ALLitems { int item; // 项目编号男子 1..m女子 m1..mw Schools *firstschool; // 指向该项目获奖学校链表的头指针 } ALLitems; typedef struct Items { int item; // 项目编号 int record; // 得分 struct Items *next; // 指向下一个获奖项目节点 } Items; typedef struct school { int school; // 学校编号 int score; // 学校总分 int boys; // 男团体总分 int girls; // 女团体总分 Items *firstitem; // 指向该校获奖项目链表的头指针 } school; typedef struct ALLNode { int n; // 学校总数 school b[11]; // 学校数组下标从 1 开始 } ALLNode;这里的核心设计是「一个录入动作两条链表同步更新」。g1以项目为入口g1-a[i].firstschool指向项目 i 的获奖学校链表g2以学校为入口g2-b[x].firstitem指向学校 x 的获奖项目链表。两边的节点在录入时分别用malloc分配并用头插法挂到链表头部。选链表的理由是数据规模不确定且插入频繁。题目允许前五名计分最多 n10、mw40 个项目最坏情况下节点数不超过 40*5200 个。数组在插入时需要移动元素链表插入是 O(1)在头插法场景下优势明显。但代价也很直接查询某个学校的所有获奖项目必须从头遍历链表时间复杂度 O(k)k 是该校获奖项目数。m、w20 的约束让这个 O(k) 完全可接受这也是为什么这份课设能用链表而不翻车的原因。2.3 结构体数组与链表的混合使用边界注意ALLNode里学校用了定长数组b[11]而不是链表。n10 是题目写死的约束定长数组在初始化时直接把 11 个元素置零省去了链表节点分配和释放的麻烦。这里的选型逻辑值得记住确定性小规模数据用数组动态增长数据用链表。对照知识点ALLitems本身也是定长数组a[]项目编号作为数组下标直接寻址。这意味着查项目 i 的获奖学校时不需要遍历项目表直接g1-a[i].firstschool就能拿到链表头。这是「数组随机访问 O(1)」特性的典型应用与链表节点内线性查找形成互补。初始化部分的完整逻辑如下g1 (ALLitems *)malloc(sizeof(ALLitems)); g2 (ALLNode *)malloc(sizeof(ALLNode)); if (!g2 || !g1) exit(1); for (k 1; k m w; k) { g1-a[k].item k; g1-a[k].firstschool NULL; } for (k 1; k n; k) { g2-b[k].school k; g2-b[k].firstitem NULL; g2-b[k].score 0; g2-b[k].boys 0; g2-b[k].girls 0; }g1-a[k].item k其实可以省略因为数组下标本身就是项目编号但保留赋值有利于调试时直接观察结构体内容。g2 的b[0]元素在后续排序中会被用作临时交换变量这也是 C 语言课设里常见的「浪费一个数组位置换代码简洁」的做法。2.4 为什么不建议把链表换成纯顺序表如果完全用结构体数组存学校和项目的关系例如score[n1][mw1]的二维积分矩阵代码会更短但会丢失「项目链」这个语义。链表版本里g1-a[i].firstschool天然表达了「项目 i 有哪些学校获奖」遍历时能看到名次顺序。而二维数组只能表达「学校 x 在项目 i 得了多少分」查某个项目的前三名需要额外比较反而绕。3. 成绩录入与统计模块积分映射与链表同步更新的实现细节3.1 前三名与前五名的积分规则映射题目基础规则是前三名积分 5、3、2扩展规则是前五名积分 7、5、3、2、1。注意前五名规则下第一名的 7 分不是 52 拼出来的而是一套独立积分表。录入时先让用户选择「前三名」还是「前五名」再按名次 h 映射记录分数名次前三名积分前五名积分1572353234-25-1代码里用if (h3) p2-record2这种逐名次判断虽然啰嗦但直观不会出现算错名次对应关系的问题。若想更精简可以用数组下标映射score3[4] {0,5,3,2}和score5[6] {0,7,5,3,2,1}按下标取分。两种写法本人都见过课设场景下逐 if 判断反而便于答辩时讲解。3.2 录入函数 funct1 的完整实现录入是八个功能里最核心的一段因为链表节点、总分累计、男女团体总分拆分全在这一步完成void funct1(ALLitems *g1, ALLNode *g2) { int i, j, x, h, m, w, n; Schools *p1; Items *p2; printf(输入男子项目总数 m); scanf(%d, m); printf(输入女子项目总数 w); scanf(%d, w); printf(输入参加运动会的学校总数 n); scanf(%d, n); // 初始化两个结构体数组 for (i 1; i m w; i) { g1-a[i].item i; g1-a[i].firstschool NULL; } for (i 1; i n; i) { g2-b[i].school i; g2-b[i].firstitem NULL; g2-b[i].score 0; g2-b[i].boys 0; g2-b[i].girls 0; } while (1) { printf(\n输入项目编号0 返回主菜单); scanf(%d, i); if (i 0) break; if (i 1 || i m w) { printf(项目编号超出范围请重新输入。\n); continue; } printf(该项目取前几名 1.前三名 2.前五名\n); scanf(%d, j); if (j ! 1 j ! 2) { printf(输入有误请重新选择。\n); scanf(%d, j); } h (j 1) ? 3 : 5; while (h 0) { printf(第 %d 名学校编号输入 0 结束当前项目, h); scanf(%d, x); if (x 0) break; if (x 1 || x n) { printf(学校编号超出范围请重新输入。\n); continue; } // 分配节点并计算分数 p1 (Schools *)malloc(sizeof(Schools)); p2 (Items *)malloc(sizeof(Items)); p1-school x; p2-item i; if (j 1) { if (h 3) p2-record 2; if (h 2) p2-record 3; if (h 1) p2-record 5; } else { if (h 5) p2-record 1; if (h 4) p2-record 2; if (h 3) p2-record 3; if (h 2) p2-record 5; if (h 1) p2-record 7; } p1-record p2-record; // 头插法挂入项目链表 p1-next g1-a[i].firstschool; g1-a[i].firstschool p1; // 头插法挂入学校链表 p2-next g2-b[x].firstitem; g2-b[x].firstitem p2; // 累计总分与男女团体总分 g2-b[x].score p2-record; if (i m) g2-b[x].boys p2-record; else g2-b[x].girls p2-record; h--; } } }这段代码的关键在p1-record p2-record把同一个得分同时写进两个方向相反的链表节点。如果漏掉这行项目链表的节点 record 会是未初始化值按项目查询时输出错分。i m的判断用项目编号区分男女项目这与题目「项目编号为男子 1..m女子 m1..mw」的约定严格对应不能改成i m。3.3 录入过程中的异常输入处理原始版本对x0的处理是提前结束当前项目的名次录入但对编号越界只提示不重新录入。实际跑的时候如果输入了一个不存在的学校编号 99g2-b[99]会直接越界访问破坏堆内存。所以上面的实现加了范围检查这是课设答辩时被问「鲁棒性」最常见的切入点。另一个隐患是头插法导致链表顺序和名次顺序相反。录入顺序是第 3 名、第 2 名、第 1 名头插后链表头是第 1 名输出时从头遍历得到的是 1、2、3 的正确名次顺序。如果改成尾插法输出就要反过来。这个细节可以在答辩时主动讲能加分。3.4 统计与输出模块的遍历方式funct2统计各学校总分最简单直接遍历g2-b[k]打印 score 字段O(n) 时间。funct3按学校编号输出时需要遍历每个学校的项目链表void funct3(ALLNode *g2) { int k; Items *p2; for (k 1; k g2-n; k) { printf(学校 %d, k); p2 g2-b[k].firstitem; while (p2 ! NULL) { printf(项目 %d 得 %d 分, p2-item, p2-record); p2 p2-next; } printf(\n); } }这里while (p2 ! NULL)的遍历模式贯穿整个系统。链表遍历的终止条件一定是判断指针是否为 NULL而不是计数。初学 C 语言链表时容易写成while (p2-next ! NULL)导致最后一个节点被漏掉本系统所有查询模块统一用前者也是课设代码里值得对照检查的点。4. 排序输出与查询模块插入排序、快速排序与双向链式检索4.1 八种输出顺序背后的排序需求录入完成后的输出模块分成五类按学校编号、按学校总分、按男团体总分、按女团体总分、按项目编号查学校。其中按总分、男团、女团三种输出都需要排序。原始代码在funct4、funct5、funct6里用的是直接插入排序排序时把b[0]当作哨兵位置交换整个结构体里的 score、boys、girls、school 四个字段。4.2 直接插入排序的 C 语言实现与哨兵优化以按学校总分排序为例核心排序代码如下void funct4(ALLNode *g2) { int i, j, k; Items *p2; // 辅助输出每个学校的总分 for (i 2; i g2-n; i) { g2-b[0].score g2-b[i].score; g2-b[0].boys g2-b[i].boys; g2-b[0].girls g2-b[i].girls; g2-b[0].school g2-b[i].school; j i - 1; while (g2-b[0].score g2-b[j].score j 0) { g2-b[j 1].score g2-b[j].score; g2-b[j 1].boys g2-b[j].boys; g2-b[j 1].girls g2-b[j].girls; g2-b[j 1].school g2-b[j].school; j--; } g2-b[j 1].score g2-b[0].score; g2-b[j 1].boys g2-b[0].boys; g2-b[j 1].girls g2-b[0].girls; g2-b[j 1].school g2-b[0].school; } for (k 1; k g2-n; k) printf(学校 %d 总分 %d\n, g2-b[k].school, g2-b[k].score); }哨兵b[0]在这里起到两个作用暂存当前待插入元素以及作为 while 循环的越界判断。j 0条件保证 j 递减到 0 时循环停止不会访问b[-1]。这段代码是升序还是降序取决于 while 里的大于号还是小于号。原课件里用的得到升序本文改为得到降序符合「按总分排序输出」通常期望的高分在前。排序时交换的是四个字段而不是整个结构体原因是 C 语言里结构体直接赋值在早期编译器上可能存在兼容问题逐字段赋值更保险。现在的 GCC 和 Code::Blocks 自带编译器都支持结构体整体赋值g2-b[j1] g2-b[j]一行能替代四行课设代码不必照抄逐字段风格但要知道老代码为什么这么写。4.3 快速排序与冒泡排序的选型对比摘要部分提到了快速排序算法和冒泡排序算法但源码里实际用的是直接插入排序。三种排序在 n10 的数据规模下性能差异完全无感选型更应关注代码可解释性排序算法平均时间复杂度最坏时间复杂度空间代码量适合场景冒泡排序O(n^2)O(n^2)O(1)最少教学演示直接插入排序O(n^2)O(n^2)O(1)少n 很小或基本有序快速排序O(n log n)O(n^2)O(log n)多大规模乱序数据直接插入排序在 n10 时比较次数最多 45 次快速排序的递归调用开销反而可能更大。数据结构课程里讲「快排是工程首选」是指大规模数据下的平均表现课设答辩如果被问到「为什么不用快排」可以从数据规模、代码稳定性、哨兵优化三个角度解释。如果要把排序改成快排需要一个额外的递归函数和分区函数交换结构体数组元素时同样要处理四个字段。但注意ALLNode里的b数组下标从 1 开始快排的分区函数需要适配这个偏移left从 1 传入、right取g2-n交换时用临时结构体变量中转即可。改动本身不难但插入排序已经够用没必要引入额外的递归栈风险。4.4 双向查询按学校查项目与按项目查学校funct7按学校编号查询项目情况funct8按项目编号查询取得名次的学校这两段正好验证 2.2 节说的双链表设计。void funct7(ALLNode *g2) { int school_id, item_id; Items *p2; printf(输入要查询的学校编号); scanf(%d, school_id); printf(输入要查询的项目编号); scanf(%d, item_id); p2 g2-b[school_id].firstitem; while (p2 ! NULL) { if (p2-item item_id) printf(学校 %d 在项目 %d 得 %d 分\n, school_id, p2-item, p2-record); p2 p2-next; } }这里只能遍历学校链逐个比较项目编号是否匹配因为没有「学校 x 项目 i」的直接索引。如果想 O(1) 查询需要维护一个score[n1][mw1]的积分矩阵但会增加约 11*40440 个 int 的存储。在 n10 的约束下遍历链表的最坏情况是 20 个项目节点耗时可以忽略。这就是「空间换时间」需要结合实际规模判断的典型例子。funct8按项目编号查学校则是走g1-a[i].firstschool链表找到项目 i 后直接遍历输出学校编号和得分。因为录入时用了头插法链表头是第 1 名自然得到名次从高到低的顺序。这里注意原代码给这个函数传的是ALLitems *g1而不是ALLNode *g2如果传错参数类型编译器会报警告但用void*强转的程序也能编译通过运行时才会崩溃。5. 文件持久化与调试收尾从能跑到跑得稳5.1 二进制文件存储与 fwrite 的坑save()函数把两个全局结构体直接以二进制方式写入文件void save() { FILE *fp; if ((fp fopen(sports1, wb)) NULL) { printf(cannot open file.\n); return; } if (fwrite(g1, sizeof(ALLitems), 1, fp) ! 1) printf(file write error.\n); fclose(fp); if ((fp fopen(sports2, wb)) NULL) { printf(cannot open file.\n); return; } if (fwrite(g2, sizeof(ALLNode), 1, fp) ! 1) printf(file write error.\n); fclose(fp); }fwrite的四个参数依次是数据指针、单个元素字节数、元素个数、文件指针。这里容易踩的坑有两个一是结构体里有指针字段firstschool、firstitemfwrite把指针的值地址写入文件重启程序后这些地址早已失效不能直接加载回内存使用二是sizeof(ALLitems)是(mw1)倍项目节点结构体大小如果 m、w 和上次运行时不一样读取时数据会对不上。所以这个 save 函数的定位是「演示用」真正的持久化方案应该把链表逐个节点展开写入或者转成线性数组再存。课设答辩时能主动指出这个缺陷并提出序列化方案比说「我的系统支持持久化存储」更可信。5.2 常见调试错误getchar、scanf 与死循环原代码在主菜单循环里用了scanf(%d, t)接收选择遇到非法字符比如输入字母 a时 scanf 会返回 0变量 t 保持旧值程序带着旧值继续跑表现是「按什么都没反应」。处理方式是把scanf的返回值检查加上int result; do { printf(请选择0~8); result scanf(%d, t); while (getchar() ! \n); // 清空输入缓冲区 } while (result ! 1 || t 0 || t 8);while (getchar() ! \n)是经典的清空缓冲区写法。如果不清空残留的换行符会被后续的 scanf 跳过scanf 会忽略空白符通常没问题但如果后续用的是 getchar()就会直接读到残留换行符导致菜单一闪而过。原代码里多处getchar()在scanf之后正是这类 bug 的高发位置。5.3 单元测试计划与排序结果的验证方法建议按以下顺序测试能覆盖本系统全部功能点录入 3 个学校、2 个男子项目、1 个女子项目全部取前三名手动计算每个学校的 score、boys、girls 期望值调用功能 2 核对统计结果任何一项不匹配优先检查if (i m)这条分支调用功能 3、4、5、6 观察排序输出构造一个男团总分和总分的排序顺序不同的用例验证排序字段是否独立功能 7 查询一个不存在的项目编号确认不会崩溃功能 8 查询一个没有录入任何名次的项目确认输出空链表而不是野指针遍历。测试时用项目编号 0 退出录入状态再逐一触发各功能。如果在录入阶段没有调 save()直接切换到功能 2会因为 g2 未初始化而读到垃圾值所以主菜单在 case 1 后强制调用了 save()这个顺序设计要保留。5.4 扩展方向引入学校名称与项目名称原文档在需求里留了口子如果做得更好可以输入学校名称、项目名称。实现方式是在school结构体里加char name[20]在ALLitems的项目节点里加char itemName[30]。录入时同步输入字符串排序交换时要连同字符串一起复制不能用直接赋值要用strcpy。这个扩展涉及字符串数组的复制、strcmp比较和多字段交换是比原题更有区分度的加分项建议时间充裕的课设小组补上。到此这份运动会分数统计系统从链表建模、积分映射、排序选型到文件存储的完整链路已经拆完八个功能模块各有一个明确的实现细节值得在答辩时展开讲。直接插入排序保底、双向链表支撑查询、fwrite 演示持久化——这套 C 语言课设的常规打法里真正拉开差距的是对边界条件和异常输入的预处理以及能不能说清楚每一步数据结构选型的理由。本文还有配套的精品资源点击获取