別人家的面試題:統計「1」的個數

小鬍子哥@Barret李靖給我推薦了一個寫算法刷題的地方leetcode.com,沒有ACM那麼難,但題目頗有趣。並且聽說這些題目都來源於一些公司的面試題。好吧,解解別人公司的面試題其實很好玩,既能整理思路鍛鍊能力,又不用擔憂漏題 ╮(╯▽╰)╭。javascript

長話短說,讓咱們來看一道題java

統計「1」的個數

給定一個非負整數num,對於任意i,0 ≤ i ≤ num,計算i的值對應的二進制數中「1」 的個數,將這些結果返回爲一個數組。程序員

例如:web

當num = 5時,返回值爲[0,1,1,2,1,2]。面試

/** 
  * @param {number} num 
  * @return {number[]} 
  * /
var countBits = function(num) {
    //在此處實現代碼
};

解題思路

這道題咋一看還挺簡單的,無非是:算法

  • 實現一個方法countBit,對任意非負整數n,計算它的二進制數中「1」的個數數組

  • 循環i從0到num,求countBit(i),將值放在數組中返回。性能

JavaScript中,計算countBit能夠取巧:優化

function countBit(n){
    return n.toString(2).replace(/0/g,"").length;
}

上面的代碼裏,咱們直接對n用toString(2)轉成二進制表示的字符串,而後去掉其中的0,剩下的就是「1」的個數。prototype

而後,咱們寫一下完整的程序:

function countBit(n){
    return n.toString(2).replace(/0/g,'').length;
}
function countBits(nums){
    var ret = [];   
    for(var i = 0; i <= nums; i++){
        ret.push(countBit(i));
    }
    return ret;
}

上面這種寫法十分討巧,好處是countBit利用JavaScript語言特性實現得十分簡潔,壞處是若是未來要將它改寫成其餘語言的版本,就有可能懵B了,它不是很通用,並且它的性能還取決於Number.prototype.toString(2)和String.prototype.replace的實現。

因此爲了追求更好的寫法,咱們有必要考慮一下countBit的通用實現法。

咱們說,求一個整數的二進制表示中「1」的個數,最普通的固然是一個O(logN) 的方法:

function countBit(n){
    var ret = 0;
    while(n > 0){
        ret += n & 1;
        n >>= 1;
    }
    return ret;
}

這麼實現也很簡潔不是嗎?可是這麼實現是否最優?建議此處思考10秒鐘再往下看。


更快的countBit

上一個版本的countBit的時間複雜度已是O(logN) 了,難道還能夠更快嗎?固然是能夠的,咱們不須要去判斷每一位是否是「1」,也能知道n的二進制中有幾個「1」。

有一個訣竅,是基於如下一個定律:

  • 對於任意 n, n ≥ 1,有以下等式成立:

countBit(n & (n - 1)) === countBit(n) - 1

這個很容易理解,你們只要想一下,對於任意n,n – 1的二進制數表示正好是n的二進制數的最末一個「1」退位,所以n & n – 1正好將n的最末一位「1」消去,例如:

  • 6的二進制數是110, 5 = 6 – 1的二進制數是101,6 & 5的二進制數是110 & 101 == 100

  • 88的二進制數是1011000,87 = 88 – 1的二進制數是 1010111,88 & 87的二進制數是1011000 & 1010111 == 1010000

因而,咱們有了一個更快的算法:

function countBit(n){
    var ret = 0;
    while(n > 0){
        ret++;
        n &= n - 1;
    }
    return ret;
}
function countBits(nums){
    var ret = [];
    for(var i = 0; i <= nums; i++){
        ret.push(countBit(i));
    }
    return ret;
}

上面的countBit(88)只循環3次,而上一版本的countBit(88)卻須要循環7次。

優化到了這個程度,是否是一切都結束了呢?從算法上來講彷佛已是極致了?真的嗎?再給你們 30 秒時間思考一下,而後再往下看。


countBits的時間複雜度

考慮countBits, 上面的算法:

  • 最第一版本的時間複雜度是O(N*M),M取決於Number.prototype.toString和String.prototype.replace的複雜度。

  • 第二版本的時間複雜度是O(N*logN)

  • 最後版本的時間複雜度是O(N*M),M是N的二進制數中的「1」的個數,介於1 ~ logN之間。

上面三個版本的countBits的時間複雜度都大於O(N)。那麼有沒有時間複雜度O(N)的算法呢?

實際上,最後版本已經爲咱們提示了答案,答案就在上面的那個定律裏,我把那個等式再寫一遍:

countBit(n & (n - 1)) === countBit(n) - 1

也就是說,若是咱們知道了countBit(n & (n - 1)),那麼咱們也就知道了countBit(n)

而咱們知道countBit(0)的值是 0,因而,咱們能夠很簡單的遞推:

function countBits(nums){
    var ret = [0];
    for(var i = 1; i <= nums; i++){
        ret.push(ret[i & i - 1] + 1);
    }
    return ret;
}

原來就這麼簡單,你想到了嗎 ╮(╯▽╰)╭

以上就是全部的內容,簡單的題目思考起來頗有意思吧?程序員就應該追求完美的算法,不是嗎?

轉載整理自:http://web.jobbole.com

相關文章
相關標籤/搜索