freeCodeCamp 日常编程挑战解析:Challenge 151「Sorted Array?」数组排序状态判定
本文围绕 freeCodeCamp 开源课程中 daily-coding-challenges-javascript 块的 Challenge 151:Sorted Array?(对应挑战文档位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69373793f5a867f769cde136.md),完整剖析题目要求、六个官方测试断言、参考解法的时间/空间复杂度,并结合仓库源码说明这类挑战从 Markdown 源文件到前端评测、后端 API 分发的完整运行链路,帮助你既会解这道题,又理解它在 freeCodeCamp 代码库中的真实位置。
题目要求:判断数组的排序状态
给定一个数字数组,需要判断其排列属于以下哪种情况,并返回对应的字符串:
| 数组状态 | 含义 | 返回值 |
|---|---|---|
| 升序(Ascending) | 从最低到最高排列 | "Ascending" |
| 降序(Descending) | 从最高到最低排列 | "Descending" |
| 未排序 | 既非升序也非降序 | "Not sorted" |
题面要求实现一个 isSorted(arr) 函数。所谓“升序”并不要求严格递增——从后续参考解法可以看出,只要任意相邻两元素没有出现“后者小于前者”,数组即判定为升序;同理“降序”要求任意相邻两元素没有出现“后者大于前者”。这一宽松定义天然覆盖了包含相等元素的边界场景。
六个官方测试断言:逐步锁定行为
题目通过 --hints-- 段落给出了 6 组可执行的验证断言(编写格式为 Node 环境下的 assert.equal):
assert.equal(isSorted([1, 2, 3, 4, 5]), "Ascending");
assert.equal(isSorted([10, 8, 6, 4, 2]), "Descending");
assert.equal(isSorted([1, 3, 2, 4, 5]), "Not sorted");
assert.equal(isSorted([3.14, 2.71, 1.61, 0.57]), "Descending");
assert.equal(isSorted([12.3, 23.4, 34.5, 45.6, 56.7, 67.8, 78.9]), "Ascending");
assert.equal(isSorted([0.4, 0.5, 0.3]), "Not sorted");
归纳起来,测试覆盖了四类关键场景:
- 正整数升序(
[1, 2, 3, 4, 5]):最简单的连续升序用例; - 整数降序(
[10, 8, 6, 4, 2]):覆盖偶数步长降序; - 乱序数组(
[1, 3, 2, 4, 5]与[0.4, 0.5, 0.3]):数组绝大部分有序、仅个别相邻元素倒序时也必须正确返回"Not sorted",这是最容易写错的地方——例如只比较“首尾元素”或用一次排序后逐一比对坐标,都会在此类用例上失败; - 浮点数序列:既验证了浮点升序(
3.14 → 78.9方向),也验证浮点降序,说明实现不能依赖取整或类型转换。
起步代码与参考解法
题目在 --seed-contents-- 段落提供如下函数骨架,要求你只补全函数体:
function isSorted(arr) {
return arr;
}
官方在 --solutions-- 段落给出的参考实现采用双布尔标志单次遍历策略:
function isSorted(arr) {
let ascending = true;
let descending = true;
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) ascending = false;
if (arr[i] > arr[i - 1]) descending = false;
}
if (ascending) return "Ascending";
if (descending) return "Descending";
return "Not sorted";
}
逐步拆解
- 初始化两个标志位:假设数组既是升序又是降序,都设为
true。对所有长度为 0 或 1 的数组(循环体不执行),最终会命中if (ascending)分支返回"Ascending",符合题面语义; - 逐对比较:从
i = 1开始,每轮把arr[i]与前一元素arr[i - 1]比较:- 若出现
arr[i] < arr[i - 1],说明升序被破坏,ascending = false; - 若出现
arr[i] > arr[i - 1],说明降序被破坏,descending = false; - 相邻元素相等时,两个标志均保持不变,因此相等元素不构成“未排序”证据,这与题面宽松的排序定义一致;
- 若出现
- 收尾判定:遍历结束后检查标志位。注意两个
if的顺序:在单元素或空数组(两标志仍为真)场景下,先返回"Ascending"。其余场景三者互斥,判定正确。
复杂度分析
- 时间复杂度 O(n):只需从第 2 个元素扫描到末尾一次,比较次数为
n - 1; - 空间复杂度 O(1):仅使用两个布尔变量,不复制数组、不依赖排序或额外数据结构。
作为对比,若先对数组拷贝排序再与原始数组比对,需要 O(n log n) 时间与 O(n) 额外空间,且还要分别比对升序与降序两次;若借助 every((v, i) => i === 0 || v >= arr[i - 1]) 等函数式写法,可以更简洁但每调用一次仍是 O(n) 的线性遍历。参考解法通过一次遍历同时取消两个标志,是本题在时间与空间上都最优的经典实现。
深入仓库:这道题如何嵌入 freeCodeCamp 课程体系
只看解题还不够。Challenge 151 的实际价值在于它演示了 freeCodeCamp 如何把一道算法题组织成可被前端渲染、后台校验、并持久化分发的课程资产。
1. 挑战源文件的规范化结构
该挑战文档本体(69373793f5a867f769cde136.md)遵循 freeCodeCamp 挑战的统一 Markdown 规范,包含:
- YAML frontmatter:
id(24 位十六进制唯一标识)、title(人类可读标题)、challengeType(值为28,标识“每日 JavaScript 编码挑战”类型,与 show-daily-coding-challenge.tsx 中 JS 侧使用challengeType: 28的取值相互印证)、dashedName(用于生成 URL 的 kebab-case 名称); # --description--:对学习者的题目描述与返回值约定;# --hints--:以assert.equal形式给出的自动化验收断言;# --seed--/## --seed-contents--:提供给学习者的初始代码;# --solutions--:官方参考实现。
整份文件以“描述 + 测试 + 种子代码 + 解法”四段式驱动,仓库中同目录其余 300+ 个挑战 Markdown 均采用这一模式,属于可机器解析的标准化课程数据。
2. 从 Markdown 到在线评测的流转链路
这类 JS 挑战会经由两条并行的落地路径呈现给用户:
- 每日挑战(Daily Coding Challenge)路径:挑战数据通过 tools/daily-challenges 下的播种脚本写入数据库。种子脚本会先启动带“Dev Playground”区块的客户端,通过 GraphQL 拉取挑战内容(数据结构见 tools/daily-challenges/types.ts,包含
description、tests[{testString, text}]、challengeFiles等字段),再执行pnpm seed-daily-challenges写入DailyCodingChallenges集合。前端页面 show-daily-coding-challenge.tsx 会调用后端接口获取当天题目; - API 分发路径:挑战由 freeCodeCamp API 统一对外提供。路由注册在 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts,提供 6 个公开 GET 端点:
/daily-coding-challenge/date/:date(按YYYY-MM-DD精确取题)、/daily-coding-challenge/day/:day(按MM-DD取题)、/daily-coding-challenge/today(取当日题,以美国中部时间 UTC 午夜为界)、/daily-coding-challenge/month/:month(按YYYY-MM列出某月题目)、/daily-coding-challenge/all(列出全部)、/daily-coding-challenge/newest(返回最近一期日期)。代码注释明确指出最后一个挑战日期为 2026-08-10,且date、day路由会拒绝返回晚于当天(美国中部时间)的“未来题目”,保证挑战逐日解锁的节奏。
3. 返回数据的结构与校验
接口返回的单题 JSON 形状定义于 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts:包含 id(ObjectId)、date(ISO 日期时间)、challengeNumber、title、description,以及 javascript 与 python 两套语言数据;每套语言数据又含 tests(text + testString,其中 testString 正是 Challenge 151 中 assert.equal(...) 这类评测代码)与 challengeFiles(fileKey + contents)。这解释了为什么同一道 isSorted 题在仓库中既存在于 JavaScript 日常挑战块、也配套 Python 变体——daily-coding-challenge-validator.ts(client/src/utils/daily-coding-challenge-validator.ts)用 Joi 对来自数据库的整份挑战结构做运行时校验,确保 JS/Python 两套语言数据完整、测试与种子文件齐全后才进入前端渲染。
理解了这条链路,再看 Challenge 151 的 isSorted,就不仅是“写一个循环”,而是一个最小但完整的闭环:算法正确性由 --hints-- 中的 assert.equal 决定,种子代码决定起始环境,testString 注入评测器执行,返回字符串与断言逐字比对后判定通过。
小结
Challenge 151「Sorted Array?」用一次线性遍历配合两个布尔标志,以 O(n) 时间、O(1) 空间完成数组升序/降序/无序三态判定,是数组遍历与状态折叠类题目的标准示范。同时,通过阅读 69373793f5a867f769cde136.md 以及同目录、API 路由、校验器与播种脚本等周边源码,可以完整还原 freeCodeCamp 中一道日常编程挑战从题目编写、数据播种到接口分发与浏览器评测的全过程,为深入理解该开源课程体系提供了可复用的研读路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0632
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00