[191].位1的個數

11 的個數

 


題目

編寫一個函數,輸入是一個無符號整數,返回其二進制表達式中數字位數爲 ‘1’ 的個數(也被稱爲漢明重量)。

示例 1:

輸入:00000000000000000000000000001011
輸出:3
解釋:輸入的二進制串 00000000000000000000000000001011 中,共有三位爲 '1'

示例 2:

輸入:00000000000000000000000010000000
輸出:1
解釋:輸入的二進制串 00000000000000000000000010000000 中,共有一位爲 '1'

示例 3:

輸入:11111111111111111111111111111101
輸出:31
解釋:輸入的二進制串 11111111111111111111111111111101 中,共有 31 位爲 '1'

 


函數原型

C的函數原型:

int hammingWeight(uint32_t n) {}

 


邊界判斷

int hammingWeight(uint32_t n) {
    if( n <= 0 )
        return 0;
}

 


算法設計:枚舉

思路:挨個數一遍,32個數每位數都看一下是否等於1(通過不斷右移),左移的過程中把1的個數記下來即可。

int hammingWeight(uint32_t n) {
    if( n <= 0 )
        return 0;
     
    int cnt = 0;
    while( n != 0 ){
        if( n % 2 == 1 )    // \ 
        是二進制,所以 %2,看看餘數是否爲 1
            cnt ++;
        n /= 2;            // \
        因爲題目是二進制數,所以右移一位是除以 2 
	}
	return cnt;
}

代碼方面可以優化下:

n % 2 和 n & 1 是一樣的,但 &% 快。  
// n % m 時,只要 m 是 2的冪,那 n & (m-1) = n % m

n /= 2, 也可以改爲位運算,n >>= 1;

完整代碼:

int hammingWeight(uint32_t n) {
    if( n <= 0 )
        return 0;
     
    int cnt = 0;
    while( n != 0 ){
        if( n & 1 == 1 )    // \ 
        %2,看看餘數是否爲 1
            cnt ++;
        n >>= 1;            // \
        因爲題目是二進制數,所以右移一位是除以 2 
	}
	return cnt;
}

枚舉的複雜度:

  • 時間複雜度:Θ(1)\Theta(1),雖然說是枚舉,但無論什麼情況都是一個常數32。
  • 空間複雜度:Θ(1)\Theta(1)
     

算法設計:判斷一個數字是否是2的冪

我們不再檢查數字的每一個位,而是不斷把數字最後一個 1 反轉。

1000 00001000~0000(1後面七個0)和 0111 11110111~1111(0後面七個1)大小隻相差11,具體說後者是前者減11

這裏關鍵的想法是對於任意數字 nn ,將 nnn1n - 1 做與運算,會把最後一個 11 的位變成 00

爲什麼?

考慮 nnn1n - 1 的二進制表示。

這兩個數字所有的位數都不相同,1100 正好相反,在計算機裏判斷兩個數是否所有的位數都相反只需要一次操作。

比如1000 10001000~1000,它就是 272的7次方 + 232的3次方。也就是說,可以通過兩次判斷判別出來。

  • n       =1000 1000n ~~~~~~~= 1000~1000,(十進制的 136136)
  • n1=1000 0111n-1 = 1000~0111,(十進制的 135135)

n&(n1)=1000 1000 & 1000 0111=1000 0000n \&(n-1) =1000~1000~\&~1000~0111=1000~0000

進行與運算一次後,就去掉了最小的一個 1

那第二次會怎麼樣呢?

第二次是拿第一次與運算的結果 1000 00001000~0000 算。

n&(n1)=1000 0000 & 0111 1111=0000 0000n \&(n-1) =1000~0000~\&~0111~1111=0000~0000

您看,與倆次後,n 裏面的倆個 1 就全部消除了。

推而廣之,如果一個二進制數字中有N個1,只要判斷N次就知道有多少個1,而不需要把二進制的每一位都看一遍。

這種方法的本質實際上是判斷出一個數字是否是2的整數次方

int hammingWeight(uint32_t n) {
    if( n <= 0 )
        return 0;
    
    int cnt = 0;
    while( n != 0 ){
        n &= (n-1);   // n = n&(n-1)
        cnt ++;
    }
    return cnt;
}

這是一道微軟和谷歌經常考的面試題,有經驗的面試官每個問題的背後,都隱藏着TA想了解的關於面試者的職業相關能力。

透過題目,能看清面試者解決問題的思路和應用所學知識的水平。

其次這些題也會在工作中遇到,所以也有實際價值的。

做這種面試題,一定要超出預期、一般化~

也就是這道題的考點,比如這個是【漢明重量】,如果面試者想超出面試官的預期,那實現的程序不僅是在二進制的情況下,還可以推廣到其它進制。

判斷一個數字是否是2的冪的複雜度:

  • 時間複雜度:Θ(1)\Theta(1),與 1 的個數相關。
  • 空間複雜度:Θ(1)\Theta(1)
     

算法設計:查表法

微軟給出的答案:空間換時間。

這張圖存儲的範圍是 0256280-256 即 2^{8},而我們題目要求的是 32位 不是 8位 呀,應該應該開一個 2322^{32} 的數組,大概 4G

空間換時間,簡單地講,就是對所有的二進制編一張大表,表中直接記錄這個二進制中有多少個1即可,比如10001000有兩個111011011有六個1

這種算法,只要進行一次操作,就可以知道一個二進制數中有多少個1了,因此比上述的算法還快。

然而,凡事都有成本,這種查表的做法在節省時間的時候顯然要用其他資源作代價,那就是存儲這張大表所需要的空間。

如果是32位數,則需要4GB的存儲空間,如果放在計算機裏,一半的內存就沒了。

如果是64位,全世界所有計算機的內存都用上也放不下。

您可能會奇怪爲什麼增長這麼多,這就是指數增長的可怕之處。

當然,也有補救的法子。

因爲佔用內存太多。

如果把32位數字變成兩組16位的數字,建立一個16位數字的表格,32位數字前後查表兩次,然後把兩次的結果加起來即可。

16位數字的表格只佔64K空間,只是4G的零頭。

當然,還可以將32位二進制數變成四個八位數(也就是四個字節),對八位數建表,只需要256個字節的存儲單元就夠了,這樣需要查表四次,做三次加法。
 


在真實的計算機中,內存和處理器之間還有一個高速緩存,程序和數據要先從內存進入高速緩存,才能運行。高速緩存的空間非常有限,通常只有幾兆,4G內存上的內容是緩存容量的上千倍,肯定是放不下的,遇到這種情況,計算機本身要進行上千次額外操作,把內存的內容往緩存倒騰。

也就是說,如果建立一個大表,雖然查表只需要做一次,但是準備工作可能要做上千次。

如果您對上面的解釋還不是很理解,不妨看一眼下表,給出了建立32位二進制大表、16位二進制中表和8位二進制小表所需要的內存資源,和所需要的計算操作次數。

從這個表中你可以看出建立大表的方法只是在理論上有效(操作次數是1),在實際工程上根本不可行(準備次數太高)。

顯然,要回答好這道面試題,光是把計算機算法學好還不夠,還要對計算機系統結構有精深的瞭解。

查表法的複雜度:

  • 時間複雜度:Θ(1)\Theta(1)
  • 空間複雜度:Θ(2n)\Theta(2^{n})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章