小鬍子哥 @Barret李靖 給我推薦了一個寫演算法刷題的地方 leetcode.com,沒有 ACM 那麼難,但題目很有趣。而且據說這些題目都來源於一些公司的面試題。好吧,解解別人公司的面試題其實很好玩,既能整理思路鍛鍊能力,又不用擔心漏題 ╮(╯▽╰)╭。
長話短說,讓我們來看一道題:
統計“1”的個數
給定一個非負整數 num,對於任意 i,0 ≤ i ≤ num,計算 i 的值對應的二進位制數中 “1” 的個數,將這些結果返回為一個陣列。
例如:
當 num = 5 時,返回值為 [0,1,1,2,1,2]。
1 2 3 4 5 6 7 |
/** * @param {number} num * @return {number[]} */ var countBits = function(num) { //在此處實現程式碼 }; |
解題思路
這道題咋一看還挺簡單的,無非是:
- 實現一個方法
countBit
,對任意非負整數 n,計算它的二進位制數中“1”的個數 - 迴圈 i 從 0 到 num,求
countBit(i)
,將值放在陣列中返回。
JavaScript中,計算 countBit
可以取巧:
1 2 3 |
function countBit(n){ return n.toString(2).replace(/0/g,"").length; } |
上面的程式碼裡,我們直接對 n 用 toString(2) 轉成二進位制表示的字串,然後去掉其中的0,剩下的就是“1”的個數。
然後,我們寫一下完整的程式:
1 2 3 4 5 6 7 8 9 10 11 |
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) 的方法:
1 2 3 4 5 6 7 8 |
function countBit(n){ var ret = 0; while(n > 0){ ret += n & 1; n >>= 1; } return ret; } |
所以我們有了版本2
這麼實現也很簡潔不是嗎?但是這麼實現是否最優?建議此處思考10秒鐘再往下看。
更快的 countBit
上一個版本的 countBit
的時間複雜度已經是 O(logN) 了,難道還可以更快嗎?當然是可以的,我們不需要去判斷每一位是不是“1”,也能知道 n 的二進位制中有幾個“1”。
有一個訣竅,是基於以下一個定律:
- 對於任意 n, n ≥ 1,有如下等式成立:
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
於是,我們有了一個更快的演算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 次,而“版本2”的 countBit(88)
卻需要迴圈 7 次。
優化到了這個程度,是不是一切都結束了呢?從演算法上來說似乎已經是極致了?真的嗎?再給大家 30 秒時間思考一下,然後再往下看。
countBits 的時間複雜度
考慮 countBits
, 上面的演算法:
- “版本1” 的時間複雜度是 O(N*M),M 取決於 Number.prototype.toString 和 String.prototype.replace 的複雜度。
- “版本2” 的時間複雜度是 O(N*logN)
- “版本3” 的時間複雜度是 O(N*M),M 是 N 的二進位制數中的“1”的個數,介於 1 ~ logN 之間。
上面三個版本的 countBits
的時間複雜度都大於 O(N)。那麼有沒有時間複雜度 O(N) 的演算法呢?
實際上,“版本3”已經為我們提示了答案,答案就在上面的那個定律裡,我把那個等式再寫一遍:
1 |
countBit(n & (n - 1)) === countBit(n) - 1 |
也就是說,如果我們知道了 countBit(n & (n - 1))
,那麼我們也就知道了 countBit(n)
!
而我們知道 countBit(0)
的值是 0,於是,我們可以很簡單的遞推:
1 2 3 4 5 6 7 |
function countBits(nums){ var ret = [0]; for(var i = 1; i <= nums; i++){ ret.push(ret[i & i - 1] + 1); } return ret; } |
原來就這麼簡單,你想到了嗎 ╮(╯▽╰)╭
以上就是所有的內容,簡單的題目思考起來很有意思吧?程式設計師就應該追求完美的演算法,不是嗎?
這是 leetcode 演算法面試題系列的第一期,下一期我們討論另外一道題,這道題也很有趣:判斷一個非負整數是否是 4 的整數次方,別告訴我你用迴圈,想想更巧妙的辦法吧~
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式