數據結構算法——位運算相關知識及示例

    理解位運算的第一步是理解二進制。二進制是指數字的每一位都是1或者0。比如十進制的2轉換成二進制之後是10,而十進制的10轉換成二進制之後是1010。在程序員圈子中有一則流傳了很久的笑話,說世界上有10種人,一種人知道二進制,而另一種人不知道二進制……

    其實二進制的位運算並不是很難掌握,位運算總共只有5種運算:與/或/異或/左移/右移。如下表:

    左移運算m << n 表示把 m 左移 n 位。左移 n 位的時候,將左邊的 n 位丟棄,同時在最右邊補上 n 個 0。比如:

00001010 << 2 = 0010100

10001010 << 3 = 01010000

    右移運算符 m >> n 表示把 m 右移 n 位。在右移 n 位時候,最右邊的 n 位將被丟棄,同時處理左邊位的情形稍微複雜一點。如果數字是一個無符號數值,則用 0 填補最左邊的 n 位;如果數字是一個有符號數值,則用數字的符號位填補最左邊的 n 位。也就是說如果數字原先是一個證書,則右移之後在最左邊補 n 個 0 ;如果數字原先是附屬,則右移之後在最左邊補 n 個1。 比如:

00001010 >> 2 = 00000010

10001010 >> 3 = 11110001

面試題:二進制中 1 的個數

題目: 請實現一個函數,輸入一個整數,輸出該數二進制表示中 1 的個數。例如: 把9表示成二進制是1001,有2位是1。因此,如果輸入9,則該函數輸出2。

可能引起死循環的解法

基本思路: 先判斷整數二進制表示中的最右邊一位是不是1:接着把輸入的整數右移一位,此時原來處於右邊數起的第二位被移到最右邊了,再判斷是不是1:這樣每次移動一位,知道整個整數變成0爲止。如果一個整數與1做與運算的結果是1,則表示該整數最右邊一位是1,否則是0。基於這種思路,代碼如下:

int NumberOf1(int n)
{
    int count = 0;
    while(n)
    {
        if ( n & 1){
            count++;
        }
        n = n >> 1;
    }
    return count;
}

    在上述代碼中,使用右移1位的運算代替除以2的操作在實際代碼運行時候效率更高。因爲除法的效率比移位運算要低得多,在實際編程中應儘可能地用移位運算符代替乘除法。然而上述代碼確沒有考慮到負數的情況。如果輸入一個負數,比如0x80000000,則運行時候會發生什麼情況?當把負數0x80000000右移一位的時候,並不是簡單地把最高位的1移到第二位變成0xC0000000。因爲移位前是一個負數,仍要保證移位後是一個負數,因此移位後的最高位會設爲1。如果一直做右移運算,那麼最終這個數字就會變成0xFFFFFFFF而陷入死循環。

常規解法

    爲了避免死循環,我們可以不右移輸入的數字 n 。首先把 n 和 1 做與運算,判斷 n 的最低位是不是1。接着把 1 左移一位得到2, 再和 n 做與運算,就能判斷 n 的次低位是不是 1 ……這樣反覆左移,每次都能判斷 n 的其中一位是不是1。基於這種思路,我們可以把代碼修改爲:

int NumberOf1(int n){
    int count =0;
    unsigned int flag =1;
    while(flag){
        if( n & flag){
            count++;
        }
        flag=flag << 1;
    }
    return count;
}

    注意在這個解法中循環的次數等於整數二進制的位數,32位的整數需要循環32次。即:循環終止的條件是flag爲0, 在32位操作系統上,unsigned int的最大賦值是2^31=2147483647,當2^32時候超過最大值flag被賦予0。

能給面試官帶來驚喜的解法

    常規解法的時間複雜度是O(32),需要循環32次,下面的解法只需要幾遍循環即可(有幾個1就循環幾次)。

    我們先分析一下把一個數減去1的情況。如果這個整數不等於0,那麼該整數的二進制表示中至少有一位是1。假設這個數的最右邊一位是1,那麼減去1時,最後一位變成0而其它位都保持不變。也就是最後一位相當於做了取反操作,由1變成了0。

    接下來假設最後一位不是1而是0的情況。如果該整數的二進制表示中最右邊的1位於第m位,那麼減去1時,第m位由1變成0,而第m位之後的所有0都變成1,整數中的第m位之前的所有位都保持不變。舉個例子:一個二進制數1100,它的第二位時從最右邊數起的第一個1。減去1後,第二位變成0,後面兩位0變成1,前面的1保持不變,因此得到的結果時1011。

    前面的兩種情況中,我們發現把一個整數減去1,都是把最右邊的1變成0,如果它的右邊還有0,則把所有的0都變成1,它左邊的所有位都保持不變。接下來我麼把一個整數和它減去1的結果做位與運算,相當於把它最右邊的1變成0。還是以前面的1100爲例,它減1的結果位1011。把1100和1011做位與運算,得到的結果時1000。我們把1100最右邊的1變成了0,結果剛好就是1000。

    把上面的分析總結起來就是:把一個整數減去1,再和原整數做與運算,會把該整數最右邊的1變成0。那麼一個整數的二進制表示中有多少個1,就可以進行多少次這樣的操作。基於這樣的思路,我們可以寫出新的代碼如下:

int NumberOf1(int n)
{
    int count=0;
    while(n){
        ++count;
        n = n & (n-1);
    }
    return count;
}

 相關拓展

  • 用一條語句判斷一個整數是不是2的整數次方。一個整數如果時2的整數次方,那麼它的二進制表示中有且只有一位時1,而其它所有位都是0。根據前面的分析,把這個整數減去1之後再和它自己做與運算,這個整數中唯一的的1就會變成0。
  • 輸入兩個整數m和n,計算需要改變m的二進制表示中的多少位才能得到n。比如10的二進制表示爲1010,13的二進制表示爲1101,需要改變1010中的3位才能得到1101。我們可以分兩步解決這個問題:第一步求這兩個數的異或;第二步統計異或結果中1的位數。

 

參考資料

《劍指offer第二版》

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章