news 2026/9/10 7:13:50

freeCodeCamp 每日编程挑战精讲:用 JavaScript 实现 Inventory Update 库存更新算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
freeCodeCamp 每日编程挑战精讲:用 JavaScript 实现 Inventory Update 库存更新算法

freeCodeCamp 每日编程挑战精讲:用 JavaScript 实现 Inventory Update 库存更新算法

【免费下载链接】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)中的第 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: truehelpCategory: "JavaScript"usesMultifileEditor: true,且使用legacy-challenge-list的块布局——这意味着它运行在 freeCodeCamp 的多文件编辑器中,挑战文件本身即该挑战的英文原文:691b559495c5cb5a37b9b484.md。

每个挑战文件都以 YAML frontmatter 开头,声明id(固定 24 位十六进制)、titlechallengeTypedashedName,随后用--description----hints----seed----solutions--四个区块组织内容:

区块作用
--description--问题陈述、输入格式约束与合并规则
--hints--assert断言组成的自动化测试用例
--seed--提供给学习者的起始代码骨架
--solutions--官方参考解法(在课程数据中被引用,不直接展示给学习者)

该挑战编号与日期一一对应:挑战序列从 2025-08-11 开始、按天递增(挑战 124 约对应 2025-12 月中旬),这与 API 侧日期工具 中ORIGINAL_START_YEAR = 2025ORIGINAL_START_MONTH = 8ORIGINAL_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。

四条核心合并规则

  1. 匹配相加:对shipment中每一项,如果该条目名已存在于inventory中,则将quantity累加到对应条目;
  2. 新增追加:如果收到的条目名在当前库存中不存在,则将其作为新条目追加到库存末尾
  3. 顺序保持:返回的库存必须保持原有顺序,新增条目按照它们在shipment中出现的顺序排在末尾;
  4. 原样返回:函数签名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 SocksMicrophone数量为 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; }

逐行剖析

第一阶段:建立「条目名 → 数组下标」的索引(第 2~5 行)

const inventoryMap = new Map(); inventory.forEach(([qty, item], index) => { inventoryMap.set(item, index); });

遍历inventory,利用数组解构同时取出qtyitem,将条目名映射到它在数组中的下标。这里的巧妙之处在于:Map 只存下标、不存数量,数量始终以inventory[index][0]为唯一事实来源(single source of truth),避免了 Map 与数组之间出现数据不同步。

第二阶段:遍历 shipment 完成合并(第 7~15 行)

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),因此官方解法在数据规模变大时优势明显。

边界情况与易错点清单

  1. 空库存inventory为空时不能崩溃,所有 shipment 条目都走新增分支(用例 3 覆盖);
  2. 空 shipmentshipment为空时应当原样返回库存;官方解法中forEach不执行任何逻辑,天然正确;
  3. 数量为 0 的条目:0 数量不代表条目不存在,不能被误删(用例 4 覆盖);
  4. 条目名匹配是精确字符串比较:题目未要求忽略大小写,"Apples""apples"被视为不同条目——这是日常编码挑战「以题目字面约束为准」的典型体现;
  5. shipment 内重复条目:Map 在下标登记后,第二条同名货品会走匹配分支完成累加;
  6. 副作用语义:官方解法修改了调用方传入的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/:dateYYYY-MM-DD日期返回指定挑战
/daily-coding-challenge/day/:dayMM-DD(月份-日)返回,可跨年复用
/daily-coding-challenge/month/:monthYYYY-MM返回某月挑战的摘要列表(id、编号、日期、标题)
/daily-coding-challenge/all返回全部已发布挑战的摘要列表
/daily-coding-challenge/newest返回最新挑战的日期

这些路由的响应结构与挑战内容直接对应:单挑战响应包含iddatechallengeNumbertitledescription,以及javascript/python两个语言对象(各含testschallengeFiles字段),其 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 是一道典型的「双数组合并 + 保序」问题,核心考点有三:

  1. 数据建模:用Map建立「条目名 → 下标」的反向索引,以 O(1) 查找替代线性扫描;
  2. 顺序语义:严格区分「匹配更新(保持原位)」与「新增追加(shipment 顺序末尾)」两条路径,这是与一般哈希合并题的最大差异;
  3. 边界健壮性:空库存、零数量、重复条目与 shipmnet 乱序等场景都要覆盖。

掌握这道题的 Map 索引思路后,你可以将其推广到同类的「合并 + 保序」场景,例如合并两份用户关注列表、按来源顺序聚合配置项、或实现表格数据的增量合并。若要继续练习,可在同目录挑战块中尝试相邻题目,如 Challenge 123: Roman Numeral Builder(贪心映射表思想)与 Challenge 125: Game of Life(二维数组遍历),形成对 2D 数组与映射表类题目的系统认知。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:13:01

CentOS7 上安装 Munin

关于Munin Munin是一个系统,像MRTG和cacti一样,监控磁盘、内存、CPU和网络等资源。 配置包括在被监控的服务器上安装“munin-node”(代理),并在收集数据的服务器上安装“munin-server”(数据收集服务器&a…

作者头像 李华
网站建设 2026/9/10 7:12:18

Serena 高级用法深度指南:提示规划策略与 Git Worktree 并行开发

Serena 高级用法深度指南:提示规划策略与 Git Worktree 并行开发 【免费下载链接】serena A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent 项目地址: https://gitcode.com/GitHub_Trending…

作者头像 李华
网站建设 2026/9/10 7:10:55

改进蜣螂优化算法TDBO的Matlab实现与对比实验分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:10:50

Arm-2D源码评测:Cortex-M图形加速库的工程价值与集成实践

1. Arm-2D 源码静态工程评测:为什么这会成为 Cortex-M 端 GUI 选型的关键一票做嵌入式图形界面开发的人,这几年应该都体会过同一种纠结:Cortex-M 上能跑的 GUI 框架越来越多,LVGL 迭代快、生态大,TouchGFX 有 ST 官方加…

作者头像 李华