freeCodeCamp 每日编程挑战精讲:用 JavaScript 实现 Inventory Update 库存更新算法
freeCodeCamp 每日编程挑战精讲用 JavaScript 实现 Inventory Update 库存更新算法【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp本篇技术指南围绕 freeCodeCamp 每日编程挑战Daily Coding Challenge中的第 124 题「Inventory Update」展开完整讲解问题定义、数据格式约束、四条合并规则、逐条测试用例与官方参考解法的源码级剖析并结合仓库中 daily-coding-challenges-javascript 挑战块结构 与 每日挑战 API 路由实现 说明该挑战在真实平台中的落盘与分发机制。读完本文你将掌握「以条目名为键、以数组下标为索引」的 O(n) 原地合并思路并理解此类 2D 数组型算法题目的通用解题范式。挑战全景它处于每日编程挑战体系中的哪个位置「Inventory Update」是 freeCodeCamp 课程中daily-coding-challenges-javascript挑战块的第 124 个挑战。从块配置文件 curriculum/structure/blocks/daily-coding-challenges-javascript.json 可以看到该块标记为isUpcomingChange: true、helpCategory: JavaScript、usesMultifileEditor: true且使用legacy-challenge-list的块布局——这意味着它运行在 freeCodeCamp 的多文件编辑器中挑战文件本身即该挑战的英文原文691b559495c5cb5a37b9b484.md。每个挑战文件都以 YAML frontmatter 开头声明id固定 24 位十六进制、title、challengeType与dashedName随后用--description--、--hints--、--seed--与--solutions--四个区块组织内容区块作用--description--问题陈述、输入格式约束与合并规则--hints--由assert断言组成的自动化测试用例--seed--提供给学习者的起始代码骨架--solutions--官方参考解法在课程数据中被引用不直接展示给学习者该挑战编号与日期一一对应挑战序列从 2025-08-11 开始、按天递增挑战 124 约对应 2025-12 月中旬这与 API 侧日期工具 中ORIGINAL_START_YEAR 2025、ORIGINAL_START_MONTH 8、ORIGINAL_START_DAY 11的常量设定吻合。问题定义合并两份 2D 数组形式的库存清单题目原文给出的核心任务是给定一个表示店铺当前库存的二维数组以及一个表示新到货品的二维数组返回更新后的库存。输入数据格式数组中的每个元素格式固定为[quantity, item]第一个位置quantity是整数int第二个位置item是字符串string第一个参数inventory代表当前库存第二个参数shipment代表新收到的货品。例如库存为[[2, apples], [5, bananas]]新到货为[[1, apples], [3, bananas]]则更新后的库存为[[3, apples], [8, bananas]]——苹果 2 1 3香蕉 5 3 8。四条核心合并规则匹配相加对shipment中每一项如果该条目名已存在于inventory中则将quantity累加到对应条目新增追加如果收到的条目名在当前库存中不存在则将其作为新条目追加到库存末尾顺序保持返回的库存必须保持原有顺序新增条目按照它们在shipment中出现的顺序排在末尾原样返回函数签名updateInventory(inventory, shipment)直接返回更新后的数组官方解法采用原地修改mutate方式不创建新数组。第 3 条是本题与「哈希表直接重建数组」做法的关键差异点不能把库存重排为按字母序或按数量排序必须严格保留inventory传入时的条目顺序。规则推演用三步把示例拆解清楚以[[2, apples], [5, bananas]]与[[1, apples], [3, bananas], [4, oranges]]为例步骤当前处理的货品动作更新后的库存初始——[[2, apples], [5, bananas]]1[1, apples]apples 已存在 → 2 1 3[[3, apples], [5, bananas]]2[3, bananas]bananas 已存在 → 5 3 8[[3, apples], [8, bananas]]3[4, oranges]oranges 不存在 → 追加到末尾[[3, apples], [8, bananas], [4, oranges]]可见前两项触发「匹配相加」第三项触发「新增追加」最终结果与测试断言的[[3, apples], [8, bananas], [4, oranges]]完全一致。测试用例hints逐条拆解文档共给出 4 组自动化测试覆盖了三种典型场景用例 1纯匹配合并assert.deepEqual( updateInventory([[2, apples], [5, bananas]], [[1, apples], [3, bananas]]), [[3, apples], [8, bananas]] );shipment中所有条目在inventory中均已存在不触发任何新增验证「匹配相加」与「顺序保持」。用例 2匹配 新增assert.deepEqual( updateInventory([[2, apples], [5, bananas]], [[1, apples], [3, bananas], [4, oranges]]), [[3, apples], [8, bananas], [4, oranges]] );在用例 1 基础上混入不存在的oranges验证「新增条目追加到末尾、且保持 shipment 中的相对顺序」。用例 3空库存assert.deepEqual( updateInventory([], [[10, apples], [30, bananas], [20, oranges]]), [[10, apples], [30, bananas], [20, oranges]] );当inventory为空数组时所有货品都是新条目结果应与shipment顺序完全一致。该用例专门检验新增路径在空输入下是否健壮。用例 4零数量条目与新条目交错assert.deepEqual( updateInventory( [[0, Bowling Ball], [0, Dirty Socks], [0, Hair Pin], [0, Microphone]], [[1, Hair Pin], [1, Half-Eaten Apple], [1, Bowling Ball], [1, Toothpaste]] ), [[1, Bowling Ball], [0, Dirty Socks], [1, Hair Pin], [0, Microphone], [1, Half-Eaten Apple], [1, Toothpaste]] );这是最有含金量的用例覆盖了三个易错点数量为 0 的条目依然存在于库存中Dirty Socks和Microphone数量为 0 但没有收到新货必须原样保留不能被删除shipment 与 inventory 的条目顺序不同Hair Pin在 shipment 中排第 1但在 inventory 中排第 3合并后必须回到 inventory 原本的位置数量变为 0 1 1新条目追加时保持 shipment 顺序Half-Eaten Apple在 shipment 中位于Hair Pin之后、Toothpaste之前追加到末尾后也必须维持该相对顺序。官方解法Map 映射下标 原地累加文档--solutions--区块给出了官方参考实现function updateInventory(inventory, shipment) { const inventoryMap new Map(); inventory.forEach(([qty, item], index) { inventoryMap.set(item, index); }); shipment.forEach(([qty, item]) { if (inventoryMap.has(item)) { const index inventoryMap.get(item); inventory[index][0] qty; } else { inventory.push([qty, item]); inventoryMap.set(item, inventory.length - 1); } }); return inventory; }逐行剖析第一阶段建立「条目名 → 数组下标」的索引第 25 行const inventoryMap new Map(); inventory.forEach(([qty, item], index) { inventoryMap.set(item, index); });遍历inventory利用数组解构同时取出qty与item将条目名映射到它在数组中的下标。这里的巧妙之处在于Map 只存下标、不存数量数量始终以inventory[index][0]为唯一事实来源single source of truth避免了 Map 与数组之间出现数据不同步。第二阶段遍历 shipment 完成合并第 715 行shipment.forEach(([qty, item]) { if (inventoryMap.has(item)) { const index inventoryMap.get(item); inventory[index][0] qty; } else { inventory.push([qty, item]); inventoryMap.set(item, inventory.length - 1); } });匹配分支Map.has命中后取出下标直接对inventory[index][0]做加法累加新增分支Array.prototype.push将新条目追加到末尾新下标恰为inventory.length - 1随即登记进 Map保证后续shipment中再次出现同名条目时能走「匹配分支」继续累加例如 shipment 中出现两条apples的情况也能正确处理。第三阶段返回原数组第 17 行return inventory;由于全程原地修改直接返回inventory即符合「保持原顺序、新条目在末尾」的语义也满足用例 3 中空库存退化为「按 shipment 顺序返回」的边界行为。复杂度分析指标复杂度说明时间复杂度O(n m)构建 Map 遍历库存 n 项合并遍历 shipment m 项Map 的查找/插入均为平均 O(1)空间复杂度O(n)仅额外维护一张条目名 → 下标的 Map若采用「每次用Array.find线性查找」的朴素写法时间复杂度会退化为 O(n·m)因此官方解法在数据规模变大时优势明显。边界情况与易错点清单空库存inventory为空时不能崩溃所有 shipment 条目都走新增分支用例 3 覆盖空 shipmentshipment为空时应当原样返回库存官方解法中forEach不执行任何逻辑天然正确数量为 0 的条目0 数量不代表条目不存在不能被误删用例 4 覆盖条目名匹配是精确字符串比较题目未要求忽略大小写Apples与apples被视为不同条目——这是日常编码挑战「以题目字面约束为准」的典型体现shipment 内重复条目Map 在下标登记后第二条同名货品会走匹配分支完成累加副作用语义官方解法修改了调用方传入的inventory数组若调用方需要保留原始数据应自行传入副本。更多实现思路对比除官方解法的「Map 存下标」外还有几种常见思路各有权衡思路一Map 存数量最后重建数组function updateInventory(inventory, shipment) { const countMap new Map(); for (const [qty, item] of inventory) countMap.set(item, qty); for (const [qty, item] of shipment) { countMap.set(item, (countMap.get(item) ?? 0) qty); } return [...countMap.entries()].map(([item, qty]) [qty, item]); }优点是利用了Map的插入序特性新条目自动排在末尾缺点是必须先按原始顺序遍历 inventory 写入 Map再追加 shipment 新条目才能复现题目要求的顺序。若直接[...countMap.entries()]展开Map 的迭代顺序是「首次插入顺序」需要额外保证 inventory 先于 shipment 插入。该写法更符合函数式风格、无副作用但会生成新数组、空间开销略高。思路二Object作为字典function updateInventory(inventory, shipment) { const indexByItem {}; inventory.forEach(([qty, item], i) { indexByItem[item] i; }); shipment.forEach(([qty, item]) { if (item in indexByItem) inventory[indexByItem[item]][0] qty; else { inventory.push([qty, item]); indexByItem[item] inventory.length - 1; } }); return inventory; }与官方解法等价但需注意两点Object的键会被强制转为字符串且in操作符会沿着原型链查找对toString、constructor这类键名存在误判风险。官方解法选用Map正是为了规避这两个问题——这也解释了为什么仓库解法优先选择Map.has而不是in。思路三朴素线性查找不推荐function updateInventory(inventory, shipment) { shipment.forEach(([qty, item]) { const found inventory.find(entry entry[1] item); if (found) found[0] qty; else inventory.push([qty, item]); }); return inventory; }代码最简洁但每次查找都是 O(n)整体退化为 O(n·m)仅在条目数量极少时可用。平台侧支撑挑战如何被检索与分发「Inventory Update」这类每日挑战的题目内容与测试数据来自课程仓库而运行时则由 freeCodeCamp API 提供检索服务。api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 注册了 6 个公开 GET 路由路由用途/daily-coding-challenge/today返回今日美国中部时区的挑战全文/daily-coding-challenge/date/:date按YYYY-MM-DD日期返回指定挑战/daily-coding-challenge/day/:day按MM-DD月份-日返回可跨年复用/daily-coding-challenge/month/:month按YYYY-MM返回某月挑战的摘要列表id、编号、日期、标题/daily-coding-challenge/all返回全部已发布挑战的摘要列表/daily-coding-challenge/newest返回最新挑战的日期这些路由的响应结构与挑战内容直接对应单挑战响应包含id、date、challengeNumber、title、description以及javascript/python两个语言对象各含tests与challengeFiles字段其 TypeBox 校验定义见 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts。两个与「日期语义」相关的实现细节值得注意时区锚定getNowUsCentral()通过date-fns-tz计算America/Chicago的时区偏移确保「今天」以美国中部时间为准日期回收与闰日兜底getSourceDate()将任意日期映射回原始挑战周期2025-08-11 至 2026-08-10见 helpers.ts 中ORIGINAL_*常量并在请求 2 月 29 日时自动回退到 2 月 28 日的挑战——因为 2000 年被选为闰年占位年Date.UTC不会把 2 月 29 日滚动到 3 月 1 日。课程侧挑战编号、日期与内容的对应关系由 daily-coding-challenges-javascript.json 中的challengeOrder数组维护挑战正文则存放在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/ 目录下每道题一个 Markdown 文件、以固定 24 位 id 命名。总结与延伸Inventory Update 是一道典型的「双数组合并 保序」问题核心考点有三数据建模用Map建立「条目名 → 下标」的反向索引以 O(1) 查找替代线性扫描顺序语义严格区分「匹配更新保持原位」与「新增追加shipment 顺序末尾」两条路径这是与一般哈希合并题的最大差异边界健壮性空库存、零数量、重复条目与 shipmnet 乱序等场景都要覆盖。掌握这道题的 Map 索引思路后你可以将其推广到同类的「合并 保序」场景例如合并两份用户关注列表、按来源顺序聚合配置项、或实现表格数据的增量合并。若要继续练习可在同目录挑战块中尝试相邻题目如 Challenge 123: Roman Numeral Builder贪心映射表思想与 Challenge 125: Game of Life二维数组遍历形成对 2D 数组与映射表类题目的系统认知。【免费下载链接】freeCodeCampfreeCodeCamp.orgs open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考