位操作基礎篇之位操作全面總結

jiang199235jiangjj發表於2012-09-12

Title:       位操作基礎篇之位操作全面總結
Author:     MoreWindows
E-mail:      morewindows@126.com
KeyWord:   C/C++ 位操作 位操作技巧 判斷奇偶 交換兩數 變換符號 求絕對值 位操作壓縮空間 篩素數 位操作趣味應用 位操作筆試面試

位操作篇共分為基礎篇和提高篇,基礎篇主要對位操作進行全面總結,幫助大家梳理知識。提高篇則針對各大IT公司如微軟、騰訊、百度、360等公司的筆試面試題作詳細的解答,使大家能熟練應對在筆試面試中位操作題目。

      下面就先來對位操作作個全面總結,歡迎大家補充。

在計算機中所有資料都是以二進位制的形式儲存的。位運算其實就是直接對在記憶體中的二進位制資料進行操作,因此處理資料的速度非常快。

在實際程式設計中,如果能巧妙運用位操作,完全可以達到四兩撥千斤的效果,正因為位操作的這些優點,所以位操作在各大IT公司的筆試面試中一直是個熱點問題。因此本文將對位操作進行如下方面總結:

      一. 位操作基礎,用一張表描述位操作符的應用規則並詳細解釋。

      二. 常用位操作小技巧,有判斷奇偶、交換兩數、變換符號、求絕對值。

      三. 位操作與空間壓縮,針對篩素數進行空間壓縮。

      四. 位操作的趣味應用,列舉了位操作在高低位交換、二進位制逆序、二進位制中1的個數以及缺失的數字這4種趣味應用。

希望讀者能認真學習和親自上機輸入程式碼進行實驗,相信通過本文及適當的練習可以使你對位操作有更加深入的瞭解,在筆試面試中遇到位操作相關試題能更加從容。

一. 位操作基礎

基本的位操作符有與、或、異或、取反、左移、右移這6種,它們的運算規則如下所示:

符號

 描述

 運算規則                        by MoreWindows

&      

 與

兩個位都為1時,結果才為1

|  

 或    

兩個位都為0時,結果才為0

^    

異或

兩個位相同為0,相異為1

~   

取反

0變1,1變0

<< 

左移

各二進位全部左移若干位,高位丟棄,低位補0

>> 

右移

各二進位全部右移若干位,對無符號數,高位補0,有符號數,各編譯器處理方法不一樣,有的補符號位(算術右移),有的補0(邏輯右移)

注意以下幾點:

1.  在這6種操作符,只有~取反是單目操作符,其它5種都是雙目操作符。

2.  位操作只能用於整形資料,對float和double型別進行位操作會被編譯器報錯。

3.  對於移位操作,在微軟的VC6.0和VS2008編譯器都是採取算術稱位即算術移位操作,算術移位是相對於邏輯移位,它們在左移操作中都一樣,低位補0即可,但在右移中邏輯移位的高位補0而算術移位的高位是補符號位。如下面程式碼會輸出-4和3。

  1. int a = -15, b = 15;  
  2. printf("%d %d\n", a >> 2, b >> 2);  

因為15=0000 1111(二進位制),右移二位,最高位由符號位填充將得到0000 0011即3。-15 = 1111 0001(二進位制),右移二位,最高位由符號位填充將得到1111 1100即-4(見注1)。

4.  位操作符的運算優先順序比較低,因為儘量使用括號來確保運算順序,否則很可能會得到莫明其妙的結果。比如要得到像1,3,5,9這些2^i+1的數字。寫成int a = 1 << i + 1;是不對的,程式會先執行i + 1,再執行左移操作。應該寫成int a = (1 << i) + 1;

5.  另外位操作還有一些複合操作符,如&=、|=、 ^=、<<=、>>=。

 

二. 常用位操作小技巧

下面對位操作的一些常見應用作個總結,有判斷奇偶、交換兩數、變換符號及求絕對值。這些小技巧應用易記,應當熟練掌握。

1.判斷奇偶

只要根據最未位是0還是1來決定,為0就是偶數,為1就是奇數。因此可以用if (a & 1 == 0)代替if (a % 2 == 0)來判斷a是不是偶數。

下面程式將輸出0到100之間的所有奇數。

  1. for (i = 0; i < 100; ++i)  
  2.     if (i & 1)  
  3.         printf("%d ", i);  
  4. putchar('\n');  

2.交換兩數

一般的寫法是:

  1. void Swap(int &a, int &b)  
  2. {  
  3.     if (a != b)  
  4.     {  
  5.         int c = a;  
  6.         a = b;  
  7.         b = c;  
  8.     }  
  9. }  

可以用位操作來實現交換兩數而不用第三方變數:

  1. void Swap(int &a, int &b)  
  2. {  
  3.     if (a != b)  
  4.     {  
  5.         a ^= b;  
  6.         b ^= a;  
  7.         a ^= b;  
  8.     }  
  9. }  

可以這樣理解:

第一步  a^=b 即a=(a^b);

第二步  b^=a 即b=b^(a^b),由於^運算滿足交換律,b^(a^b)=b^b^a。由於一個數和自己異或的結果為0並且任何數與0異或都會不變的,所以此時b被賦上了a的值。

第三步 a^=b 就是a=a^b,由於前面二步可知a=(a^b),b=a,所以a=a^b即a=(a^b)^a。故a會被賦上b的值。
再來個例項說明下以加深印象。int a = 13, b = 6;

a的二進位制為 13=8+4+1=1101(二進位制)

b的二進位制為 6=4+2=110(二進位制)

第一步 a^=b  a = 1101 ^ 110 = 1011;

第二步 b^=a  b = 110 ^ 1011 = 1101;即b=13

第三步 a^=b  a = 1011 ^ 1101 = 110;即a=5

3.變換符號

變換符號就是正數變成負數,負數變成正數。

如對於-11和11,可以通過下面的變換方法將-11變成11

      1111 0101(二進位制) –取反-> 0000 1010(二進位制) –加1-> 0000 1011(二進位制)

同樣可以這樣的將11變成-11

      0000 1011(二進位制) –取反-> 0000 1010(二進位制) –加1-> 1111 0101(二進位制)

因此變換符號只需要取反後加1即可。完整程式碼如下:

  1. //by MoreWindows( http://blog.csdn.net/MoreWindows )    
  2. #include <stdio.h>  
  3. int SignReversal(int a)  
  4. {  
  5.     return ~a + 1;  
  6. }  
  7. int main()  
  8. {  
  9.     printf("對整數變換符號 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  10.     int a = 7, b = -12345;  
  11.     printf("%d  %d\n", SignReversal(a), SignReversal(b));  
  12.     return 0;  
  13. }  

4.求絕對值

位操作也可以用來求絕對值,對於負數可以通過對其取反後加1來得到正數。對-6可以這樣:

      1111 1010(二進位制) –取反->0000 0101(二進位制) -加1-> 0000 0110(二進位制)

來得到6。

因此先移位來取符號位,int i = a >> 31;要注意如果a為正數,i等於0,為負數,i等於-1。然後對i進行判斷——如果i等於0,直接返回。否之,返回~a+1。完整程式碼如下:

  1. //by MoreWindows( http://blog.csdn.net/MoreWindows )  
  2. int my_abs(int a)  
  3. {  
  4.     int i = a >> 31;  
  5.     return i == 0 ? a : (~a + 1);  
  6. }  

現在再分析下。對於任何數,與0異或都會保持不變,與-1即0xFFFFFFFF異或就相當於取反。因此,a與i異或後再減i(因為i為0或-1,所以減i即是要麼加0要麼加1)也可以得到絕對值。所以可以對上面程式碼優化下:

  1. //by MoreWindows( http://blog.csdn.net/MoreWindows )  
  2. int my_abs(int a)  
  3. {  
  4.     int i = a >> 31;  
  5.     return ((a ^ i) - i);  
  6. }  

注意這種方法沒用任何判斷表示式,而且有些筆面試題就要求這樣做,因此建議讀者記住該方法(^_^講解過後應該是比較好記了)。

 

三. 位操作與空間壓縮

篩素數法在這裡不就詳細介紹了,本文著重對篩素數法所使用的素數表進行優化來減小其空間佔用。要壓縮素數表的空間佔用,可以使用位操作。下面是用篩素數法計算100以內的素數示例程式碼(注2):

  1. //by MoreWindows( http://blog.csdn.net/MoreWindows )  
  2. #include <stdio.h>  
  3. #include <memory.h>  
  4. const int MAXN = 100;  
  5. bool flag[MAXN];  
  6. int primes[MAXN / 3], pi;  
  7. //對每個素數,它的倍數必定不是素數。  
  8. //有很多重複如flag[10]會在訪問flag[2]和flag[5]時各訪問一次  
  9. void GetPrime_1()  
  10. {  
  11.     int i, j;  
  12.     pi = 0;  
  13.     memset(flag, falsesizeof(flag));  
  14.     for (i = 2; i < MAXN; i++)  
  15.         if (!flag[i])  
  16.         {  
  17.             primes[pi++] = i;  
  18.             for (j = i; j < MAXN; j += i)  
  19.                 flag[j] = true;  
  20.         }  
  21. }  
  22. void PrintfArray()  
  23. {  
  24.     for (int i = 0; i < pi; i++)  
  25.         printf("%d ", primes[i]);  
  26.     putchar('\n');  
  27. }  
  28. int main()  
  29. {  
  30.     printf("用篩素數法求100以內的素數\n-- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");    
  31.     GetPrime_1();  
  32.     PrintfArray();  
  33.     return 0;  
  34. }  

執行結果如下:

在上面程式是用bool陣列來作標記的,bool型資料佔1個位元組(8位),因此用位操作來壓縮下空間佔用將會使空間的佔用減少八分之一。

下面考慮下如何在陣列中對指定位置置1,先考慮如何對一個整數在指定位置上置1。對於一個整數可以通過將1向左移位後與其相或來達到在指定位上置1的效果,程式碼如下所示:

  1. //在一個數指定位上置1  
  2. int j = 0;  
  3. j |=  1 << 10;  
  4. printf("%d\n", j);  

同樣,可以1向左移位後與原數相與來判斷指定位上是0還是1(也可以將原數右移若干位再與1相與)。

  1.    //判斷指定位上是0還是1  
  2. int j = 1 << 10;  
  3. if ((j & (1 << 10)) != 0)  
  4.     printf("指定位上為1");  
  5. else  
  6.     printf("指定位上為0");  

擴充套件到陣列上,我們可以採用這種方法,因為陣列在記憶體上也是連續分配的一段空間,完全可以“認為”是一個很長的整數。先寫一份測試程式碼,看看如何在陣列中使用位操作:

  1. //by MoreWindows( http://blog.csdn.net/MoreWindows )    
  2. #include <stdio.h>  
  3. int main()  
  4. {  
  5.     printf("     對陣列中指定位置上置位和判斷該位\n");  
  6.     printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  7.     //在陣列中在指定的位置上寫1  
  8.     int b[5] = {0};  
  9.     int i;  
  10.     //在第i個位置上寫1  
  11.     for (i = 0; i < 40; i += 3)  
  12.         b[i / 32] |= (1 << (i % 32));  
  13.     //輸出整個bitset  
  14.     for (i = 0; i < 40; i++)  
  15.     {  
  16.         if ((b[i / 32] >> (i % 32)) & 1)  
  17.             putchar('1');  
  18.         else   
  19.             putchar('0');  
  20.     }  
  21.     putchar('\n');  
  22.     return 0;  
  23. }  

執行結果如下:

可以看出該陣列每3個就置成了1,證明我們上面對陣列進行位操作的方法是正確的。因此可以將上面篩素數方法改成使用位操作壓縮後的篩素數方法:

  1. //使用位操作壓縮後的篩素數方法  
  2. //by MoreWindows( http://blog.csdn.net/MoreWindows )   
  3. #include <stdio.h>  
  4. #include <memory.h>  
  5. const int MAXN = 100;  
  6. int flag[MAXN / 32];  
  7. int primes[MAXN / 3], pi;  
  8. void GetPrime_1()  
  9. {  
  10.     int i, j;  
  11.     pi = 0;  
  12.     memset(flag, 0, sizeof(flag));  
  13.     for (i = 2; i < MAXN; i++)  
  14.         if (!((flag[i / 32] >> (i % 32)) & 1))  
  15.         {  
  16.             primes[pi++] = i;  
  17.             for (j = i; j < MAXN; j += i)  
  18.                 flag[j / 32] |= (1 << (j % 32));  
  19.         }  
  20. }  
  21. void PrintfArray()  
  22. {  
  23.     for (int i = 0; i < pi; i++)  
  24.         printf("%d ", primes[i]);  
  25.     putchar('\n');  
  26. }  
  27. int main()  
  28. {  
  29.     printf("用位操作壓縮後篩素數法求100以內的素數\n-- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");    
  30.     GetPrime_1();  
  31.     PrintfArray();  
  32.     return 0;  
  33. }  

同樣執行結果為:

另外,還可以使用C++ STL中的bitset類來作素數表。篩素數方法在筆試面試出現的機率還是比較大的,能寫出用位操作壓縮後的篩素數方法無疑將會使你的程式碼脫穎而出,因此強烈建議讀者自己親自動手實現一遍,平時多努力,考試才不慌。

 

四. 位操作的趣味應用

位操作有很有趣的應用,下面列舉出一些,歡迎讀者補充。

1.  高低位交換

給出一個16位的無符號整數。稱這個二進位制數的前8位為“高位”,後8位為“低位”。現在寫一程式將它的高低位交換。例如,數34520用二進位制表示為:

      10000110 11011000

將它的高低位進行交換,我們得到了一個新的二進位制數:

      11011000 10000110

它即是十進位制的55430。

這個問題用位操作解決起來非常方便,設x=34520=10000110 11011000(二進位制) 由於x為無符號數,右移時會執行邏輯右移即高位補0,因此x右移8位將得到0000000010000110。而x左移8位將得到11011000 00000000。可以發現只要將x>>8與x<<8這兩個數相與就可以得到11011000 10000110。用程式碼實現非常簡潔:

  1. //高低位交換 by MoreWindows( http://blog.csdn.net/MoreWindows )    
  2. #include <stdio.h>  
  3. template <class T>  
  4. void PrintfBinary(T a)  
  5. {  
  6.     int i;  
  7.     for (i = sizeof(a) * 8 - 1; i >= 0; --i)  
  8.     {  
  9.         if ((a >> i) & 1)  
  10.             putchar('1');  
  11.         else   
  12.             putchar('0');  
  13.         if (i == 8)  
  14.             putchar(' ');  
  15.     }  
  16.     putchar('\n');  
  17. }  
  18. int main()  
  19. {  
  20.     printf("高低位交換 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  21.   
  22.     printf("交換前:    ");  
  23.     unsigned short a = 3344520;  
  24.     PrintfBinary(a);  
  25.   
  26.     printf("交換後:    ");  
  27.     a = (a >> 8) | (a << 8);  
  28.     PrintfBinary(a);  
  29.     return 0;  
  30. }  

執行結果如下:

2.  二進位制逆序

我們知道如何對字串求逆序,現在要求計算二進位制的逆序,如數34520用二進位制表示為:

      10000110 11011000

將它逆序,我們得到了一個新的二進位制數:

      00011011 01100001

它即是十進位制的7009。

    回顧下字串的逆序,可以從字串的首尾開始,依次交換兩端的資料。在二進位制逆序我們也可以用這種方法,但運用位操作的高低位交換來處理二進位制逆序將會得到更簡潔的方法。類似於歸併排序的分組處理,可以通過下面4步得到16位資料的二進位制逆序:

第一步:每2位為一組,組內高低位交換

      10 00 01 10  11 01 10 00

  -->01 00 10 01 11 10 01 00

第二步:每4位為一組,組內高低位交換

      0100 1001 1110 0100

  -->0001 0110 1011 0001

第三步:每8位為一組,組內高低位交換

      00010110 10110001

  -->01100001 00011011

第四步:每16位為一組,組內高低位交換

      01100001 00011011

  -->00011011 01100001

對第一步,可以依次取出每2位作一組,再組內高低位交換,這樣有點麻煩,下面介紹一種非常有技巧的方法。先分別取10000110 11011000的奇數位和偶數位,空位以下劃線表示。

      原 數   100001111011000

      奇數位 1_0_0_1_ 1_0_1_0_

      偶數位 _0_0_1_0 _1_1_0_0

將下劃線用0填充,可得

      原 數   100001111011000

      奇數位 100000110001000

      偶數位 00000100 01010000

再將奇數位右移一位,偶數位左移一位,此時將這兩個資料相與即可以達到奇偶位上資料交換的效果了。

      原 數          100001111011000

      奇數位右移 010000101101100

      偶數位左移 0000100 010100000

      相或得到     01001000 11100100

可以看出,結果完全達到了奇偶位的資料交換,再來考慮程式碼的實現——

      取x的奇數位並將偶數位用0填充用程式碼實現就是x & 0xAAAA

      取x的偶數位並將奇數位用0填充用程式碼實現就是x & 0x5555

因此,第一步就用程式碼實現就是:

       x = ((x & 0xAAAA) >> 1) | ((x & 0x5555) << 1);

類似可以得到後三步的程式碼。完整程式如下:

  1. //二進位制逆序 by MoreWindows( http://blog.csdn.net/MoreWindows )    
  2. #include <stdio.h>  
  3. template <class T>  
  4. void PrintfBinary(T a)  
  5. {  
  6.     int i;  
  7.     for (i = sizeof(a) * 8 - 1; i >= 0; --i)  
  8.     {  
  9.         if ((a >> i) & 1)  
  10.             putchar('1');  
  11.         else   
  12.             putchar('0');  
  13.         if (i == 8)  
  14.             putchar(' ');  
  15.     }  
  16.     putchar('\n');  
  17. }  
  18. int main()  
  19. {  
  20.     printf("二進位制逆序 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  21.   
  22.     printf("逆序前:    ");  
  23.     unsigned short a = 34520;  
  24.     PrintfBinary(a);  
  25.   
  26.     printf("逆序後:    ");   
  27.     a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1);  
  28.     a = ((a & 0xCCCC) >> 2) | ((a & 0x3333) << 2);  
  29.     a = ((a & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4);  
  30.     a = ((a & 0xFF00) >> 8) | ((a & 0x00FF) << 8);  
  31.     PrintfBinary(a);  
  32. }  

執行結果如下:

3.  二進位制中1的個數

統計二進位制中1的個數可以直接移位再判斷,當然像《程式設計之美》書中用迴圈移位計數或先打一個表再計算都可以。本文詳細講解一種高效的方法。以34520為例,可以通過下面四步來計算其二進位制中1的個數二進位制中1的個數。

第一步:每2位為一組,組內高低位相加

      10 00 01 10  11 01 10 00

  -->01 00 01 01  10 01 01 00

第二步:每4位為一組,組內高低位相加

      0100 0101 1001 0100

  -->0001 0010 0011 0001

第三步:每8位為一組,組內高低位相加

      00010010 00110001

  -->00000011 00000100

第四步:每16位為一組,組內高低位相加

      00000011 00000100

  -->00000000 00000111

這樣最後得到的00000000 00000111即7即34520二進位制中1的個數。類似上文中對二進位制逆序的做法不難實現第一步的程式碼:

       x = ((x & 0xAAAA) >> 1) + (x & 0x5555);

好的,有了第一步,後面幾步就請讀者完成下吧,先動動筆再看下面的完整程式碼:

  1. //二進位制中1的個數  by MoreWindows( http://blog.csdn.net/MoreWindows )   
  2. #include <stdio.h>  
  3. template <class T>  
  4. void PrintfBinary(T a)  
  5. {  
  6.     int i;  
  7.     for (i = sizeof(a) * 8 - 1; i >= 0; --i)  
  8.     {  
  9.         if ((a >> i) & 1)  
  10.             putchar('1');  
  11.         else   
  12.             putchar('0');  
  13.         if (i == 8)  
  14.             putchar(' ');  
  15.     }  
  16.     putchar('\n');  
  17. }  
  18. int main()  
  19. {  
  20.     printf("二進位制中1的個數 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  21.       
  22.     unsigned short a = 34520;  
  23.     printf("原數    %6d的二進位制為:  ", a);  
  24.     PrintfBinary(a);  
  25.       
  26.     a = ((a & 0xAAAA) >> 1) + (a & 0x5555);  
  27.     a = ((a & 0xCCCC) >> 2) + (a & 0x3333);  
  28.     a = ((a & 0xF0F0) >> 4) + (a & 0x0F0F);  
  29.     a = ((a & 0xFF00) >> 8) + (a & 0x00FF);     
  30.     printf("計算結果%6d的二進位制為:  ", a);     
  31.     PrintfBinary(a);  
  32.     return 0;  
  33. }  

執行結果如下:

可以發現巧妙運用分組處理確實是解決很多二進位制問題的靈丹妙藥。

4.  缺失的數字

很多成對出現數字儲存在磁碟檔案中,注意成對的數字不一定是相鄰的,如2, 3, 4, 3, 4, 2……,由於意外有一個數字消失了,如何儘快的找到是哪個數字消失了?

由於有一個數字消失了,那必定有一個數只出現一次而且其它數字都出現了偶數次。用搜尋來做就沒必要了,利用異或運算的兩個特性——1.自己與自己異或結果為0,2.異或滿足交換律。因此我們將這些數字全異或一遍,結果就一定是那個僅出現一個的那個數。 示例程式碼如下:

  1. //缺失的數字  by MoreWindows( http://blog.csdn.net/MoreWindows )   
  2. #include <stdio.h>  
  3. int main()  
  4. {  
  5.     printf("缺失的數字 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");  
  6.       
  7.     const int MAXN = 15;  
  8.     int a[MAXN] = {1, 347, 6, 9, 13, 65, 889, 712, 889, 347, 1, 9, 65, 13, 712};  
  9.     int lostNum = 0;  
  10.     for (int i = 0; i < MAXN; i++)  
  11.         lostNum ^= a[i];  
  12.     printf("缺失的數字為:  %d\n", lostNum);     
  13.     return 0;  
  14. }  

 

位操作是一種高效優美的方法,同時由於其高效的運算效能和掌握難度較大,位操作運算一直是筆試面試時的熱門話題之一。本文詳細總結了位操作的方法與技巧並列出4種位操作趣味應用,如果讀者能親自上機實現程式碼,相信必能更好應對筆試和麵試時可能遇到的位操作問題。

另外,歡迎各位能提供筆試面試中的位操作相關的題目給我,我將會在提高篇中加入這些。謝謝大家。

 

 

注1.int型別一般佔4位元組,32位。因此15準確表達為

15=00000000 00000000 00000000 00001111(二進位制)

-15準確表達為

-15=11111111 11111111 11111111 11110001(二進位制)

為了簡便起見,文章中使用15=00001111(二進位制),-15=11110001(二進位制)。

 

注2.這種篩素數的方法很樸素,會多次重複訪問資料,有什麼辦法能改進一下嗎?請看《改進的篩素數方法》一文。

 

轉載請標明出處,原文地址:http://blog.csdn.net/morewindows/article/details/7354571

相關文章