freeCodeCamp 每日编程挑战第 149 题 vOwElcAsE 解题指南:字符串元音大写化算法
本指南以 freeCodeCamp 课程仓库中 daily-coding-challenges-javascript 板块的 Challenge 149 为题,完整讲解题目要求、5 组断言测试的边界语义、官方参考解法的逐行原理与复杂度分析,并给出多行 replace、map 等替代实现与本地自测方法。读者通过本文可掌握一类"逐字符分类重写字符串"问题的通用分析套路,也能了解这类题库文件在 freeCodeCamp 课程体系中的组织方式与校验规则。
题目定位:它在仓库中的哪个位置
该题是 freeCodeCamp 课程 daily-coding-challenges-javascript 板块(每日编程挑战 JavaScript 系列)中编号连续的每日一题。原始挑战文件位于:
每个挑战文件头部都有一组 YAML frontmatter,其中 challengeType: 28 是关键元数据。结合 packages/shared/src/config/challenge-types.ts 中的类型定义(第 30 行 const dailyChallengeJs = 28;)可以确认,类型 28 代表"JavaScript 每日编程挑战"(Daily Coding Challenge - JavaScript),这一类型在该文件的映射表中对应 runner 'tests',也就是说它走的是标准测试运行器路径,由断言逐条判定正确性。
题目的注册信息记录在板块结构文件 curriculum/structure/blocks/daily-coding-challenges-javascript.json 的 challengeOrder 列表中:id 为 69306364df283fcaff2e1ada,标题为 "Challenge 149: vOwElcAsE"。同一板块还收录了编号相邻的 Challenge 144(Resolution Streak)到 Challenge 155(Plant the Crop)等题目。所有课程 Markdown 文件在入库前都需通过 curriculum/schema/challenge-schema.js 的 Schema 校验,其中 challengeType: Joi.number().min(0).max(33).required() 即对应 frontmatter 中 28 的取值合法性。
题目描述与三条核心规则
原题描述如下:
Given a string, return a new string where all vowels are converted to uppercase and all other alphabetical characters are converted to lowercase.
翻译过来即:给定一个字符串,返回一个新字符串——所有元音字母转大写,其余字母转小写,非字母字符保持不变。
描述下方附带两条约束细则,它们是所有边界情况的判据来源:
- 元音定义为
"a"、"e"、"i"、"o"、"u",不分大小写(即A、E、I、O、U也属于元音,处理时应按元音处理)。 - 非字母字符保持不变(数字、标点、空格、波浪号等一律原样输出)。
三句话合起来,其实定义了输出规则的三分支逻辑:
| 字符类别 | 处理动作 |
|---|---|
| 元音字母(a/e/i/o/u 及其大写形式) | 转为大写 |
| 其他字母(辅音字母,大小写皆可) | 转为小写 |
| 非字母字符(数字、空格、标点等) | 原样保留 |
注意第二类与第三类的区别是解题关键:"HELLO" 中的 E 和 O 是元音,需要大写;但输入已经是大写的 H、L,它们属于"其他字母",必须被小写化,因此 "HELLO" 经过转换会变成 "hEllO" 而不是保持 "HELLO"。
测试用例解读:hints 定义的验收标准
原文件的 # --hints-- 区块给出了 5 组断言,全部使用 assert.equal 进行严格返回值比较。它们是该题通过的充分必要条件,也恰好覆盖了上面三类分支:
assert.equal(vowelCase("vowelcase"), "vOwElcAsE");
assert.equal(vowelCase("coding is fun"), "cOdIng Is fUn");
assert.equal(vowelCase("HELLO, world!"), "hEllO, wOrld!");
assert.equal(vowelCase("git cherry-pick"), "gIt chErry-pIck");
assert.equal(vowelCase("HEAD~1"), "hEAd~1");
逐一拆解这些用例的考察点:
| 输入 | 预期输出 | 主要考察点 |
|---|---|---|
"vowelcase" |
"vOwElcAsE" |
全小写输入:元音上挑为大写、辅音保持小写 |
"coding is fun" |
"cOdIng Is fUn" |
含空格:空格原样保留,单词内元音独立大写 |
"HELLO, world!" |
"hEllO, wOrld!" |
全大写输入:元音保持大写、辅音被降为小写,逗号与感叹号不动 |
"git cherry-pick" |
"gIt chErry-pIck" |
含连字符 -:非字母字符保留,且不影响两侧字符的独立判断 |
"HEAD~1" |
"hEAd~1" |
混合数字 1 与波浪号 ~:两者原样保留,数字不参与字母判断 |
从第 3、5 两个用例可以提炼出最容易踩的坑:大小写混合输入中,辅音字母一律要小写,即便输入原本是大写;同时输出长度必须与输入完全一致(因为没有任何字符被删除或新增,只有大小写变化),任何丢失字符或拆分不当的实现都会立刻导致断言失败。
种子代码:需要补全的函数骨架
原文件的 # --seed-contents-- 给出如下起始代码:
function vowelCase(str) {
return str;
}
函数接收一个字符串参数 str,当前是"原样返回"的空实现。要全部通过上面的 5 条断言,你需要在不改变函数签名与返回值形态的前提下,补全中间逻辑。由于函数的输入、输出都是单个字符串,且不产生任何副作用,这是一个典型的纯函数题目,非常便于单元测试。
官方解法逐行解读
原文件 # --solutions-- 区块给出的参考实现如下:
function vowelCase(str) {
const vowels = "aeiouAEIOU";
let result = "";
for (let char of str) {
if (vowels.includes(char)) {
result += char.toUpperCase();
} else if (/[a-zA-Z]/.test(char)) {
result += char.toLowerCase();
} else {
result += char;
}
}
return result;
}
逐行分析其设计思路:
const vowels = "aeiouAEIOU";:用一个包含 10 个字符的字符串作为元音查找表。同时收录大小写两套,是为了让后续的includes判断无需额外的大小写归一化调用。let result = "";:累积结果的字符串变量。采用"扫描 + 拼接"的方式逐字符构建输出,保证输出与输入逐位置一一对应。for (let char of str):for...of会按码点顺序遍历字符串的每个字符,天然适合逐字符重写类问题。- 第一层判断
vowels.includes(char):直接命中"是否是元音"这个最高优先级条件。命中则执行char.toUpperCase()。注意:由于查找表同时包含大小写,这里即使原字符是大写元音,toUpperCase()也只是幂等地保持大写,不会出错。 - 第二层判断
/[a-zA-Z]/.test(char):正则字面量匹配任意单个英文字母。走到这一分支说明字符是辅音(已排除元音且是字母),因此统一执行char.toLowerCase()降为小写。 else兜底:既非元音也非字母的字符(数字、空格、标点、连字符、波浪号等)直接原样追加。return result;:返回累积的新字符串,原字符串str完全不被修改。
把 "HELLO, world!" 代入走一遍:H、L、L 落入第二个分支转小写;E、O 落入第一个分支(保持)大写;空格与 ,、! 进入 else 原样保留,最终得到 "hEllO, wOrld!",与断言一致。
时间复杂度与空间复杂度
- 时间复杂度 O(n):单次线性遍历,每个字符只做常数次字符串查找(
includes对 10 字符表为常数级)与一次正则测试,n 为输入字符串长度。 - 空间复杂度 O(n):
result字符串的长度与输入一致(字符一一对应、长度不变),因此需要与输入等长的额外空间。
若追求"原地、零额外空间"则不可行——JavaScript 字符串不可变,重写结果必然要落到新字符串中,因此 O(n) 空间是本类题目不可避免的下界。
等价替代实现:正则替换与数组化写法
除官方逐字符遍历写法外,还有两种常见等价实现,核心差异只在代码组织方式,判定逻辑(元音大写、辅音小写、其余不动)完全一致。
写法一:利用 String.prototype.replace + 回调函数。 先用字符类 [a-zA-Z] 匹配所有字母,再在回调内用不区分大小写的正则 /i 判定元音:
function vowelCase(str) {
return str.replace(/[a-zA-Z]/g, char =>
/[aeiou]/i.test(char) ? char.toUpperCase() : char.toLowerCase()
);
}
这种写法把"只处理字母、其余不动"浓缩进了一个正则,非字母字符天然不被回调触碰,语义非常清晰。
写法二:先拆分再逐字符映射,最后重组。
function vowelCase(str) {
const vowels = new Set("aeiouAEIOU");
return [...str]
.map(char =>
vowels.has(char)
? char.toUpperCase()
: /[a-zA-Z]/.test(char)
? char.toLowerCase()
: char
)
.join("");
}
其中用 Set 替代字符串做 has 查询,使元音判断的语义更直白("是否属于集合"),在字符较多场景下查找也更稳定;[...str] 展开后再 .join("") 保证字符顺序不被打乱。三种实现都可以通过全部 5 条断言,选哪种主要取决于你更熟悉 for...of、正则回调还是数组管道风格。
本地自测:不依赖网页即可验证
该题的 5 条断言用 assert.equal 编写,而 assert.equal 在 freeCodeCamp 的测试运行环境中可直接使用。想在本地快速验证自己的实现,可以把它替换成 Node.js 内置的 assert.strictEqual 手动跑一遍,例如:
const assert = require("node:assert");
function vowelCase(str) {
// ... 你的实现(可以是上方任一版本)...
}
assert.strictEqual(vowelCase("vowelcase"), "vOwElcAsE");
assert.strictEqual(vowelCase("coding is fun"), "cOdIng Is fUn");
assert.strictEqual(vowelCase("HELLO, world!"), "hEllO, wOrld!");
assert.strictEqual(vowelCase("git cherry-pick"), "gIt chErry-pIck");
assert.strictEqual(vowelCase("HEAD~1"), "hEAd~1");
console.log("all assertions passed");
将该文件保存后以 node <文件名> 运行,若控制台输出 all assertions passed,说明你的实现与官方验收标准完全一致。你还可以自行补充更刁钻的边界用例,例如空字符串 vowelCase("") 应返回 ""、纯数字串 vowelCase("12345") 应原样返回、含非英文拉丁字符的字符串等,来检验函数对"非字母一律保留"这一约束的健壮性。
这类挑战在课程与产品中的落地方式
理解题目本身之外,了解它所属的课程体系能帮助你判断这类文件的真实运行环境。从仓库证据看,该题所在的板块有如下特点:
- 板块元信息:在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中,该板块声明了
helpCategory: "JavaScript"、blockLayout: "legacy-challenge-list",并针对每日题的特性开启了usesMultifileEditor与disableLoopProtectTests(关闭循环保护测试,便于处理纯算法实现)。 - 文件规范约束:题目 Markdown 文件遵循
# --description--→# --hints--→# --seed--(内含## --seed-contents--)→# --solutions--的统一章节结构,这些区块正是 freeCodeCamp 课程流水线(解析、Schema 校验、测试注入)的输入约定。 - 类型驱动的渲染与测试:客户端侧通过 packages/shared/src/config/challenge-types.ts 提供的
getIsDailyCodingChallenge与getDailyCodingChallengeLanguage等工具识别类型 28(JavaScript)与 29(Python)的每日题,并将其路由到tests类测试运行器执行断言(参见 client/src/templates/Challenges/utils/build.ts 对runnerTypes[challengeType]的解析);课程 Schema 校验器 curriculum/schema/challenge-schema.js 则对所有字段(含challengeType、description、hints、seed、solutions)的合法性进行把关。
小结
Challenge 149 "vOwElcAsE" 表面上是简单的大小写转换,实则精准考察了三类字符分支的优先级处理:元音优先大写、辅音一律小写、非字母字符原样保留,且元音判定不分大小写。官方解法以 for...of 线性扫描配合查找表与字母正则,实现了 O(n) 时间、O(n) 空间的纯函数;文中的正则回调版与数组管道版则是同样语义的不同表达。配合 5 组断言理解边界、再以 Node 本地自测验证,即可完整吃透这道典型的字符串重写题,并为同一板块中大量同构的逐字符处理题目(如 Challenge 82 SpOoKy~CaSe、Challenge 163 Consonant Case)打下基础。
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 StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051