freeCodeCamp 每日编程挑战 185 解析:用 JavaScript 实现花样滑冰评分系统
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本文基于 freeCodeCamp 开源仓库中的每日编程挑战(Daily Coding Challenge)第 185 题,深入讲解如何用 JavaScript 实现一套"去最高分、去最低分、再扣罚分"的花样滑冰评分算法。你将掌握数组排序、切片求和、rest 参数收集罚分等核心技巧,并理解该挑战在仓库中的存储结构、测试断言机制与运行流程,能够独立复现并验证全部示例。
挑战背景:Winter Games Day 6
这道题为 freeCodeCamp 每日编程挑战系列中的Challenge 185: Winter Games Day 6: Figure Skating,属于"冬季运动会"(Winter Games)主题周的第六关,前五关分别是 Opening Day、Snowboarding、Biathlon、Ski Jumping 与 Cross-Country Skiing,后续还有 Speed Skating、Luge 等关卡(可参见 daily-coding-challenges-javascript.json 中的 challengeOrder 列表)。
在 freeCodeCamp 中,这道题以独立的 Markdown 文件形式存储在课程目录下,文件名为697a49e6ff50d756c9b69362.md(对应仓库中的 挑战文件),通过 frontmatter 声明id、title、challengeType: 28与dashedName: challenge-185。其中challengeType: 28对应 challenge-types.ts 中定义的dailyChallengeJs,即"JavaScript 每日挑战"类型;而每日挑战另有一个 Python 变体(dailyChallengePy = 29)。
问题描述与评分规则
花样滑冰的裁判打分逻辑是这道题的核心。题目要求实现一个computeScore函数,其输入与处理规则如下:
- 第一个参数是一个包含 10 位裁判打分的数组,每个分数是 0 到 10 之间的数字(允许小数,如
8.5); - 基础分计算:去掉最高分和最低分,将剩余 8 个分数求和,得到基础分(base score);
- 罚分处理:函数后续接收的所有额外参数都是罚分(penalties),将全部罚分从基础分中减去,得到最终得分。
这一规则模拟了真实滑冰赛事中"去掉极端裁判分、再扣除技术违规罚分"的计分方式,是典型的截断均值(trimmed mean)应用场景。
起步代码与函数签名
仓库为学习者提供的种子代码(seed)如下:
function computeScore(judgeScores, ...penalties) { return judgeScores; }这里已经给出了关键的函数签名:
judgeScores:裁判打分数组,长度为 10,元素取值范围 0~10;...penalties:rest 参数,将传入的所有额外参数收集为一个数组,computeScore([...], 1, 2, 1)中的1, 2, 1会被收集为[1, 2, 1]。
种子代码目前只是原样返回judgeScores,需要学习者补齐排序、截取与罚分逻辑。
参考解法逐行拆解
仓库--solutions--区块给出的官方参考实现如下:
function computeScore(judgeScores, ...penalties) { const sortedScores = [...judgeScores].sort((a, b) => a - b); const baseScore = sortedScores.slice(1, 9).reduce((sum, s) => sum + s, 0); const totalPenalty = penalties.reduce((sum, p) => sum + p, 0); return baseScore - totalPenalty; }第一步:复制并排序打分数组
const sortedScores = [...judgeScores].sort((a, b) => a - b);- 使用展开运算符
[...judgeScores]创建原数组的副本,避免sort()原地修改入参(Array.prototype.sort默认会在原数组上排序,复制后调用是更安全的写法); sort((a, b) => a - b)提供比较函数,确保按数值升序排列,而不是默认的字符串字典序。这一点至关重要——如果省略比较函数,[10, 9, 8]会被当作字符串排成[10, 8, 9],导致结果错误;- 升序排列后,最小值位于下标 0,最大值位于下标 9。
第二步:剔除首尾并求和
const baseScore = sortedScores.slice(1, 9).reduce((sum, s) => sum + s, 0);slice(1, 9)取出下标 1 到 8 的元素,正好是去掉最低分(下标 0)和最高分(下标 9)后的剩余 8 个分数;reduce((sum, s) => sum + s, 0)将这 8 个分数累加,初始值0保证空数组场景下也能安全求和。
第三步:汇总罚分并计算最终得分
const totalPenalty = penalties.reduce((sum, p) => sum + p, 0); return baseScore - totalPenalty;penalties数组同样用reduce求和得到总罚分;- 最终得分 = 基础分 − 总罚分。因为罚分只可能减少总分,所以结果可能小于基础分,但仍应保持数值类型一致(整数相减得整数、小数相减得小数)。
测试断言:5 组用例验证
题目通过--hints--区块给出 5 组测试断言,学习者在本地或网页编辑器中提交代码后,会逐一运行这些assert.equal检查。逐组验证如下:
| 用例 | 输入 | 期望输出 | 计算过程 |
|---|---|---|---|
| 1 | [10, 8, 9, 6, 9, 8, 8, 9, 7, 7], 1 | 64 | 排序后去首尾,8 个分数求和为 65,减去罚分 1 |
| 2 | [10,10,10,10,10,10,10,10,10,10] | 80 | 全 10 分,去首尾后 8×10=80,无罚分 |
| 3 | [10, 8, 9, 10, 9, 8, 8, 9, 10, 7], 1, 2, 1 | 67 | 基础分 71,罚分合计 4,71−4=67 |
| 4 | [8.0, 8.5, 9.0, 8.5, 9.0, 8.0, 9.0, 8.5, 9.0, 8.5], 0.5, 1.0 | 67.5 | 含小数的分数与罚分,基础分 69.0,罚分 1.5 |
| 5 | [6.0, 8.5, 7.0, 9.0, 7.5, 8.0, 6.5, 9.5, 7.0, 8.0], 1.5, 0.5, 0.5 | 59 | 基础分 61.5,三项罚分合计 2.5,61.5−2.5=59 |
以第 1 组为例手动推演:数组排序后为[6, 7, 7, 8, 8, 8, 9, 9, 9, 10],slice(1, 9)得到[7, 7, 8, 8, 8, 9, 9, 9],求和为 65,减去罚分 1,最终64,与断言一致。第 4、5 组用例同时验证了函数对小数分数与多条小数罚分的处理能力,说明sort比较函数与reduce累加对浮点数的兼容性都在考察范围内。
代码仓库中的支撑机制
这道挑战并非孤立存在,仓库中有一套完整的配套体系:
- 课程结构定义:
daily-coding-challenges-javascript块在 daily-coding-challenges-javascript.json 中注册,声明了helpCategory: "JavaScript"、usesMultifileEditor: true、disableLoopProtectTests: true与blockLayout: "legacy-challenge-list",并将本挑战的id(697a49e6ff50d756c9b69362)列入 challengeOrder; - 类型系统:challenge-types.ts 中的
getIsDailyCodingChallenge与getDailyCodingChallengeLanguage函数识别dailyChallengeJs/dailyChallengePy,将第 28、29 两种类型分别映射到javascript/python语言,支撑前后端对该类挑战的差异化处理;同一文件中的viewTypes与submitTypes则将dailyChallengeJs映射为classic视图与tests提交类型,表明这道题采用经典编辑器并依靠测试用例判定通关; - 数据校验:daily-coding-challenge-validator.ts 定义了每日挑战的数据结构,其中
tests字段正是{ text, testString }数组——对应本题--hints--中"提示文本 + assert 代码"的组织方式; - Schema 约束:challenge-schema.js 校验所有挑战文件格式,要求
challengeType为 0~33 的整数(Joi.number().min(0).max(33)),并规定solutions为文件数组(Joi.array().items(...).min(1)),与本题"种子代码 + 参考解法"的结构完全吻合; - 前端组件:daily-coding-challenge 组件目录 下的
widget.tsx、calendar.tsx等实现每日挑战的页面展示与日历,说明该系列挑战已深度集成到 freeCodeCamp 的学习平台中。
易错点与边界情况
- 排序陷阱:不加比较函数
(a, b) => a - b时,sort()按字典序排序,10会排在9之前,导致去分错误; - 原地修改:直接对
judgeScores.sort(...)会修改传入的数组,虽然不影响本题目断言,但属于不良实践;参考解法用[...judgeScores]复制后排序更安全; - 罚分可能为空:第 2 组用例没有罚分参数,此时
penalties为空数组,reduce需提供初始值0才能正确返回基础分; - 浮点小数:第 4、5 组用例包含
8.5、0.5等小数,算法无需额外取整,保持原值相加减即可。
扩展思考
本算法本质是"截断均值 + 惩罚项"模型,可迁移到其他需要剔除极端值再计分的场景,例如体育赛事裁判打分、问卷异常值处理、数据预处理中的离群值剔除等。若想进一步优化,可考虑:
- 将"去掉几个最高/最低分"参数化(如改为去掉前 k 个极端值);
- 增加入参校验(长度不足 10、分数超出 0~10 范围时的处理策略);
- 使用
Math.max/Math.min找出极值后从总和中减去,实现无需排序的等价格式:
function computeScore(judgeScores, ...penalties) { const sum = judgeScores.reduce((s, x) => s + x, 0); const baseScore = sum - Math.max(...judgeScores) - Math.min(...judgeScores); return baseScore - penalties.reduce((s, p) => s + p, 0); }两种写法都能通过全部 5 组断言,前者逻辑更直观,后者免去排序的开销,适合在数据规模更大时使用。
总结
Challenge 185 是一道兼具趣味性与实用性的数组处理题:用sort+slice+reduce三件套完成"去极值求和",用 rest 参数收集不定数量的罚分并扣除。配合仓库中类型定义、Schema 校验与前端组件,你可以完整理解这类挑战从 Markdown 文件到学习平台运行的整条链路,并在自己的项目中复现这套计分逻辑。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考