肝不动业务代码的时候,就时不时地做个题吧~
¶题目要求
维基百科中的“汉明距离”:
在信息论中,两个等长字符串之间的汉明距离(英语:Hamming distance)是两个字符串对应位置的不同字符的个数。换句话说,它就是将一个字符串变换成另外一个字符串所需要替换的字符个数。
题干:现在求两个整数 x
和 y
之间的汉明距离,其中,0 ≤ x, y < 2^31。
¶思路
根据汉明距离的定义、再结合原题目中的“Explanation”部分,可以得知我们需要对整数 x
,y
对应的二进制数逐位累加差异——统计得到的不相同的位的个数,即为所求。
- 思路一
- 我们要么先将整数转换成二进制——但是由于 JavaScript 只有一个 Number 类型用来表示数字,所以二进制数只能用别的方式代为表示——e.g.
4
的二进制数可以表示为字符串(4).toString(2); // "100"
,或者数组[1,0,0]
(数组好像没有直接的转换方法?)。 - 再对字符串或者数组中的“二进制值”做求解/求和。
- 我们要么先将整数转换成二进制——但是由于 JavaScript 只有一个 Number 类型用来表示数字,所以二进制数只能用别的方式代为表示——e.g.
- 思路二
- 虽然 JavaScript 不能很好地支持二进制数的表示,但是 JS 中天然有位操作符——其中,异或
^
恰好能满足我们的需求!- 如果
x
异或y
得到z
(z = x ^ y
),那么z
对应的二进制表示中 1 的个数即为所求。
- 如果
- 故再逐位求和即可:
- 首先我们来看看,一个变量和 1 做与操作(
&
)会有什么现象:val & 1
,如果val
的最右一位是 1 那么结果是 1 ;val & 1
,如果val
的最右一位是 0 那么结果是 0 。
- 此时我们至少能判断最右一位的 01 情况了。
- 那么再结合右移位操作
>>
来不断使高位逐个变成最右位,我们就能计算一个二进制数的所有位的 01 情况!
- 首先我们来看看,一个变量和 1 做与操作(
- 虽然 JavaScript 不能很好地支持二进制数的表示,但是 JS 中天然有位操作符——其中,异或
其实按照上述思路来看,整体是分成两个步骤的:
- 得到差异
- 累加差异
两个步骤均可以用一下两种方式二选一解决:
- 位操作
- 其他数据结构,比如字符串
所以可以 2 * 2 = 4
组合出四种大致思路。而第一步的“得到差异”个人比较推荐的做法是用异或一步到位。(后续实现中的第一步均采取了异或。)
更多的 JS 相关的位操作符请参考 Bitwise operators @MDN。
¶代码实现
¶实现一
先异或再转字符串最后通过 match
方法(正则)计数的实现:
1 | /** |
提交详情:
¶实现二
先异或再转字符串最后通过 split
方法计数的实现:
1 | /** |
提交详情:
¶实现三
完全的位操作实现:
1 | /** |
提交详情:
¶结果分析
其实第一个提交详情的图里的 runtime 和 distribution 不太可信,因为我第一次截实现一的详情图的时候,结果是“Your runtime beats 94.43 % of javascript submissions.”,后来我重新 submit 再打开之后,就变成“99.80 %”了……然后为了满足自己的虚荣心,贴了第二次的图(不要打我)。
¶其他解法
1 | /** |
第一眼看到这个排名极靠前、完全位操作实现的解法的时候,虽然看不懂,但是可以看出这个 accepted 的解法中的 while 在最少的循环次内就得到了结果。
因为在我的完全位操作的实现(实现三)中,当最高位是 1 的那一位在越高位的时候,while 就会循环越多次,如果最高位的 1 在第 M 位(M >= 0),那么循环条件将执行 M+1 次,循环体将执行 M 次,即很多是 0 的位也被列入总和—— 0 虽然并不会对总和产生影响,但是多执行的代码会增加时间上的开销。
而上面这个解法中能够只执行 count
次就结束循环,堪称完美!那么,现在我们来看看最为关键的代码 n = n & (n - 1)
有什么奥秘。
二进制数减一是一个奇妙的操作——当一个二进制数减一的时候,低位的 0 会变成 1,直到遇到一个最低位的 1 被减成 0。假设这个数 n 中最低位的 1 位于第 m 位(m >= 0),最高位的 1 位于第 M 位,最高位为第 N 位。那么此时,0~m 位各位上的数字都做了取反操作(包含一个 m 位的 1 和 0 ~ m-1 位的所有 0),而 m+1 ~ N 位各位上的数字都保持不变,即数 n 与上 (n - 1) 会导致 0~m 位均变成 0 ,这个过程中影响到了最低位(m 位上的一个 1)。即,做一次 n = n & (n - 1)
的操作会使得二进制数少一个最低位上的 1。
特别的,二进制数中只有一个 1 的时候,n & (n - 1) // == 0
。由此 n > 0 && (n & (n - 1))
也常用于判断整数 n 是不是 2 的指数:
1 | function isPowerOfTwo(n){ |
关于 JS 中整数的判断,ES5 及以前请看 How to check if a variable is an integer in JavaScript? @SO,ES6 及以后可以用 Number.isInteger()。
¶相关补充
刚好最近在看《Effective JavaScript》这本书,书中第二条——“理解 JavaScript 的浮点数”,有一些相关知识。
¶JS 中的数字
JavaScript 中的数字(number)都是 64 位双精度浮点数,即 double。JS 中的整数仅仅是其一个子集,整数的范围在 [-2^53, 2^53]。
¶Safe Integer
Safe integer 是符合如下描述的整数:
- 能被精确地表示为一个 IEEE-754 双精度浮点数
- 这个表示不能是其他整数的舍入结果
所以,2^53 虽然能被 IEEE-754 双精度浮点数精确表示,但是由于 2^53 + 1 在向零舍入和就近舍入中会被舍入为 2^53 ,所以不符合 safe integer 的要求,用 Number.isSafeInteger
判断会得到 false :
¶JS 中的位运算
位运算符的工作原理:
1 | 标准的 JS 浮点数 =隐式转换=> 32 位的有符号整数 =做位运算后返回=> 标准的 JS 浮点数 |
说到 32 位有符号整数,比较容易想到 C 语言中的 int
类型。那么也就稍微可以理解下题目中给出的 x
和 y
的范围 [0,2^31)
( 32 位有符号整数的非负整数范围)了。
¶小结
这虽然是 leetcode 上最简单的一道题目,但是我还是有所收获,特别是对 二进制数中 1 的个数的求解方法。