- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
导读
本文围绕 Modern JavaScript Tutorial(本仓库 en.javascript.info)中「Map 与 Set」一节的经典练习「过滤变位词」(Filter anagrams 任务)展开,逐行剖析其官方解法 solution.md:通过「按字母排序后的字符串」作为 Map 键,将互为变位词的单词归入同一组并只保留一个代表词。读完本文,你将掌握Map的键去重覆盖语义、split → sort → join字符串规范化的核心套路,以及Map方案与普通对象方案的取舍,并看到仓库内置测试 test.js 如何验证这两项行为。
一、任务背景:什么是变位词,要过滤掉什么
「变位词(Anagram)」指拥有相同数量的相同字母、但排列顺序不同的单词。任务文档 task.md 给出的示例是:
nap - pan ear - are - era cheaters - hectares - teachers要求编写函数aclean(arr),返回一个去掉变位词后的数组,每个变位词组只保留任意一个单词即可:
let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); // "nap,teachers,ear" 或 "PAN,cheaters,era"注意两个关键点,它们在后面的解法和测试中都会体现:
- 大小写不敏感:
"PAN"与"nap"视为同一组变位词(注意示例输入中特意混入了大写); - 保留哪个单词不限:每组只需留下一个,可以是组内任意成员。
二、核心思路:把单词规范化为「字母排序形式」
官方解法 solution.md 的第一句话就点明了核心思想:
将每个单词拆成字母并排序,排序后的结果对所有变位词是相同的。
对每个单词做「字母排序」后,互为变位词的单词会收敛到同一个规范化形式:
nap, pan -> anp ear, era, are -> aer cheaters, hectares, teachers -> aceehrst这个规范化字符串就可以作为分组键(key)。这个思路不限于变位词,它是字符串归组类问题(如「去重」「找同构词」)的通用模式:先通过某种确定性变换得到规范形式,再以规范形式为键做一次归组。
三、Map 解法:以排序串为键,覆盖式去重
官方推荐的完整实现如下(与仓库内 solution.js 完全一致):
function aclean(arr) { let map = new Map(); for (let word of arr) { // 拆字母、排序、拼回:所有变位词得到同一个键 let sorted = word.toLowerCase().split('').sort().join(''); map.set(sorted, word); } return Array.from(map.values()); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) );整段代码只有四个动作:规范化 → 设键 → 遍历 → 取回值。下面逐步拆解。
3.1 关键一行:toLowerCase().split('').sort().join('')
字母排序由一行方法链完成,官方文档将其拆解为多行便于理解:
let sorted = word // PAN .toLowerCase() // pan .split('') // ['p','a','n'] .sort() // ['a','n','p'] .join(''); // anp四个步骤各司其职:
toLowerCase():先把单词统一转为小写,保证"PAN"与"nap"得到同一个规范化键'anp'——这是满足「大小写不敏感」要求的关键;split(''):按空字符串拆分,把单词打散为字符数组,这样才能调用数组的sort;sort():默认按 UTF-16 码元升序排序,字母会被排成规范顺序。注意:这里没有传比较函数,因此只适合处理纯英文字母等简单场景(Unicode 变体的注意事项见第五节);join(''):把排序后的数组拼回字符串,得到规范化键。
3.2 覆盖语义:map.set(sorted, word)自动去重
接下来map.set(sorted, word)把单词存入以规范化键命名的槽位:
map.set(sorted, word);这里利用了Map的一个关键特性——同键写入是覆盖式的。当后面再次遇到具有相同排序形式的单词时(例如先存了'nap',又遇到'PAN',两者键都是'anp'),set会用新值覆盖旧值。因此遍历完整个数组后,每个规范化键最多只对应一个单词,变位词组天然被压缩成单元素。官方解法原文对此的表述是:
如果之后再次遇到具有相同字母排序形式的单词,它会在 Map 中覆盖同键下的旧值。所以我们总能保证每个字母形式最多对应一个单词。
这正是Map相比「先收集全组再手动去重」的优雅之处:去重不再需要额外的includes或find检查,归组与去重由键的覆盖语义一步完成。
3.3 取回结果:Array.from(map.values())
最后一行把结果取回数组:
return Array.from(map.values());map.values()返回一个按插入顺序迭代值(即单词本身)的可迭代对象,键(排序串)在此处不再需要;Array.from(...)把这个可迭代对象转换为普通数组,正是aclean需要的返回值类型。
关于Map的遍历方法(keys()/values()/entries())及其保持插入顺序的特性,可参见本节主文章 Map and Set 中「Iteration over Map」部分的详细介绍。
四、另一种解法:用普通对象代替 Map
官方解法还给出了一条等价路径:因为规范化键是字符串,普通对象同样可以作为「字符串键 → 单词」的映射使用:
function aclean(arr) { let obj = {}; for (let i = 0; i < arr.length; i++) { let sorted = arr[i].toLowerCase().split("").sort().join(""); obj[sorted] = arr[i]; } return Object.values(obj); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) );两种写法一一对应:
| 环节 | Map 版本 | 对象版本 |
|---|---|---|
| 建容器 | let map = new Map() | let obj = {} |
| 循环方式 | for (let word of arr) | for (let i = 0; i < arr.length; i++) |
| 写入 | map.set(sorted, word) | obj[sorted] = arr[i] |
| 取结果 | Array.from(map.values()) | Object.values(obj) |
两者行为等价:对象的同名属性赋值同样是覆盖式的,Object.values(obj)也能直接取回所有保留的单词。官方文档给出的取舍建议很明确:
这里也可以用普通对象代替 Map,因为键是字符串。
什么时候选哪个?需要键为任意类型(对象、数字、布尔等)、或需要size、has、delete等便捷方法时选Map;本任务键恰好是字符串,对象写法更轻量。Map与Object的能力差异可参考 Map and Set 中的 Summary 对比。
五、仓库测试验证:两组断言锁定行为
仓库为该任务配备了可运行测试 test.js(与 solution.js 同目录),用 Mocha 的describe/it/assert从两个维度验证aclean:
断言一:每组变位词只保留一个
it("returns exactly 1 word from each anagram set", function() { let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; let result = aclean(arr); assert.equal(result.length, 3); // 三组变位词 -> 三个元素 assert.equal(intersection(result, ["nap", "PAN"]).length, 1); // nap/PAN 组只留 1 个 assert.equal(intersection(result, ["teachers", "cheaters", "hectares"]).length, 1); assert.equal(intersection(result, ["ear", "era"]).length, 1); });其中intersection是测试文件自带的辅助函数,用filter+includes计算两个数组的交集元素数量。断言精确刻画了「每组恰好留一个」的语义。
断言二:大小写不敏感
it("is case-insensitive", function() { let arr = ["era", "EAR"]; assert.equal(aclean(arr).length, 1); });"era"与"EAR"经toLowerCase()后得到同一规范化键'aer',最终只保留一个单词。这两个断言恰好与任务文档 task.md 中的示例输入和「大小写不敏感」要求一一对应,是解法正确性的直接代码级证据。
六、边界情况与复杂度
边界情况
- 空字符串与单字母:
''.split('')得[],sort()后仍为空数组,join('')得'',一切空串会归到同一个键;单字母单词的排序形式就是它自己。这两种情况都能被上述代码正确处理。 - 大小写混合:
toLowerCase()保证'PAN'、'Pan'、'nap'统一。 - 非英文字母 / Unicode:
sort()默认按 UTF-16 码元排序,对带重音字符(如é)或需要规范化(如fi连字、组合字符)的文本,排序结果可能不够直观;若业务场景需要,可额外考虑用比较函数或 Unicode 规范化做键,但这不是本任务的要求范围。
复杂度
从代码结构可以推断:aclean的时间开销主要来自两处——每个单词做一次O(k log k)的排序(k 为单词平均长度),以及整个数组的O(n)次set操作,因此总体时间复杂度为O(n · k log k),空间复杂度为O(n)(Map 中最多存 n 个键值对)。相比「对每个词再线性扫描已收集单词」的朴素做法,按键归组避免了二次方级别的重复比较。
七、小结:一个可复用的「规范化键」模式
aclean这道题的解法虽然短,却浓缩了两个可迁移到真实工程的知识点:
- 规范形式(canonical form):
split → sort → join把「字母集合相同但顺序不同」的单词归一为同一个键,这是字符串归组问题的通用预处理手段; - Map 的覆盖式键值语义:
map.set(key, value)遇同键自动覆盖,天然实现「每组取一」,配合Array.from(map.values())把结果还原成数组。
同时,仓库中的 solution.md、solution.js 与 test.js 三者共同构成了「任务 → 实现 → 验证」的完整闭环,也是本项目所有练习任务的标准组织方式(参见 01-array-unique-map 同构的任务目录)。建议读者在本地以node直接运行 test.js 与 solution.js,观察两组断言全部通过,即可确认对本题语义的理解无误。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
30 Seconds of Interviews 算法实战:用 JavaScript 将数组中的变位词(Anagram)分组输出
30 Seconds of Interviews 算法实战:用 JavaScript 将数组中的变位词(Anagram)分组输出 导读 本文基于 30 Seco
教程前端终极 Modern JavaScript Cheatsheet:掌握 map、filter、reduce 的完整指南
终极 Modern JavaScript Cheatsheet:掌握 map、filter、reduce 的完整指南 Modern JavaScript Che
JavaScript 数组区间过滤实战:用 filter 实现 filterRange 且不改动原数组(Modern JavaScript Tutorial)
JavaScript 数组区间过滤实战:用 filter 实现 filterRange 且不改动原数组(Modern JavaScript Tutorial)
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考