简单总结一些用 JavaScript 刷力扣的基本调试技巧。最近又刷了点题,总结了些数据结构和算法,希望能对各为 JSer 刷题提供帮助。
此篇文章主要想给大家一些开箱即用的 JavaScipt 版本的代码模板,涉及到较复杂的知识点,原理部分可能会省略,有需要的话后面有时间可以给部分知识点单独写一篇详细的讲解。
BigInt
众所周知,JavaScript 只能精确表达 Number.MIN_SAFE_INTEGER(-2^53+1)
~ Number.MAX_SAFE_INTEGER(2^53-1)
的值。
而在一些题目中,常常会有较大的数字计算,这时就会产生误差。举个栗子:在控制台输入下面的两个表达式会得到相同的结果:
>> 123456789*123456789 // 15241578750190520
>> 123456789*123456789+1 // 15241578750190520
而如果使用 BigInt 则可以精确求值:
>> BigInt(123456789)*BigInt(123456789) // 15241578750190521n
>> BigInt(123456789)*BigInt(123456789)+BigInt(1) // 15241578750190522n
可以通过在一个整数字面量后面加 n
的方式定义一个 BigInt
,如:10n
,或者调用函数 BigInt()
。上面的表达式也可以写成:
>> 123456789n*123456789n // 15241578750190521n
>> 123456789n*123456789n+1n // 15241578750190522n
BigInt
只能与 BigInt
做运算,如果和 Number
进行计算需要先通过 BigInt()
做类型转换。
BigInt
支持运算符,+
、*
、-
、**
、%
。除 >>>
(无符号右移)之外的位操作也可以支持。因为 BigInt
都是有符号的, >>>
(无符号右移)不能用于 BigInt
。BigInt
不支持单目 (+
) 运算符。
BigInt
也支持 /
运算符,但是会被向上取整。
const rounded = 5n / 2n; // 2n, not 2.5n
取模运算
在数据较大时,一般没有办法直接去进行计算,通常都会给一个大质数(例如,1000000007
),求对质数取模后的结果。
取模运算的常用性质:
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a * b) % p = (a % p * b % p) % p
a ^ b % p = ((a % p) ^ b) % p
可以看出,加/减/乘/乘方,都可直接在运算的时候取模,至于除法则会复杂一些,稍后再讲。
举一个例子,LeetCode 1175. 质数排列
请你帮忙给从
1
到n
的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从1
开始)上;你需要返回可能的方案总数。让我们一起来回顾一下「质数」:质数一定是大于
1
的,并且不能用两个小于它的正整数的乘积来表示。由于答案可能会很大,所以请你返回答案 模 mod
10^9 + 7
之后的结果即可。
题目很简单,先求出质数的个数 x
,则答案为 x!(n-x)!
(不理解的可以去看题解区找题解,这里就不详细解释了)
由于阶乘的值很大,所以在求阶乘的时候需要在运算时取模,同时这里用到了上面所说的BigInt
。
/** * @param {number} n
* @return {number} */
var numPrimeArrangements = function(n) {
const mod = 1000000007n;
// 先把100以内的质数打表(不想再写判断质数的代码了
const prime = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97];
// 预处理阶乘
const fac = new Array(n + 1);
fac[0] = 1n; // 要用bigint
for (let i = 1; i <= n; i++) {
fac[i] = fac[i - 1] * BigInt(i) % mod;
}
// 先求n以内的质数的个数
const x = prime.filter(i => i <= n).length;
// x!(n-x)!
return fac[x] * fac[n - x] % mod;
};
快速幂
快速幂,顾名思义,快速求幂运算。原理也很简单,比如我们求 x^10
我们可以求 (x^5)^2
可以减少一半的运算。
假设我们求 (x^n)
- 如果
n
是偶数,变为求(x^(n/2))^2
- 如果
n
是奇数,则求(x^⌊n/2⌋)^2 * x
(⌊⌋
是向下取整)
因为快速幂涉及到的题目一般数据都很大,需要取模,所以加了取模运算。其中,代码中 n>>=1
相当于 n=n/2
,if(n&1)
是在判断n
是否为奇数。
代码如下:
// x ^ n % mod
function pow(x, n, mod) {
let ans = 1;
while (n > 0) {
if (n & 1) ans = ans * x % mod;
x = x * x % mod;
n >>= 1;
}
return ans;
}
乘法逆元(数论倒数)
上面说了除法的取模会复杂一些,其实就是涉及了乘法逆元。
当我们求 (a/b)%p
你以为会是简单的 ((a%p)/(b%p))%p
?当然不是!(反例自己想去Orz
假设有 (a*x)%p=1
则称 a
和x
关于p
互为逆元(a
是 x
关于 p
的逆元,x
是 a
关于 p
的逆元)。比如:2*3%5=1
则 2
和 3
关于 5
互为逆元。
我们把 a
的逆元用 inv(a)
表示。那么:
(a/b) % p
= ( (a/b) * (b*inv(b)) ) % p // 因为(b*inv(b))为1
= (a * inv(b)) % p
= (a%p * inv(b)%p) % p
现在通过逆元神奇的把除法运算变没了~~~
问题在于怎么求乘法逆元。有两种方式,费马小定理 和 扩展欧几里德算法
不求甚解的我只记了一种解法,即费马小定理:a^(p-1) ≡ 1 (mod p)
由费马小定理我们可以推论:a^(p-2) ≡ inv(a) (mod p)
数学家的事我们程序员就不要想那么多啦,记结论就好了。即:
a
关于p
的逆元为a^(p-2)
好了,现在可以