編程技巧--位運算的巧妙運用(1)


 作者:yunyu5120

 

             這是我的這一系列文章的第一篇,主要講述我學習過程中積累的一些編程技巧,由於我也是一個初學者,高手莫笑。這一篇主要講解位運算的基礎知識魚與其簡單應用,我主要以C/C++語言講述,其他語言可以類推。如果你已經對位運算基礎和應用十分熟悉,那麼本文並不適合你。

             我相信還是有一部分人對位運算還不是很瞭解,我希望你在看了本博文之後能對位運算有深刻的瞭解,並運能夠用自如,能夠體會到編程的樂趣。

            “寫程序,位運算是必要的嗎?”

             這個問題問的好,其實位運算並不是必要的,有什多方法可以可以代替位運算,但是位運算其特有的對程序的優化特點是無法替代的!當然如果你在寫Windows應用程序,其中調用的一些Windows APi 你就必須用到位運算,如最簡單的MessageBox。當然其中牽扯到的位運算過於簡單,就是簡單的或運算。想想當初寫的第一個windows程序用到MessageBox竟然出現了一個windows窗口,而不是那黑糊糊的Console,讓我興奮了還一段時間!可是當時的我也不知道這裏面牽扯的很多知識,甚至什麼是API都不知道!

             我們在學習C/C++的時候書本上對位運算的相關知識講得很少,就是簡單的“或與非”。如果你的記性好那麼你還會記得在位運算中還有一個運算叫做 “異或”運算和移位運算。不知道你現在對位運算的基礎是否還清楚,我在這裏假設我們都忘了位運算的基礎,所以下面我們對位運算進行復習一下。

            

C/C++語言提供的位運算符有:

運算符 含義 功能
& 按位與 如果兩個相應的二進制位都爲1,則該位的結果值爲1;否則爲0。
| 按位或 兩個相應的二進制位中只要有一個爲1,該位的結果值爲1。
按位異或 若參加運算的兩個二進制位同號則結果爲0(假)異號則結果爲1(真)
取反 ~是一個單目(元)運算符,用來對一個二進制數按位取反,即將0變1,將1變0。
<< 左移 左移運算符是用來將一個數的各二進制位全部左移N位,右補0。
>> 右移 表示將a的各二進制位右移N位,移到右端的低位被捨棄,對無符號數,高位補0。

位運算的結果演示:

位運算 或 “|” or 與 “&”and 非 “~” not 異或 “^” xor
操作數1 01010101 11010101 10101010 10000001
操作數2 00101010 10101010 (無) 01111111
也能算結果 01111111 10000000 01010101 11111110

      

               好了看了上面的兩個表格,相信你已經對位運算有所瞭解了,那麼接下來,我們就來講講位運算的應用。

 

1、  用於整數的奇偶性判斷

               想想,我們要判斷一個數的奇偶性,在沒用位運算之前我們可以用下列的代碼來實現:

 

  1. template<class Type>  
  2. bool Parity(Type value)  
  3. {  
  4.     if(value % 2 == 0)  
  5.         return false;  
  6.     else  
  7.         return true;  
  8. }  
  9. //加以優化   
  10. template<class Type>  
  11. inline bool Parity(Type value)  
  12. {  
  13.     return (value % 2 != 0);  
  14. }  
template<class Type>
bool Parity(Type value)
{
	if(value % 2 == 0)
		return false;
	else
		return true;
}
//加以優化 
template<class Type>
inline bool Parity(Type value)
{
	return (value % 2 != 0);
}



 

             要知道,上面的代碼我們使用的是對2取餘,如果操作數value是小數的話,還勉強行得通,但是value是一個上百萬的大數,那麼這就白白浪費了CPU的大量時間,程序的效率和性能就很差。我們知道任何數在計算機儲存中都是以二進制儲存的,細心的你就會發現在二進制的最小一位有個特點,爲0就是偶數,爲1就是奇數,按照這個原理我們根本沒必要讓我們的CPU大哥白白做那麼多的工作,只要一步判斷就可以了。接下來就讓我們看看位運算的精妙之處!

             那麼我們的目的就是判斷最小位是0還是1,可是我們怎麼判斷呢?我們要用位運算阿里判斷,就是與或非。在上面的複習之中我們只說了位運算的計算方法,並沒有說其用處。那麼在這裏我們用到的就是“與”!與運算特有的一個功能就是判斷指定位上的值(0或1)。我們來看下面的表格(與運算)。

操作數1 10101010 01010101 11111111 11111110
操作數2 00000001 00000001 00000001 00000001
運算結果 00000000 00000001 00000001 00000000

            我們要注意一下這裏的 操作數2 ,它只有最低位是1,其餘位都是0,這就是關鍵所在,操作數1是隨機值。我們看看結果只會有兩種結果:0或1。這個結果就取決於操作數1的最低位,它爲1時就爲1,爲0時就爲0.

            “那麼我要判斷的是第二位呢?”

            好!那我們就把操作數2改爲 00000010 那麼結果就只會有 00000000 或 00000010 其結果取決於第二位。

            有了這個基礎那麼我們來看看怎麼用位運算判斷奇偶性吧:
 

  1. template<class Type>  
  2. bool Parity(Type value)  
  3. {  
  4.     if(value & 0x0001 == 0)  
  5.         return false;  
  6.     else  
  7.         return true;  
  8. }  
  9. //加以優化   
  10. template<class Type>  
  11. inline bool Parity(Type value)  
  12. {  
  13.     return (value & 1 != 0);  
  14. }  
  15. //在簡化   
  16. #define PARITY(value) (value&1)  
template<class Type>
bool Parity(Type value)
{
	if(value & 0x0001 == 0)
		return false;
	else
		return true;
}
//加以優化 
template<class Type>
inline bool Parity(Type value)
{
	return (value & 1 != 0);
}
//在簡化 
#define	PARITY(value) (value&1)


 

                    使用a%2來判斷奇偶性和a & 1是一樣的作用,但是a & 1要快好多。

 

2、  判斷n是否是2的整數冪

 

                所謂2的整數冪就是指 1(2的0次冪),2,4,8,16,32,64,128,256,512,1024,2048.............等數字,若何判斷一個數是否是這樣的數呢?我們看看不用位運算的計算方法:

 

  1. #include "math.h"   
  2.   
  3. template<class Type>  
  4. bool IsPowerOfTwo(Type value)  
  5. {  
  6.     for(int i = 0,l = 8*sizeof(value); i < l ;i++)  
  7.     {  
  8.         if(pow(2,i) == value)  
  9.         {  
  10.             return true;  
  11.         }  
  12.     }   
  13.     return false;  
  14. }  
#include "math.h" 

template<class Type>
bool IsPowerOfTwo(Type value)
{
	for(int i = 0,l = 8*sizeof(value); i < l ;i++)
	{
		if(pow(2,i) == value)
		{
			return true;
		}
	} 
	return false;
}


 

             在這個算法中,我們使用了一個循環。其原理非常簡單就是一一的對比,但是其中還調用了數學函數庫,效率大大降低。接下來我們講講怎樣用位運算來判斷。我們首先要研究一下這些數的特性,請看下錶(與運算):

2的冪 8 16 32 64
n 00001000 00010000 00100000 01000000
n-1 00000111 00001111 00011111 00111111
與結果 00000000 00000000 00000000 00000000

            我們發現 n &(n-1) =  0  我們可以 用邏輯非  !(n&(n-1)) =  1 。那是不是這樣就可以了呢,你會發現 !(0&(0-1)) =  1 但是 0並不是 2的正整數冪。我們可以用 邏輯與 (!(n&(n-1) &&  n) = 1;請看下面的代碼:

 

  1. template<class Type>  
  2. inline bool IsPowerOfTwo(Type n)  
  3. {  
  4.     if(((!(n&(n-1))) && n) == 1)  
  5.         return true;  
  6.     else  
  7.         return false;  
  8. }  
  9. //簡化   
  10. #define ISPOWEROFTWO(n) ((!(n&(n-1)) ) && n)  
template<class Type>
inline bool IsPowerOfTwo(Type n)
{
	if(((!(n&(n-1))) && n) == 1)
		return true;
	else
		return false;
}
//簡化 
#define	ISPOWEROFTWO(n) ((!(n&(n-1)) ) && n)


 

 

3、  統計n在二進制中1的個數

 

             樸素的統計辦法是:先判斷n的奇偶性,爲奇數時計數器增加1,然後將n右移一位,重複上面步驟,直到移位完畢。

 

  1. template<class Type>  
  2. inline bool Parity(Type value)  
  3. {  
  4.     return (value % 2 != 0);  
  5. }  
  6.   
  7. template<class Type>  
  8. inline int CountOne(Type value)  
  9. {  
  10.     if(value != 0)  
  11.     {  
  12.         return Parity(value) + CountOne(value >> 1);  
  13.     }  
  14.     return 0;  
  15. }  
template<class Type>
inline bool Parity(Type value)
{
	return (value % 2 != 0);
}

template<class Type>
inline int CountOne(Type value)
{
	if(value != 0)
	{
		return Parity(value) + CountOne(value >> 1);
	}
	return 0;
}



 

             樸素的統計辦法是比較簡單的,那麼我們來看看比較高級的辦法。

 

              舉例說明,

                      考慮2位整數 n=11(十進制爲3),裏邊有2個1,先提取裏邊的偶數位10,奇數位01,把偶數位右移1位,然後與奇數位相加,因爲每對奇偶位相加的和不會超過“兩位”,所以結果中每兩位保存着數n中1的個數,那麼把 n 計算之後得到的值爲:(10>>1)+01 = 01 + 01 = 10, 把10換成十進制就是 2,2就代表 n(3)=11 中有兩個1!

                     相應的如果n是四位整數 n=0111(十進制7),先以“一位”爲單位做奇偶位提取:偶數位 0010,奇數位0101。然後偶數位移位(右移1位)再相加:(0010>>1)+0101=0110;再用0110以“兩位”爲單位做奇偶提取:偶數爲0100,奇數位0010。偶數位移位(這時就需要移2位)再相加:(0100>>2)+0010=0011,因爲此時每對奇偶位的和不會超過“四位”,所以結果中保存着n中1的個數:(0100>>2)+0010=0011 把0011換成十進制就是3,3就是n(7)=0111中有3個1。

                     依次類推可以得出更多位n的算法。整個思想類似分治法。

  
在這裏就順便說一下常用的二進制數:

二進制數 二進制值 用處
0xAAAAAAAA 10101010101010101010101010101010 偶數位爲1,以1位爲單位提取奇位
0x55555555 01010101010101010101010101010101 奇數位爲1,以1位爲單位提取偶位
0xCCCCCCCC 11001100110011001100110011001100 以“2位”爲單位提取奇位
0x33333333 00110011001100110011001100110011 以“2位”爲單位提取偶位
0xF0F0F0F0 11110000111100001111000011110000 以“8位”爲單位提取奇位
0x0F0F0F0F 00001111000011110000111100001111 以“8位”爲單位提取偶位
0xFFFF0000 11111111111111110000000000000000 以“16位”爲單位提取奇位
0x0000FFFF 00000000000000001111111111111111 以“16位”爲單位提取偶位

    

例如:32位無符 號數的1的個數可以這樣數:

 

  1. int CountOne(unsigned int n)  
  2. {  
  3.     //0xAAAAAAAA,0x55555555分別是以“1位”爲單位提取奇偶位  
  4.     n = ((n & 0xAAAAAAAA) >> 1) + (n & 0x55555555);  
  5.     //0xCCCCCCCC,0x33333333分別是以“2位”爲單位提取奇偶位  
  6.     n = ((n & 0xCCCCCCCC) >> 2) + (n & 0x33333333);  
  7.     //0xF0F0F0F0,0x0F0F0F0F分別是以“4位”爲單位提取奇偶位  
  8.     n = ((n & 0xF0F0F0F0) >> 4) + (n & 0x0F0F0F0F);  
  9.     //0xFF00FF00,0x00FF00FF分別是以“8位”爲單位提取奇偶位  
  10.     n = ((n & 0xFF00FF00) >> 8) + (n & 0x00FF00FF);  
  11.     //0xFFFF0000,0x0000FFFF分別是以“16位”爲單位提取奇偶位  
  12.     n = ((n & 0xFFFF0000) >> 16) + (n & 0x0000FFFF);  
  13.   
  14.     return n;  
  15. }  
int CountOne(unsigned int n)
{
    //0xAAAAAAAA,0x55555555分別是以“1位”爲單位提取奇偶位
    n = ((n & 0xAAAAAAAA) >> 1) + (n & 0x55555555);
    //0xCCCCCCCC,0x33333333分別是以“2位”爲單位提取奇偶位
    n = ((n & 0xCCCCCCCC) >> 2) + (n & 0x33333333);
    //0xF0F0F0F0,0x0F0F0F0F分別是以“4位”爲單位提取奇偶位
    n = ((n & 0xF0F0F0F0) >> 4) + (n & 0x0F0F0F0F);
    //0xFF00FF00,0x00FF00FF分別是以“8位”爲單位提取奇偶位
    n = ((n & 0xFF00FF00) >> 8) + (n & 0x00FF00FF);
    //0xFFFF0000,0x0000FFFF分別是以“16位”爲單位提取奇偶位
    n = ((n & 0xFFFF0000) >> 16) + (n & 0x0000FFFF);

    return n;
}


 

                    

                    看起來似乎採用位運算的代碼比樸素方法代碼要複雜的多,但是在性能上有着樸素方法無法比擬的優越性,只要四步簡單的運算就能達到目的,而樸素方法不是用循環就是遞歸,這大大降低了CPU的運算性能。

 

  

4、對於正整數的模運算(注意,負數不能這麼算)

先說下比較簡單的:

乘除法是很消耗時間的,只要對數左移一位就是乘以2,右移一位就是除以2,據說用位運算效率提高了60%。

乘2^k 衆所周知: n<<k。所以你以後還會傻傻地去敲2566*4的結果10264嗎?直接2566<<2就搞定了,又快又準確。

除2^k衆所周知: n>>k。

那麼 mod 2^k 呢?(對2的倍數取模)

n&((1<<k)-1)

用通俗的言語來描述就是,對2的倍數取模,只要將數與2的倍數-1做按位與運算即可。

好!方便理解就舉個例子吧。

思考:如果結果是要求模2^k時,我們真的需要每次都取模嗎?

在此很容易讓人想到快速冪取模法。

快速冪取模算法

經常做題目的時候會遇到要計算 a^b mod c 的情況,這時候,一個不小心就TLE(算法計算超時,ACM題目測試結果常見問題)了。那麼如何解決這個問題呢?位運算來幫你吧。

 

首先介紹一下秦九韶算法:(數值分析講得很清楚)

把一個n次多項式f(x) = a[n]x^n+a[n-1]x^(n-1)+......+a[1]x+a[0]改寫成如下形式:

  f(x) = a[n]x^n+a[n-1]x^(n-1))+......+a[1]x+a[0]

  = (a[n]x^(n-1)+a[n-1]x^(n-2)+......+a[1])x+a[0]

  = ((a[n]x^(n-2)+a[n-1]x^(n-3)+......+a[2])x+a[1])x+a[0]

  =. .....

  = (......((a[n]x+a[n-1])x+a[n-2])x+......+a[1])x+a[0].

  求多項式的值時,首先計算最內層括號內一次多項式的值,即

  v[1]=a[n]x+a[n-1]

  然後由內向外逐層計算一次多項式的值,即

  v[2]=v[1]x+a[n-2]

  v[3]=v[2]x+a[n-3]

  ......

  v[n]=v[n-1]x+a[0]

這樣,求n次多項式f(x)的值就轉化爲求n個一次多項式的值。

好!有了前面的基礎知識,我們開始解決問題吧

由(a × b) mod c=( (a mod c) × b) mod c.

我們可以將 b先表示成就:

  b = a[t] × 2^t + a[t-1]× 2^(t-1) + …… + a[0] × 2^0.  (a[i]=[0,1]).

這樣我們由 a^b  mod  c = (a^(a[t] × 2^t  +  a[t-1] × 2^(t-1) + …a[0] × 2^0) mod c.

然而我們求  a^( 2^(i+1) ) mod c=( (a^(2^i)) mod c)^2 mod c .求得。

具體實現如下:

使用秦九韶算法思想進行快速冪模算法,簡潔漂亮

// 快速計算 (a ^ p) % m 的值
  1. __int64 FastM(__int64 a, __int64 p, __int64 m)  
  2. {   
  3.     if (p == 0) return 1;  
  4.     __int64  r = a % m;  
  5.     __int64  k = 1;  
  6.     while (p > 1)  
  7.     {  
  8.         if ((p & 1)!=0)  
  9.         {  
  10.             k = (k * r) % m;   
  11.         }  
  12.         r = (r * r) % m;  
  13.         p >>= 1;  
  14.     }  
  15.     return (r * k) % m;  
  16. }  
__int64 FastM(__int64 a, __int64 p, __int64 m)
{ 
    if (p == 0) return 1;
    __int64  r = a % m;
    __int64  k = 1;
    while (p > 1)
    {
        if ((p & 1)!=0)
        {
            k = (k * r) % m; 
        }
        r = (r * r) % m;
        p >>= 1;
    }
    return (r * k) % m;
}

 http://acm.pku.edu.cn/JudgeOnline/problem?id=3070

 

5、計算掩碼

什麼是掩碼?掩碼是一串二進制代碼對目標字段進行位與運算,屏蔽當前的輸入位。用於從一個或多個字節中選出的位的集合。

舉個例子:

我們有一個IP地址:192.168.1.111 對應二進制:11000000.10101000.00000001.01101111。

我們讓這個IP位與:255.255.255.0 對應二進制:11111111.11111111.11111111.00000000

可以得到子網地址:192.168.1.0     對應二進制:11000000.10101000.00000001.00000000

在例子中我們通過觀察二進制碼就知道,這個過程就是拿到IP的前三個字節的數據信息,這裏用到的255.255.255.0就是掩碼,也就是我們常說的子網掩碼。通過子網掩碼可以輕鬆的得到子網地址。那麼通過掩碼我們就可以輕鬆的得到多個字節中指定的位的集合。


我們現在有一個需求:獲得數x的低n位的集合。

假設 x = 233 n= 6,我們就知道計算方法:233的二進制是 11101001,所以結果集爲 11101001&00111111 =00101001 十進制爲 41。在這個計算中233可以輕易改變,但是 00111111 已經指定 n = 6,要可以讓n也隨意改變怎麼辦呢?

我們用位運算的思維就可以得到 n = 6 時 00111111 可以表示爲 (1 << 6) - 1 

那麼掩碼的計算公式就爲:(1 << n) - 1 


現在根據需求可以寫出模版函數如下:

  1. template<class Type>  
  2. inline Type LowByte(Type x, int n)  
  3. {  
  4.     return x & ((1 << n) - 1);  
  5. }  
  6. //簡化   
  7. #define LOWBYTE(x,n) x & ((x << n) - 1)  
template<class Type>
inline Type LowByte(Type x, int n)
{
	return x & ((1 << n) - 1);
}
//簡化 
#define	LOWBYTE(x,n) x & ((x << n) - 1)

如果是高位集合呢?我們只需要把掩碼左移就可以了:n = 6 時 00111111<<2  公式爲:((1 << 6) - 1)<<2

  1. template<class Type>    
  2. inline Type HeightByte(Type x, int n)    
  3. {    
  4.     return x & (((1 << n) - 1) << (sizeof(x)-n));    
  5. }    
  6. //簡化    
  7. #define HEIGHTBYTE(x,n) x & (((1 << n) - 1) << (sizeof(x)-n))  
template<class Type>  
inline Type HeightByte(Type x, int n)  
{  
    return x & (((1 << n) - 1) << (sizeof(x)-n));  
}  
//簡化  
#define HEIGHTBYTE(x,n) x & (((1 << n) - 1) << (sizeof(x)-n))


6、子集

假設我們有一個集合 mask ={‘c’,‘b’,‘a’},要求列出集合的所有子集。我們可以使用位運算思想,把集合的元素的有無看成二進制的0和1那麼我們展開舉例:

        {‘c’,‘b’,‘a’}

            0     0     1                1                      {‘a’}

            0     1     0                2                      {‘b’}

            0     1     1                3                      {‘b’,‘a’}

                  ...                      ...                          ...

            1     1     1                7                      {‘c’,‘b’,‘a’}

              二進制                十進制                   對應子集

  枚舉出一個集合的子集。設原集合爲mask,則下面的代碼就可以列出它的所有子集: 

  for (i = mask ; i ; i = (i - 1) & mask) ; 

  很漂很漂亮吧。

發佈了39 篇原創文章 · 獲贊 71 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章