news 2026/9/8 22:10:46

freeCodeCamp 每日编程挑战解读:用字符编码与模运算实现凯撒密码解译(Message Decoder)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
freeCodeCamp 每日编程挑战解读:用字符编码与模运算实现凯撒密码解译(Message Decoder)

freeCodeCamp 每日编程挑战解读:用字符编码与模运算实现凯撒密码解译(Message Decoder)

【免费下载链接】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 系列(JavaScript)的第 12 题 "Challenge 12: Message Decoder" 为主体,完整覆盖该题的原始题目约束、四组官方测试用例与参考解答,并结合仓库中的挑战类型定义、课程结构与出题管线源码,深入讲解如何用charCodeAt/fromCharCode加模运算(% 26)实现支持正负偏移、自动环绕且保留大小写的凯撒密码解码。读完本文,你能独立写出该函数、理解偏移量取模消除的原理,并了解这道题在 freeCodeCamp 课程体系中的落地位置。

题目在仓库中的位置与出处

本题对应仓库中的课程文档 Challenge 12: Message Decoder,其 front matter 声明了以下元数据:

id: 68216ef80f957572e7c340c5 title: "Challenge 12: Message Decoder" challengeType: 28 dashedName: challenge-12

challengeType: 28并非随意编号。在共享包的 challenge-types.ts 中可以看到:

const jsLab = 26; const pyLab = 27; const dailyChallengeJs = 28; const dailyChallengePy = 29;

即 28 是dailyChallengeJs类型。同一文件还规定该类型的视图与提交方式:

// determine the component view for each challenge export const viewTypes = { ... [dailyChallengeJs]: 'classic', // 使用经典编辑器视图 ... }; // determine the type of submit function to use on completion export const submitTypes = { ... [dailyChallengeJs]: 'tests', // 通过跑断言测试判定通过 ... };

从源码结构看,dailyChallengeJs会被getIsDailyCodingChallenge(同文件 L187-L188)识别为每日编程挑战,并用viewTypes中的classic视图在客户端渲染,完成判定走tests提交路径——也就是说,本文后面的四组assert.equal用例就是判题依据本身。

该题的排序信息记录在 block 结构文件(dashedName: daily-coding-challenges-javascript)中:68216ef80f957572e7c340c5排在 "Challenge 11: Mile Pace" 之后、"Challenge 13: Unnatural Prime" 之前,title为 "Challenge 12: Message Decoder"。该结构文件同时声明了usesMultifileEditor: truehelpCategory: "JavaScript"blockLayout: "legacy-challenge-list"等块级配置。

值得注意的一点:该 block 配置中isUpcomingChange: true,且 api/src/daily-coding-challenge 的 README 说明每日挑战的信息接口独立于主 API——从目录结构(routes/schemas/utils/)推断,线上"每日挑战"是按日期轮播投放的挑战集,而本仓库中以固定文件形式存放的是题库本体。因此以下讲解以题库中的题目文件为准。

题目原文:给定密文与偏移量,还原明文

题目的原始描述(description 小节)为:

Given a secret message string, and an integer representing the number of letters that were used to shift the message to encode it, return the decoded string.

  • A positive number means the message was shifted forward in the alphabet.
  • A negative number means the message was shifted backward in the alphabet.
  • Case matters, decoded characters should retain the case of their encoded counterparts.
  • Non-alphabetical characters should not get decoded.

翻译成工程语言,函数签名是decode(message: string, shift: number): string,约束有四条:

  1. shift为正数:编码时字母在字母表中向前移动了shift位(如a → e);
  2. shift为负数:编码时字母向后移动了|shift|位;
  3. 大小写必须保留:编码字符是大写,解码结果就是大写,反之亦然;
  4. 非字母字符原样透传:空格、标点、数字等一律不做变换。

这本质上是一个双向凯撒密码:编码是"字母平移",解码就是"反向平移"。

官方测试用例与断言

题目文档的 hints 小节给出了四组官方断言,它们同时覆盖了正偏移、大偏移、负偏移、混合大小写和符号透传等关键场景:

输入message输入shift期望输出覆盖点
"Xlmw mw e wigvix qiwweki."4"This is a secret message."正偏移、含句号
"Byffi Qilfx!"20"Hello World!"大偏移(20 等价于 -6)
"Zqd xnt njzx?"-1"Are you okay?"负偏移、含问号
"oannLxmnLjvy"9"freeCodeCamp"混合大小写、无空格

对应的断言写法(即判题时执行的testString内容):

assert.equal(decode("Xlmw mw e wigvix qiwweki.", 4), "This is a secret message."); assert.equal(decode("Byffi Qilfx!", 20), "Hello World!"); assert.equal(decode("Zqd xnt njzx?", -1), "Are you okay?"); assert.equal(decode("oannLxmnLjvy", 9), "freeCodeCamp");

种子代码(seed-contents)是一个只返回原文的占位实现:

function decode(message, shift) { return message; }

解题目标就是替换return message;,让上述四个断言全部通过。

核心原理:字符编码差值 + 取模消除

字母在 Unicode 中是连续区间

ASCII/Unicode 中,'A''Z''a''z'各是一个连续编号区间('A'的码位是 65,'a'是 97)。因此字母可以无损地映射为 0–25 的"槽位":

const base = char === char.toLowerCase() ? 'a'.charCodeAt(0) : 'A'.charCodeAt(0); // base 为 97(小写)或 65(大写) const slot = char.charCodeAt(0) - base; // 0 表示 a/A,25 表示 z/Z

编码与解码互为反向平移

设明文字母的槽位为p,编码后的槽位为c,则:

  • 正偏移编码:c = (p + shift) % 26,所以解码要减去shiftp = (c - shift) mod 26
  • 负偏移编码:c = (p - |shift|) % 26,解码要加上|shift|——同样可写成(c - shift) mod 26(此时shift为负,减负等于加正)

统一公式就是p = (c - shift) mod 26,这正是题目文档中官方解答采用的形式:

const offset = (charCode - base - shift + 26) % 26;

这里有一个 JavaScript 特有的关键细节:%取余保留被除数的符号,负数取余会得到负值(如(-3) % 26 === -3)。因此在减shift之后先+ 26,保证表达式落为非负数再取模,从而得到 0–25 的合法槽位。+ 26足以覆盖单个字母一次平移的边界,因为c - shift ∈ (-26, 26)区间内时加一次 26 必为非负。

延伸:若要支持任意大小的shift(例如shift = 100),需要先把shift归一化:shift = ((shift % 26) + 26) % 26。题目给出的shift = 20在 (-26, 26) 内,所以官方解答无需这一步。

参考解答逐行拆解

题目文档 solutions 小节给出的官方解答:

function decode(message, shift) { return message.split('').map(char => { if (/[a-zA-Z]/.test(char)) { const base = char === char.toLowerCase() ? 'a'.charCodeAt(0) : 'A'.charCodeAt(0); const charCode = char.charCodeAt(0); const offset = (charCode - base - shift + 26) % 26; return String.fromCharCode(base + offset); } else { return char; } }).join(''); }

逐行说明:

  1. message.split(''):把字符串拆成单字符数组,配合map对每个字符独立处理——这是"逐字符变换 + 重组"的函数式惯用写法;
  2. /[a-zA-Z]/.test(char):只处理纯 ASCII 字母。非字母(空格、.!?等)走else分支原样返回,满足约束 4。题目数据只含 ASCII 字母,所以用[a-zA-Z]而非\p{L}是精确且安全的;
  3. char === char.toLowerCase():判断大小写并选定基准码base(97 或 65)。这一步保证解码后的字符与密文字符大小写一致,满足约束 3;
  4. (charCode - base - shift + 26) % 26:核心槽位计算,如上一节所述;
  5. String.fromCharCode(base + offset):把槽位映射回字符,大小写由base决定;
  6. .join(''):把字符数组拼回字符串。

用第一组用例手工验证一下shift = 4时的'X'

  • 'X'.charCodeAt(0)= 88,base= 65,槽位c= 23(X 是第 24 个字母);
  • offset = (23 - 4 + 26) % 26 = 45 % 26 = 19
  • String.fromCharCode(65 + 19)='T'。✓

负偏移用例'Z'shift = -1):offset = (25 - (-1) + 26) % 26 = 52 % 26 = 0'A',即"Zqd" → "Are"。✓

大偏移用例shift = 20'B'offset = (1 - 20 + 26) % 26 = 7 % 26 = 7'H'。✓ 可见 20 位的"前移编码"等价于解码时"后移 6 位",模 26 的周期性让两者完全等价。

验证思路与等价写法

由于submitTypes[dailyChallengeJs] === 'tests',本题的通过标准就是四组assert.equal全部为真。自行验证时可以直接把测试用例粘到 Node 或浏览器控制台跑一遍;仓库中同类挑战的数据在入库时还会经过客户端侧的 Joi 结构校验(参见 daily-coding-challenge-validator.ts,其中tests数组要求每项必须含texttestString两个字段,testString正是上述assert.equal(...)字符串),这从管线角度印证了"断言即判题"的设计。

在等价实现层面,还有几种常见变体,供对照理解:

  • 查表法:预构造'abcdefghijklmnopqrstuvwxyz'字符串,用indexOf定位槽位,再按下标取字符;
  • 正则替换法message.replace(/[a-zA-Z]/g, fn),跳过显式split/map/join
  • 先归一化 shiftshift = ((shift % 26) + 26) % 26,之后解码公式统一为(slot - shift + 26) % 26

三者与官方解答在题目给定的输入域(ASCII 字母 + 整数 shift)内行为一致;官方解答的优势在于不引入额外数据结构、一行表达式完成核心计算。

小结

这道 Message Decoder 是 freeCodeCamp JavaScript 每日挑战题库中的一个典型字符串处理题,其教学价值集中在三个可迁移的技能点:用charCodeAt/fromCharCode在字符与数字之间自由转换、用% 26实现字母表环绕、以及正确处理 JavaScript 取余的负数语义。题目文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68216ef80f957572e7c340c5.md,挑战类型与判题方式定义在 packages/shared/src/config/challenge-types.ts,块级排序与编辑器配置见 curriculum/structure/blocks/daily-coding-challenges-javascript.json,可按此路径继续深入研究该题库的组织方式。

【免费下载链接】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/8 22:10:41

AI编程助手实战:用Claude Code提速开发全流程

1. 快速原型:从零到可运行看板只花了一个午休做开发这几年,我见过太多好想法死在“写代码太慢”这一步。需求评审时说得头头是道,一落到代码上,光搭项目骨架、配路由、连数据库就能磨掉一整天。直到我把 Claude Code 正式用在日常…

作者头像 李华
网站建设 2026/9/8 22:09:02

基于ResNet的水果图像分类系统实战:从数据准备到部署

简介:基于深度残差网络(ResNet)的水果分类识别系统完整代码包,面向具备一定Python基础、希望快速落地图像分类项目的开发者与学生,尤其适合需要完成课程设计、毕业设计或工程演示的入门者。项目以水果分类为例&#xf…

作者头像 李华
网站建设 2026/9/8 22:06:28

小家电定时芯片选型指南:从RC电路到SOP8三档定时芯片

小家电定时功能做了十几年,我最大的感受是:方案越传统,产线越遭罪。前阵子帮客户优化一款酸奶机的定时板,原来用分立元件搭的RC定时电路,一颗定时电阻一个可调电位器,光定时部分就占了七八个元件&#xff0…

作者头像 李华