freeCodeCamp 每日编程挑战实战解析:countMedals 奖牌统计 CSV 生成器(Winter Games Day 17)
freeCodeCamp 每日编程挑战实战解析countMedals 奖牌统计 CSV 生成器Winter Games Day 17【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp本篇技术指南以 freeCodeCamp 开源仓库中的每日编程挑战Challenge 196: Winter Games Day 17: Closing Daycurriculum/challenges/english/blocks/daily-coding-challenges-javascript/697a49e9860d24853adef681.md为蓝本完整拆解将二维奖牌名单聚合为排序 CSV 字符串的算法题从输入/输出格式约定、排序规则、边界情况到官方参考解法的逐行剖析并进一步结合仓库中challengeType: 28的挑战类型定义、每日挑战的 API 路由与前端组件还原这类题目在 freeCodeCamp 平台中的真实运行链路。读完本文你将掌握分组计数 多键排序 字符串拼接这一类数据处理题的标准解法也能理解一道每日编程挑战从 Markdown 到在线判题的全流程。题目背景每日编程挑战Daily Coding Challenge这道题属于 freeCodeCamp 课程体系中的daily-coding-challenges-javascript模块见 curriculum/structure/blocks/daily-coding-challenges-javascript.json该模块被标记为isUpcomingChange: true采用legacy-challenge-list布局帮助分类为 JavaScript并启用了usesMultifileEditor多文件编辑器。整块由 246 道编号连续的挑战组成从 Challenge 1: Vowel Balance 一直到 Challenge 246: Name Initials。本挑战编号为196是 Winter Games冬季运动会主题系列的闭幕日题目Day 17。同系列前 16 天分别覆盖了开幕式、单板滑雪、冬季两项、跳台滑雪、越野滑雪、花样滑冰、速度滑冰、雪橇、钢架雪车、高山滑雪、冰球、有舵雪橇、北欧两项、登山滑雪、自由式滑雪和冰壶见 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中 id 从697a49e6ff50d756c9b6935d到697a49e9860d24853adef680的条目。作为收官题目它要求汇总整个赛会的奖牌结果——这在工程语义上非常自然将每日每场的原始比赛记录聚合为一份可按国家排序的统计报表。从题目 frontmatter 可以看到该题的类型标识id: 697a49e9860d24853adef681 title: Challenge 196: Winter Games Day 17: Closing Day challengeType: 28 dashedName: challenge-196challengeType: 28即每日编程挑战的专属类型与经典题classic、选择题multipleChoice等类型在 curriculum/schema/challenge-schema.js 中被同一套 Joi schema 统一校验。题目要求输入与输出格式输入函数签名固定为countMedals(winners)参数winners是一个二维数组其中每个子数组代表一场比赛一个项目且固定为三项[gold_winner, silver_winner, bronze_winner]例如[ [USA, CAN, NOR], [NOR, USA, CAN], [USA, NOR, SWE] ]输出返回一个CSV 字符串格式如下第一行为表头Country,Gold,Silver,Bronze,Total country_name,gold_count,silver_count,bronze_count,total_medals格式约束非常严格必须逐条满足换行符各行之间用换行字符\n分隔注意不是平台的\r\n也不是可视换行。无多余空白逗号两侧不得有空格行尾不得有空格。排序规则先按金牌数从高到低排序若金牌数相同则按国家名称字母序ascending排序。以上面的输入为例应返回Country,Gold,Silver,Bronze,Total\nUSA,2,1,0,3\nNOR,1,1,1,3\nCAN,0,1,1,2\nSWE,0,0,1,1用文本方式打印出来即为Country,Gold,Silver,Bronze,Total USA,2,1,0,3 NOR,1,1,1,3 CAN,0,1,1,2 SWE,0,0,1,1可以验证排序逻辑USA 有 2 金排第一NOR、CAN、SWE 分别有 1、0、0 金按字母序依次为 CAN、NOR、SWE。注意这里NOR 出现在 CAN 之后尽管 NOR 的金牌数更多这正是先比金牌数、再比字母序的体现而 SWE 尽管没有金牌但有 1 枚铜牌仍然出现在输出中——只参与过比赛的国家都必须出现在结果里哪怕总奖牌数只有 1。测试用例逐条解读原文档的# --hints--部分提供了 6 组assert.equal测试它们是判题的核心依据覆盖了多种数据形态。逐一拆解用例 1基本排序与计数assert.equal( countMedals([ [USA, CAN, NOR], [NOR, USA, CAN], [USA, NOR, SWE] ]), Country,Gold,Silver,Bronze,Total\nUSA,2,1,0,3\nNOR,1,1,1,3\nCAN,0,1,1,2\nSWE,0,0,1,1 );这是题目中的标准示例同一国家USA/CAN/NOR在多场比赛中反复出现需要跨行累计且出现金牌并列与奖牌并列的排序情形。用例 2单场比赛、国家首次出现顺序assert.equal( countMedals([[NOR, SWE, FIN]]), Country,Gold,Silver,Bronze,Total\nNOR,1,0,0,1\nFIN,0,0,1,1\nSWE,0,1,0,1 );只有一场比赛三个国家各得一枚奖牌。注意排序NOR1 金最前FIN 与 SWE 均 0 金按字母序 FIN 在 SWE 之前。不能依赖遍历时的插入顺序输出必须显式排序。用例 3同一国家一场比赛拿两枚奖牌assert.equal( countMedals([ [ITA, CHN, CHN], [JPN, ITA, JPN] ]), Country,Gold,Silver,Bronze,Total\nITA,1,1,0,2\nJPN,1,0,1,2\nCHN,0,1,1,2 );关键陷阱第一场比赛中 CHN 同时出现在银牌和铜牌位置第二场 JPN 同时出现在金牌和铜牌位置。同一国家的计数必须在同一场比赛中同时累加两枚奖牌不能因为先处理金牌再处理银牌而把 CHN 的金牌字段重置或漏算。这也是最容易写错的点——初始化计数对象时必须保证三个字段独立累加。用例 46 场比赛、多个国家金牌并列assert.equal( countMedals([ [USA,CAN,NOR], [GER,FRA,ITA], [JPN,KOR,CHN], [SWE,FIN,NOR], [CAN,USA,SWE], [FRA,GER,ITA] ]), Country,Gold,Silver,Bronze,Total\nCAN,1,1,0,2\nFRA,1,1,0,2\nGER,1,1,0,2\nJPN,1,0,0,1\nSWE,1,0,1,2\nUSA,1,1,0,2\nCHN,0,0,1,1\nFIN,0,1,0,1\nITA,0,0,2,2\nKOR,0,1,0,1\nNOR,0,0,2,2 );6 个国家同得 1 金CAN/FRA/GER/JPN/SWE/USA必须全部按字母序排在最前0 金的国家CHN/FIN/ITA/KOR/NOR同样按字母序跟在后面。这验证了金牌数相同 → 字母序的二级排序在多个并列键上的稳定性。用例 58 场比赛、8 个国家金牌并列assert.equal( countMedals([ [ESP,ITA,FRA], [ITA,ESP,GER], [NOR,SWE,FIN], [FIN,NOR,SWE], [USA,CAN,MEX], [CAN,USA,MEX], [JPN,KOR,CHN], [CHN,JPN,KOR] ]), Country,Gold,Silver,Bronze,Total\nCAN,1,1,0,2\nCHN,1,0,1,2\nESP,1,1,0,2\nFIN,1,0,1,2\nITA,1,1,0,2\nJPN,1,1,0,2\nNOR,1,1,0,2\nUSA,1,1,0,2\nFRA,0,0,1,1\nGER,0,0,1,1\nKOR,0,1,1,2\nMEX,0,0,2,2\nSWE,0,1,1,2 );8 个国家均得 1 金输出顺序与输入顺序完全无关纯按字母序CAN CHN ESP FIN ITA JPN NOR USA。任何依赖输入顺序的实现都会在这里失败。用例 612 场比赛、完整大排名assert.equal( countMedals([ [USA,CAN,GER], [NOR,SWE,FIN], [USA,NOR,SWE], [GER,FRA,ITA], [JPN,KOR,CHN], [USA,GER,CAN], [SWE,NOR,FIN], [CAN,USA,NOR], [FRA,GER,ITA], [JPN,CHN,KOR], [SWE,FIN,NOR], [GER,ITA,FRA] ]), Country,Gold,Silver,Bronze,Total\nUSA,3,1,0,4\nGER,2,2,1,5\nJPN,2,0,0,2\nSWE,2,1,1,4\nCAN,1,1,1,3\nFRA,1,1,1,3\nNOR,1,2,2,5\nCHN,0,1,1,2\nFIN,0,1,2,3\nITA,0,1,2,3\nKOR,0,1,1,2 );最大规模的用例USA 以 3 金居首GER/JPN/SWE 均为 2 金按字母序排列1 金组内 CAN FRA NOR0 金组内 CHN FIN ITA KOR。同时 NOR 总奖牌数达到 51 金 2 银 2 铜验证了Total列是 GoldSilverBronze 的求和与排序无关。参考解法逐行剖析原文档# --solutions--提供了官方参考实现这是理解本题的最佳范本。完整代码如下function countMedals(winners) { const counts {}; for (const [gold, silver, bronze] of winners) { if (!counts[gold]) counts[gold] { Gold: 0, Silver: 0, Bronze: 0 }; if (!counts[silver]) counts[silver] { Gold: 0, Silver: 0, Bronze: 0 }; if (!counts[bronze]) counts[bronze] { Gold: 0, Silver: 0, Bronze: 0 }; counts[gold].Gold 1; counts[silver].Silver 1; counts[bronze].Bronze 1; } const countries Object.entries(counts).map(([country, medals]) ({ Country: country, Gold: medals.Gold, Silver: medals.Silver, Bronze: medals.Bronze, Total: medals.Gold medals.Silver medals.Bronze })); countries.sort((a, b) b.Gold - a.Gold || a.Country.localeCompare(b.Country)); const lines [Country,Gold,Silver,Bronze,Total]; for (const c of countries) { lines.push(${c.Country},${c.Gold},${c.Silver},${c.Bronze},${c.Total}); } return lines.join(\n); }整个解法可以分解为四步每一步都对应前面总结的考点第一步聚合计数分组统计const counts {}; for (const [gold, silver, bronze] of winners) { if (!counts[gold]) counts[gold] { Gold: 0, Silver: 0, Bronze: 0 }; if (!counts[silver]) counts[silver] { Gold: 0, Silver: 0, Bronze: 0 }; if (!counts[bronze]) counts[bronze] { Gold: 0, Silver: 0, Bronze: 0 }; counts[gold].Gold 1; counts[silver].Silver 1; counts[bronze].Bronze 1; }使用for...of 数组解构const [gold, silver, bronze]一次性取出三个获奖者语义清晰。惰性初始化对每个首次出现的国家先创建{ Gold: 0, Silver: 0, Bronze: 0 }骨架。这里的if (!counts[gold])利用了国家名非空字符串其真值为 truthy这一特性。关键点三次 1分别针对三个不同的字段因此同一国家在同一场比赛内同时获得两枚奖牌如用例 3 的 CHN 银铜、JPN 金铜也不会相互覆盖银牌不会把铜牌计数归零。第二步结构转换与 Total 计算const countries Object.entries(counts).map(([country, medals]) ({ Country: country, Gold: medals.Gold, Silver: medals.Silver, Bronze: medals.Bronze, Total: medals.Gold medals.Silver medals.Bronze }));用Object.entries(counts)把{国家: 奖牌对象}映射为统一的记录数组。Total在这里一次性求和避免后续重复计算字段名刻意与 CSV 表头一致Country、Gold…为最后一行模板字符串的拼接做了铺垫。第三步复合排序金牌降序 字母序countries.sort((a, b) b.Gold - a.Gold || a.Country.localeCompare(b.Country));这是全题的核心一行是一个经典的复合比较器b.Gold - a.Gold当a.Gold b.Gold时为负数a排在前面 →金牌降序。当金牌数相同时b.Gold - a.Gold结果为0falsy||短路到a.Country.localeCompare(b.Country)→按国家名升序字母序。localeCompare处理的是国家代码这类短字符串用默认 locale 即按字典序比较与题目要求的 alphabetical order 一致。第四步拼接 CSV 字符串const lines [Country,Gold,Silver,Bronze,Total]; for (const c of countries) { lines.push(${c.Country},${c.Gold},${c.Silver},${c.Bronze},${c.Total}); } return lines.join(\n);表头行硬编码为第一行。每行用模板字符串以,直接连接不添加任何空格天然满足逗号两侧无空格的约束。lines.join(\n)统一以\n换行且行尾没有多余的换行符join 不会在末尾追加分隔符同时满足用 \n 换行和行尾无空格两条要求。复杂度与边界分析时间复杂度遍历winners为O(E)E 为比赛场次Object.entries为O(C)C 为国家数排序为O(C log C)。整体O(E C log C)对本题规模的输入绰绰有余。空间复杂度counts与countries均为O(C)。潜在边界代码假设国家名是合法的对象键字符串由于使用counts[gold]的 truthy 检查做惰性初始化只要国家名非空即可正常工作。若某个子数组长度不足 3解构得到undefined会创建以undefined为键的条目——不过题目保证每个子数组固定为[gold, silver, bronze]三项无需额外防御。从题目到平台每日编程挑战的完整运行链路理解了算法本身再看它在 freeCodeCamp 中是如何被收录、存储与投递的有助于理解为什么这道题长这样。1. 挑战类型的定义与校验challengeType: 28在 curriculum/schema/challenge-schema.js 中由 Joi 校验challengeType: Joi.number().min(0).max(33).required()并配合# --description--、# --hints--、# --seed--、# --solutions--四个标准 section 组成完整题目。其中--description--渲染在题目上方的题干即本文第一部分的内容--hints--判题断言前端执行用户的countMedals后逐一assert.equal比对--seed--提供给用户的初始代码return winners;占位实现保证函数存在、类型正确--solutions--官方参考答案用于查看解决方案功能。2. 每日挑战的 API 服务api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 定义了 6 个公开的 GET 端点将题目按日期维度投递给前端端点参数格式用途GET /daily-coding-challenge/date/:dateYYYY-MM-DD按具体日期取题且不会返回晚于今日美国中部时间的题目GET /daily-coding-challenge/day/:dayMM-DD按月-日取题通过 api/src/daily-coding-challenge/utils/helpers.ts 的getSourceDate映射回源题日期GET /daily-coding-challenge/today无取当天的题GET /daily-coding-challenge/month/:monthYYYY-MM取整月题目列表仅含 id、challengeNumber、date、titleGET /daily-coding-challenge/all无取全部已发布题目列表GET /daily-coding-challenge/newest无取最新题目的日期响应结构由 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts 中的 TypeBox schema 定义单个题目的响应包含id、date、challengeNumber、title、description以及javascript/python两个语言的实现数据各含tests与challengeFiles并有完整的 200/400/404/500 错误响应约定。3. 日期映射的巧妙设计api/src/daily-coding-challenge/utils/helpers.ts 揭示了一个值得玩味的工程细节仓库最初只创建了2025-08-11 至 2026-08-10 一整年的挑战数据常量ORIGINAL_START_YEAR 2025、ORIGINAL_END_YEAR 2026。getSourceDate会把任意今天映射回这一年的对应日期从而让有限的题库可以循环投递8 月 11 日及之后映射到 2025 年早于该日期则映射到 2026 年2 月 29 日请求会被映射到 2 月 28 日的题目。与之配套client/src/utils/daily-coding-challenge-validator.ts 使用 Joi 对数据库返回的题目结构tests、challengeFiles、disableLoopProtectTests等做二次校验。4. 前端展示组件在客户端client/src/components/daily-coding-challenge/ 目录提供了widget.tsx题目挂件、calendar.tsx/calendar-day.tsx日历视图让用户按日期查看历史题目、not-found.tsx题目不存在时的兜底页以及配套样式与测试。这些组件共同支撑了每日一题的用户体验首页挂件展示今日题、日历回溯历史题而挑战的完成与提交则复用主 API 的 challenge 提交路由。举一反三可迁移的工程要点这道题虽小但浓缩了三种高频可复用模式分组计数group-by用普通对象或Map做 key → 计数结构的惰性初始化是数据统计类功能的基础。本题的升级版如按多维度分组、实时更新排行榜都可在此结构上扩展。复合排序比较器b.Gold - a.Gold || a.Country.localeCompare(b.Country)这种主键降序 次级键升序的写法在榜单、报表类需求中几乎每天都会遇到||短路保证了主键相等时才触发次级比较。严格格式化输出CSV 无多余空格、统一\n换行的约束对应真实生产环境中的文件/接口契约——当对接方要求精确的字节级格式时join拼接而非手动拼接末尾换行是避免边界 bug 的可靠手段。如果希望进一步探索可以研读同模块的其他题目例如同属 Winter Games 系列的其他挑战或daily-coding-challenges-python的对应 Python 版本以及上述 API 路由与前端组件的测试文件api/src/daily-coding-challenge/routes/daily-coding-challenge.test.ts、client/src/components/daily-coding-challenge/calendar.test.tsx理解平台如何保证每道题的投递与判题行为可预期。小结Challenge 196Winter Games Day 17: Closing Day要求实现countMedals将形如[gold, silver, bronze]的每场赛事记录聚合为国家 → {金, 银, 铜, 总}的统计表按金牌降序、同金牌按字母序排序后以无空格、\n换行、带表头的 CSV 字符串返回。官方解法通过惰性初始化计数 → 结构转换求和 → 复合比较器排序 → join 拼接四步完成且对同场同国双奖牌多国金牌并列0 金国家不遗漏等边界均有正确处理。这道题同时是了解 freeCodeCamp 每日编程挑战体系challengeType: 28、6 个按日期查询的 API 端点、前端日历挂件与 Joi 校验的一个绝佳切面。【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考