原文首發(fā)于 baishusama.github.io,歡迎圍觀~
肝不動業(yè)務(wù)代碼的時候,就時不時地做個題吧/w\
題目要求
維基百科中的“漢明距離”:
在信息論中,兩個等長字符串之間的漢明距離(英語:Hamming distance)是兩個字符串對應(yīng)位置的不同字符的個數(shù)。換句話說,它就是將一個字符串變換成另外一個字符串所需要替換的字符個數(shù)。
題干:現(xiàn)在求兩個整數(shù) x
和 y
之間的漢明距離,其中,0 ≤ x, y < 2^31。
思路
根據(jù)漢明距離的定義、再結(jié)合原題目中的“Explanation”部分,可以得知我們需要對整數(shù) x
,y
對應(yīng)的二進(jìn)制數(shù)逐位累加差異——統(tǒng)計得到的不相同的位的個數(shù),即為所求。
- 思路一
- 我們要么先將整數(shù)轉(zhuǎn)換成二進(jìn)制——但是由于 JavaScript 只有一個 Number 類型用來表示數(shù)字,所以二進(jìn)制數(shù)只能用別的方式代為表示——e.g.
4
的二進(jìn)制數(shù)可以表示為字符串(4).toString(2); // "100"
,或者數(shù)組[1,0,0]
(數(shù)組好像沒有直接的轉(zhuǎn)換方法?)。 - 再對字符串或者數(shù)組中的“二進(jìn)制值”做求解/求和。
- 我們要么先將整數(shù)轉(zhuǎn)換成二進(jìn)制——但是由于 JavaScript 只有一個 Number 類型用來表示數(shù)字,所以二進(jìn)制數(shù)只能用別的方式代為表示——e.g.
- 思路二
- 雖然 JavaScript 不能很好地支持二進(jìn)制數(shù)的表示,但是 JS 中天然有位操作符——其中,異或
^
恰好能滿足我們的需求!- 如果
x
異或y
得到z
(z = x ^ y
),那么z
對應(yīng)的二進(jìn)制表示中 1 的個數(shù)即為所求。
- 如果
- 故再逐位求和即可:
- 首先我們來看看,一個變量和 1 做與操作(
&
)會有什么現(xiàn)象:-
val & 1
,如果val
的最右一位是 1 那么結(jié)果是 1 ; -
val & 1
,如果val
的最右一位是 0 那么結(jié)果是 0 。
-
- 此時我們至少能判斷最右一位的 01 情況了。
- 那么再結(jié)合右移位操作
>>
來不斷使高位逐個變成最右位,我們就能計算一個二進(jìn)制數(shù)的所有位的 01 情況!
- 首先我們來看看,一個變量和 1 做與操作(
- 雖然 JavaScript 不能很好地支持二進(jìn)制數(shù)的表示,但是 JS 中天然有位操作符——其中,異或
其實按照上述思路來看,整體是分成兩個步驟的:
- 得到差異
- 累加差異
兩個步驟均可以用一下兩種方式二選一解決:
- 位操作
- 其他數(shù)據(jù)結(jié)構(gòu),比如字符串
所以可以 2 * 2 = 4
組合出四種大致思路。而第一步的“得到差異”個人比較推薦的做法是用異或一步到位。(后續(xù)實現(xiàn)中的第一步均采取了異或。)
更多的 JS 相關(guān)的位操作符請參考 Bitwise operators @MDN。
代碼實現(xiàn)
實現(xiàn)一
先異或再轉(zhuǎn)字符串最后通過 match
方法(正則)計數(shù)的實現(xiàn):
/**
* @param {number} x
* @param {number} y
* @return {number}
*/
var hammingDistance = function(x, y) {
var xor = x ^ y;
var str = xor.toString(2);
var match = str.match(/1/g); // 用正則匹配計算個數(shù);match 為 null 或者數(shù)組。
return match ? match.length : 0 ;
};
提交詳情:
實現(xiàn)二
先異或再轉(zhuǎn)字符串最后通過 split
方法計數(shù)的實現(xiàn):
/**
* @param {number} x
* @param {number} y
* @return {number}
*/
var hammingDistance = function(x, y) {
var xor = x ^ y;
var str = xor.toString(2);
return str.split('1').length - 1; // 通過 split 計算某個字符(串)出現(xiàn)的個數(shù)
};
提交詳情:
實現(xiàn)三
完全的位操作實現(xiàn):
/**
* @param {number} x
* @param {number} y
* @return {number}
*/
var hammingDistance = function(x, y) {
var xor = x ^ y;
var sum = 0;
sum += xor & 1;
while(xor = xor >> 1){
sum += xor & 1;
}
return sum;
};
提交詳情:
結(jié)果分析
其實第一個提交詳情的圖里的 runtime 和 distribution 不太可信,因為我第一次截實現(xiàn)一的詳情圖的時候,結(jié)果是“Your runtime beats 94.43 % of javascript submissions.”,后來我重新 submit 再打開之后,就變成“99.80 %”了……然后為了滿足自己的虛榮心,貼了第二次的圖(不要打我)。
其他解法
/**
* @param {number} x
* @param {number} y
* @return {number}
*/
var hammingDistance = function(x, y) {
let n = x ^ y;
let count = 0;
while (n) {
n = n & (n - 1)
count++;
}
return count;
};
第一眼看到這個排名極靠前、完全位操作實現(xiàn)的解法的時候,雖然看不懂,但是可以看出這個 accepted 的解法中的 while 在最少的循環(huán)次內(nèi)就得到了結(jié)果。
因為在我的完全位操作的實現(xiàn)(實現(xiàn)三)中,當(dāng)最高位是 1 的那一位在越高位的時候,while 就會循環(huán)越多次,如果最高位的 1 在第 M 位(M >= 0),那么循環(huán)條件將執(zhí)行 M+1 次,循環(huán)體將執(zhí)行 M 次,即很多是 0 的位也被列入總和—— 0 雖然并不會對總和產(chǎn)生影響,但是多執(zhí)行的代碼會增加時間上的開銷。
而上面這個解法中能夠只執(zhí)行 count
次就結(jié)束循環(huán),堪稱完美!那么,現(xiàn)在我們來看看最為關(guān)鍵的代碼 n = n & (n - 1)
有什么奧秘。
二進(jìn)制數(shù)減一是一個奇妙的操作——當(dāng)一個二進(jìn)制數(shù)減一的時候,低位的 0 會變成 1,直到遇到一個最低位的 1 被減成 0。假設(shè)這個數(shù) n 中最低位的 1 位于第 m 位(m >= 0),最高位的 1 位于第 M 位,最高位為第 N 位。那么此時,0~m 位各位上的數(shù)字都做了取反操作(包含一個 m 位的 1 和 0 ~ m-1 位的所有 0),而 m+1 ~ N 位各位上的數(shù)字都保持不變,即數(shù) n 與上 (n - 1) 會導(dǎo)致 0~m 位均變成 0 ,這個過程中影響到了最低位(m 位上的一個 1)。即,做一次 n = n & (n - 1)
的操作會使得二進(jìn)制數(shù)少一個最低位上的 1。
特別的,二進(jìn)制數(shù)中只有一個 1 的時候,n & (n - 1) // == 0
。由此 n > 0 && (n & (n - 1))
也常用于判斷整數(shù) n 是不是 2 的指數(shù):
function isPowerOfTwo(n){
// better judge if n is an int at first..
if(n <= 0) return false;
return !(n&(n-1));
}
關(guān)于 JS 中整數(shù)的判斷,ES5 及以前請看 How to check if a variable is an integer in JavaScript? @SO,ES6 及以后可以用 Number.isInteger()。
相關(guān)補充
剛好最近在看《Effective JavaScript》這本書,書中第二條——“理解 JavaScript 的浮點數(shù)”,有一些相關(guān)知識。
JS 中的數(shù)字
JavaScript 中的數(shù)字(number)都是 64 位雙精度浮點數(shù),即 double。JS 中的整數(shù)僅僅是其一個子集,整數(shù)的范圍在 [-2^53, 2^53]。
Safe Integer
Safe integer 是符合如下描述的整數(shù):
- 能被精確地表示為一個 IEEE-754 雙精度浮點數(shù)
- 這個表示不能是其他整數(shù)的舍入結(jié)果
所以,2^53 雖然能被 IEEE-754 雙精度浮點數(shù)精確表示,但是由于 2^53 + 1 在向零舍入和就近舍入中會被舍入為 2^53 ,所以不符合 safe integer 的要求,用 Number.isSafeInteger
判斷會得到 false :
JS 中的位運算
位運算符的工作原理:
標(biāo)準(zhǔn)的 JS 浮點數(shù) =隱式轉(zhuǎn)換=> 32 位的有符號整數(shù) =做位運算后返回=> 標(biāo)準(zhǔn)的 JS 浮點數(shù)
說到 32 位有符號整數(shù),比較容易想到 C 語言中的 int
類型。那么也就稍微可以理解下題目中給出的 x
和 y
的范圍 [0,2^31)
( 32 位有符號整數(shù)的非負(fù)整數(shù)范圍)了。
小結(jié)
這雖然是 leetcode 上最簡單的一道題目,但是我還是有所收獲,特別是對 二進(jìn)制數(shù)中 1 的個數(shù)的求解方法。