freeCodeCamp 每日编码挑战 Matrix Builder(Challenge 87)详解: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 Challenges)的第 87 题Matrix Builder展开,完整还原题目描述、判定断言与参考实现,并从仓库源码角度剖析它在课程体系中的定位(challengeType: 28)、从 Markdown 到图数据库再到数据库的流转链路,以及rows × cols零矩阵生成中高频出现的"数组行共享引用"陷阱。读完本文,你既能独立通过该题,也能理解这套每日挑战是如何在 freeCodeCamp 前端与课程仓库中被组织、校验和投递的。
题目档案:Challenge 87 到底要求什么
本挑战的源文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68ee9e3066cfd4eb2328e8a6.md,其 frontmatter 定义了这道题在仓库中的元信息:
--- id: 68ee9e3066cfd4eb2328e8a6 title: "Challenge 87: Matrix Builder" challengeType: 28 dashedName: challenge-87 ---任务描述
给定两个整数——行数rows和列数cols——返回一个指定大小的零值矩阵(matrix,即"数组的数组"),每个元素都为0。
题目给出的示例是传入2和3,即 2 行 3 列,应返回:
[ [0, 0, 0], [0, 0, 0] ]这里需要先厘清行与列的对应关系:外层数组的长度等于行数rows,而每一个内层子数组(代表一行)的长度等于列数cols。也就是说,"2 和 3"指的是 2 行、每行 3 个零,而不是相反。
挑战类型的语义:28 = dailyChallengeJs
frontmatter 中的challengeType: 28不是随意数字。在 packages/shared/src/config/challenge-types.ts 中可以看到明确映射:
const dailyChallengeJs = 28; const dailyChallengePy = 29;即类型28是 freeCodeCamp 专门为JavaScript 每日编码挑战预留的挑战类型,29则对应 Python 版本。该配置还通过challengeTypes.dailyChallengeJs暴露给整个客户端与服务端共享包使用,前端路由 client/src/client-only-routes/show-daily-coding-challenge.tsx 中即存在challengeType: 28的分支逻辑。
判定条件拆解:四条断言到底在测什么
# --hints--区块是这套每日挑战的评分核心。Challenge 87 一共给出四条断言,全部通过后题目才算通过:
assert.deepEqual(buildMatrix(2, 3), [[0, 0, 0], [0, 0, 0]]); assert.deepEqual(buildMatrix(3, 2), [[0, 0], [0, 0], [0, 0]]); assert.deepEqual(buildMatrix(4, 3), [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]); assert.deepEqual(buildMatrix(9, 1), [[0, 0, 0, 0, 0, 0, 0, 0, 0]]);(原文中最后一条为 9 个独立内层数组,即buildMatrix(9, 1)返回 9 行 × 1 列。)把断言拆开看,可以归纳出题目验证的四个维度:
| 调用 | 行数 | 列数 | 结构要点 |
|---|---|---|---|
buildMatrix(2, 3) | 2 | 3 | 常规矩形:2 行、每行 3 个零 |
buildMatrix(3, 2) | 3 | 2 | 行列互换对称性:3 行、每行 2 个零,验证参数方向不写反 |
buildMatrix(4, 3) | 4 | 3 | 更大的矩形,验证循环次数正确 |
buildMatrix(9, 1) | 9 | 1 | 高而窄的极端形态:9 行、每行仅 1 个零 |
判定使用assert.deepEqual而非assert.equal或===,这一点至关重要:deepEqual会递归比较两个数组的内部结构与元素值,而不是比较引用地址。因此你的函数必须返回一个全新的嵌套数组结构,且每一层、每个元素都与期望值逐项相等;单纯返回一个"形状像矩阵但内部不是独立数组行"的对象是无法通过的。buildMatrix(3, 2)与buildMatrix(2, 3)两组用例故意把两个参数互换,正是为了考察你是否把rows当作外层循环次数、把cols当作内层长度,而不至于弄反维度。
起始代码(seed)分析:为什么初始实现必然失败
# --seed--下的--seed-contents--给出了一个带有迷惑性的函数骨架:
function buildMatrix(rows, cols) { return rows; }它表面上"完成"了函数声明,实则直接返回了第一个入参rows(一个数字),这会在所有四条assert.deepEqual中断言失败——因为返回的不是数组,更不是嵌套数组。这种"能跑但必然报错"的起始代码是 freeCodeCamp 经典题目设计手法:把解题者直接推向"先读描述、再读断言、最后动手改"的正确工作流,而不是照着编译器的报错去试错。
官方参考实现与数组引用陷阱
挑战文件末尾的# --solutions--区块给出了官方解法:
function buildMatrix(rows, cols) { const matrix = []; for (let i = 0; i < rows; i++) { const row = new Array(cols).fill(0); matrix.push(row); } return matrix; }逐行解读这一实现:
const matrix = []初始化外层数组,它最终容纳rows个内层行数组;for (let i = 0; i < rows; i++)循环恰好执行rows次,对应外层长度;new Array(cols).fill(0)创建长度为cols的数组,并调用fill(0)把每个槽位填充为数值0;matrix.push(row)把每一行追加进外层,循环结束后return matrix。
整体时间复杂度为 O(rows × cols),空间复杂度亦为 O(rows × cols),是该尺寸输入下的最优量级。
高频陷阱:复用同一行引用
初学者最容易踩的坑是写出这样的"精简版":
// 错误示范:所有行共享同一个数组引用! function buildMatrix(rows, cols) { return new Array(rows).fill(new Array(cols).fill(0)); }这段代码表面返回的结构与期望完全一致,四个用例中的assert.deepEqual甚至也能"碰巧"通过(deepEqual 只看值不看引用)。但它隐藏着一个严重缺陷:fill()在填充对象/数组类型时填入的是同一个引用,外层 2 个元素其实都指向同一块[0, 0, 0]。一旦后续对矩阵做赋值,例如matrix[0][1] = 9,会发现matrix[1][1]也同时变成了 9。freeCodeCamp 课程体系会在矩阵旋转、矩阵移位等后续题目中不断涉及对单元格的原地修改,这种共享引用的矩阵会在那时酿成难以排查的 bug。而官方解法每次进入循环体都用new Array(cols)新建行,保证行与行之间完全独立。
其他等价写法
若不想用命令式循环,也可以借助Array.from或展开运算符写出等价的函数式实现:
// 等价方案 A:Array.from 每次回调都新建行 function buildMatrix(rows, cols) { return Array.from({ length: rows }, () => new Array(cols).fill(0)); } // 等价方案 B:先铺外层再映射 function buildMatrix(rows, cols) { return [...Array(rows)].map(() => Array(cols).fill(0)); } // 等价方案 C:双循环逐元素写入(适合后续接自定义初始值) function buildMatrix(rows, cols) { const matrix = []; for (let r = 0; r < rows; r++) { matrix[r] = []; for (let c = 0; c < cols; c++) { matrix[r][c] = 0; } } return matrix; }三个方案各自的关键点一致:必须为每一行单独创建一个数组实例。方案 A 与 B 本质相同——Array.from的映射回调与map的回调都会为每个元素执行一次,因而天然规避了引用共享;方案 C 则把"矩阵就是数组的数组"这一数据模型用最底层的双层索引显式表达,适合在读懂后自行扩展为"填充任意值"或"生成棋盘格"。
在仓库中的位置:矩阵类题目在每日挑战里的延续
把视野拉远一点,Matrix Builder 并非孤立的矩阵题。在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 的挑战顺序表中可以看到,围绕"二维数组/矩阵"这个数据结构的题目在整套 JavaScript 每日挑战里反复出现,例如:
- Challenge 27: Matrix Rotate(矩阵旋转)
- Challenge 201: Matrix Shift(矩阵移位)
- Challenge 221: Inverted Matrix(矩阵翻转)
- Challenge 245: Spiral Matrix(螺旋矩阵)
- Challenge 87: Matrix Builder(本题,构建零矩阵)
这意味着buildMatrix这类题目承担着打地基的角色:先用最简单的零矩阵把"行是外层数组、列是内层数组、行间必须独立"这三条铁律刻进思维,后续的旋转、移位、螺旋遍历等题目才能建立在这个正确的二维数组心智模型之上。
从 Markdown 到数据库:挑战内容的仓库流转链路
这道.md文件并不只是给人看的文本。以本仓库的实现为证据,这套每日挑战从课程文件到用户浏览器大致经历如下链路,可以用作通读源码的路线图:
挑战解析与 GraphQL 暴露:课程 Markdown 经 curriculum/src/file-handler.ts、
get-challenges等工具解析后,由前端 Gatsby 层的allChallengeNodeGraphQL 节点对外提供。每日挑战被组织在dev-playgroundsuperblock 下的daily-coding-challenges-javascript与daily-coding-challenges-python两个 block 中,这一归属关系在 curriculum/src/build-curriculum.ts('dev-playground': SuperBlocks.DevPlayground)与 block 结构文件里均有体现。JS/Python 双版本一致性校验:curriculum/src/test/daily-challenges.test.js 会同时拉取 JavaScript 与 Python 两个 block 的全部挑战,断言两边挑战数量相等,且逐对校验
id、标题、描述与测试数量完全一致。换句话说,Challenge 87 在 Python block 中存在一个同 ID 的同题副本(标题同为 "Matrix Builder"),两者由同一条测试兜底保证不会出现内容漂移。数据库投递(seed):本地或生产环境的数据库通过 tools/daily-challenges/README.md 中描述的流程把挑战灌入
DailyCodingChallenges集合:先复制sample.env为.env、在开启"显示即将上线内容"的状态下运行主客户端,再在tools/daily-challenges目录执行pnpm seed-daily-challenges。具体实现见 tools/daily-challenges/helpers.ts:脚本向http://localhost:8000/___graphql发送 GraphQL 查询,按superBlock: "dev-playground"、block: "daily-coding-challenges-javascript"拉取全部挑战,将 JS 的id、标题、描述、tests、challengeFiles与 Python 的对应字段合并成一条同时携带javascript与python两种实现的数据记录(其中description还需经removeSection剥离解析器附加的<section>标签)。前端读取与展示:客户端在 client/src/client-only-routes/show-daily-coding-challenge.tsx 中处理
challengeType: 28的挑战页面,并读取数据库记录的javascript.tests与javascript.challengeFiles渲染题目与判题器。数据库返回结构的合法性由 client/src/utils/daily-coding-challenge-validator.ts 中的 Joi 模式把关——它要求每条记录必须包含challengeNumber(不小于 1 的整数)、title、date、description,以及嵌套的tests[{text, testString}]与challengeFiles[{fileKey, contents}]。本挑战中assert.deepEqual(...)的断言字符串,正是存储在tests[].testString里、最终被真实执行的那份代码。
这一条链路也解释了为什么本挑战文件中的 frontmatter(id、title、challengeType、dashedName)、--description--、--hints--、--seed--、--solutions--五个区块缺一不可:它们分别对应解析、渲染、判题、初始代码与官方答案五个环节的输入。
如何本地验证你的实现
该题是纯函数题,不依赖任何库,因此你可以用任意 JavaScript 运行环境自测。以 Node.js 为例,把下面这段完整用例保存为matrix-builder.test.js后执行node matrix-builder.test.js即可:
function buildMatrix(rows, cols) { const matrix = []; for (let i = 0; i < rows; i++) { const row = new Array(cols).fill(0); matrix.push(row); } return matrix; } const cases = [ [[2, 3], [[0, 0, 0], [0, 0, 0]]], [[3, 2], [[0, 0], [0, 0], [0, 0]]], [[4, 3], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]], [[9, 1], [[0], [0], [0], [0], [0], [0], [0], [0], [0]]] ]; for (const [[rows, cols], expected] of cases) { const actual = buildMatrix(rows, cols); const pass = JSON.stringify(actual) === JSON.stringify(expected); console.log(`buildMatrix(${rows}, ${cols}) ->`, pass ? 'PASS' : `FAIL ${JSON.stringify(actual)}`); if (!pass) process.exitCode = 1; }说明:本地用
JSON.stringify做等价比较,是因为两个数组即使内部元素完全相同,直接===也会因引用不同而返回false;这与 freeCodeCamp 判题器使用的assert.deepEqual的语义一致——递归比较值而非比较引用。
如果你还想观察"共享引用"陷阱的破坏力,可以把上面实现临时替换为return new Array(rows).fill(new Array(cols).fill(0));,再追加一行actual[0][0] = 9;并打印矩阵——你会发现每一行的第一个元素都被改成了 9。这正是 Challenge 87 想让你在入门阶段就刻进记忆的那一课。
【免费下载链接】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),仅供参考