LeetCode-461-Hamming-Distance

原文首發(fā)于 baishusama.github.io,歡迎圍觀~

肝不動業(yè)務(wù)代碼的時候,就時不時地做個題吧/w\

題目要求

原題目:461. Hamming Distance

維基百科中的“漢明距離”:

在信息論中,兩個等長字符串之間的漢明距離(英語:Hamming distance)是兩個字符串對應(yīng)位置的不同字符的個數(shù)。換句話說,它就是將一個字符串變換成另外一個字符串所需要替換的字符個數(shù)。

題干:現(xiàn)在求兩個整數(shù) xy 之間的漢明距離,其中,0 ≤ x, y < 2^31。

思路

根據(jù)漢明距離的定義、再結(jié)合原題目中的“Explanation”部分,可以得知我們需要對整數(shù) x,y 對應(yīng)的二進(jìn)制數(shù)逐位累加差異——統(tǒng)計得到的不相同的位的個數(shù),即為所求。

  1. 思路一
    • 我們要么先將整數(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)制值”做求解/求和。
  2. 思路二
    • 雖然 JavaScript 不能很好地支持二進(jìn)制數(shù)的表示,但是 JS 中天然有位操作符——其中,異或 ^ 恰好能滿足我們的需求!
      • 如果 x 異或 y 得到 zz = 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. 得到差異
  2. 累加差異

兩個步驟均可以用一下兩種方式二選一解決:

  • 位操作
  • 其他數(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)一的提交詳情

實現(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)三

完全的位操作實現(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;
};

提交詳情:

實現(xiàn)三的提交詳情

結(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 :

Math.isSafeInteger

JS 中的位運算

位運算符的工作原理:

標(biāo)準(zhǔn)的 JS 浮點數(shù) =隱式轉(zhuǎn)換=> 32 位的有符號整數(shù) =做位運算后返回=> 標(biāo)準(zhǔn)的 JS 浮點數(shù)

說到 32 位有符號整數(shù),比較容易想到 C 語言中的 int 類型。那么也就稍微可以理解下題目中給出的 xy 的范圍 [0,2^31) ( 32 位有符號整數(shù)的非負(fù)整數(shù)范圍)了。

小結(jié)

這雖然是 leetcode 上最簡單的一道題目,但是我還是有所收獲,特別是對 二進(jìn)制數(shù)中 1 的個數(shù)的求解方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容