前面几篇讲了 JavaScript 的语法、函数和数据结构,已经够你看懂并写出大部分算法代码了。这篇补充一些在刷算法题时经常会用到的实用技巧,都是一些零散但重要的知识点。
ACM 模式 I/O 模板
很多 OJ 平台(包括本站)使用 ACM 模式,需要你自己处理输入输出。前面基础语法篇简单介绍过 readline,这里给出刷题时可以直接套用的完整模板。
readline 完整模板
核心思路就是先把所有输入收集起来,等输入结束后再统一处理。这是最通用、最不容易出错的方式:
这个模板你可以直接背下来,每次做题就往 rl.on("close", ...) 的回调里写逻辑就行。
处理多组测试用例
有些题目会给你多组测试用例,格式通常是第一行告诉你有几组数据,后面每组一行或多行。套路都一样——用一个变量追踪当前读到第几行:
这里有个小技巧:用 idx++ 来逐行读取。idx++ 的意思是"先返回当前值,再加 1",这样每次调用就自动移到下一行,写起来很简洁。
多行多数据的读取模板
再来看一个稍微复杂一点的场景:第一行给 n 和 m,接下来 n 行每行 m 个数,要读成一个二维数组:
注意这里用到了 .split(" ").map(Number) 这个组合拳——先按空格拆分成字符串数组,再用 map(Number) 把每个字符串转成数字。这是 ACM 模式输入处理的标配写法,非常常用。
排序
sort() + 比较函数
JavaScript 的 sort() 方法可以对数组进行原地排序。但这里有一个大坑:不传比较函数时,sort() 会把元素转成字符串来排序!
你看到了吧,100 排在 20 前面!这是因为字符串 "100" 的首字符 '1' 小于 "20" 的首字符 '2'。这个坑每年不知道坑倒多少人,数字排序必须传比较函数。
比较函数的规则很简单:
- 返回负数:
a排在b前面 - 返回正数:
b排在a前面 - 返回 0:顺序不变
所以 (a, b) => a - b 就是升序(a 小就返回负数,a 排前面),(a, b) => b - a 就是降序。
对象数组排序
刷题时经常需要对复杂数据排序,比如按照分数给学生排名:
原理完全一样,只不过比较的不再是元素本身,而是元素的某个属性。
多级排序
有时候需要按多个条件排序,比如先按分数降序,分数相同再按名字字典序升序:
多级排序的套路就是:先比较第一优先级,不相等就直接返回结果;相等了再比较第二优先级,以此类推。
注意字符串的比较不能直接用减法(字符串减法结果是 NaN),要用 < 和 > 来比较,然后手动返回 -1、1 或 0。
常用 Math 函数
基本数学函数
JavaScript 的 Math 对象提供了一大堆数学函数,刷题时最常用的就这几个:
其中 Math.floor() 要注意负数的行为:Math.floor(-3.7) 是 -4 而不是 -3,因为"向下"取整是往数轴左边走。如果你想要截断小数部分(-3.7 变成 -3),用 Math.trunc() 或者 parseInt()。
Math.max 求数组最大值
Math.max() 接受的是一个一个的参数,不能直接传数组。要对数组求最大值,用扩展运算符 ... 展开:
空数组传给 Math.max() 返回 -Infinity,传给 Math.min() 返回 Infinity,这两个特殊值在后面会讲到。
不过要注意,如果数组特别大(超过几万个元素),Math.max(...arr) 可能会因为参数太多而报错。这时候就老老实实用循环求最大值吧。
整数边界和 Infinity
JavaScript 的数字都是 64 位浮点数(IEEE 754),整数运算在一定范围内是精确的:
在刷算法题时,Infinity 和 -Infinity 经常用来初始化"最小值"和"最大值"。比如求数组最小值,先设 minVal = Infinity,然后遍历更新。因为任何有限数字都比 Infinity 小,所以第一次比较就会被更新为数组的实际值。
Number.MAX_SAFE_INTEGER 的值大约是 9 x 10^15,一般的算法题不会超出这个范围。如果真的需要处理更大的整数,JavaScript 还有 BigInt 类型,但在 OJ 中很少用到,先不展开。
实用技巧
类型转换速写
在基础语法篇我们学过 Number()、String() 这些显式转换函数。其实还有几个更简洁的速写方式,在刷题时经常看到:
总结一下:+str 转数字,num + "" 转字符串,!!val 转布尔。这些写法在别人的代码里经常出现,认识就行。自己写的时候,用 Number()、String() 也完全可以,看个人习惯。
解构交换
前面在"函数与对象"篇已经见过数组解构,这里再强调一个在算法题中用得最多的技巧——交换两个变量的值:
在写排序、交换元素等操作时,[a, b] = [b, a] 比用临时变量简洁太多了。
扩展运算符复制数组
同样在前面讲过,这里再把刷题中最常用的场景强调一下:
最后那个用 Set 去重的技巧很实用:先把数组扔进 Set(自动去重),再用 ... 展开回数组。一行搞定。
Array.from 创建数组的技巧
Array.from 是创建和初始化数组的一把好手,特别是创建二维数组时,是避坑的关键:
这里要特别警告一个常见的坑:创建二维数组千万不要用 new Array(3).fill(new Array(4).fill(0))!因为 fill 填进去的是同一个数组的引用,修改一行会影响所有行。用 Array.from 配合回调函数,每次回调都会创建一个新数组,才是正确的写法。
空值合并和可选链
这两个是 ES2020 引入的语法糖,代码里经常见到:
?? 和 || 的区别在于:?? 只在 null 和 undefined 时取默认值,而 || 在所有 falsy 值(0、""、false、null、undefined)时都会取默认值。在算法题中,如果你的值可能是 0 且 0 是合法值,就用 ?? 而不是 ||。
?. 可选链在处理嵌套对象时非常方便,不用写一堆 if 来检查中间属性是否存在。
进制转换
有些算法题会涉及进制相关的问题,了解标准库提供的进制转换方法,就不需要自己手搓转换逻辑了。JavaScript 可以用 toString(radix) 和 parseInt(string, radix) 在不同进制之间转换:
注意 (42).toString(2) 需要用括号把数字括起来,否则 . 会被解析为小数点。或者先存到变量里再调用 n.toString(2)。刷题中最常用的是二进制和十六进制的互转。
小结
至此,JavaScript 刷算法题需要的技巧就介绍完了。从基础语法、函数与对象、常用数据结构,到本篇的算法题常用技巧,你已经具备了用 JavaScript 刷算法题的全部语言基础。接下来就去本站的题目列表里开始实战吧,遇到不会的语法可以随时回来翻阅,多写多练,自然就熟了。
后面还有一篇选读内容:异步编程基础。如果你暂时只想刷算法题,可以先跳过。