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

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

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

      下面就先來對位操做做個全面總結,歡迎你們補充。編程

在計算機中全部數據都是以二進制的形式儲存的。位運算其實就是直接對在內存中的二進制數據進行操做,所以處理數據的速度很是快。windows

在實際編程中,若是能巧妙運用位操做,徹底能夠達到四兩撥千斤的效果,正由於位操做的這些優勢,因此位操做在各大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。

int a = -15, b = 15;
	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之間的全部奇數。

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

2.交換兩數

通常的寫法是:

void Swap(int &a, int &b)
{
	if (a != b)
	{
		int c = a;
		a = b;
		b = c;
	}
}

能夠用位操做來實現交換兩數而不用第三方變量:

void Swap(int &a, int &b)
{
	if (a != b)
	{
		a ^= b;
		b ^= a;
		a ^= b;
	}
}

能夠這樣理解:

第一步  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=6

3.變換符號

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

如對於-11和11,能夠經過下面的變換方法將-11變成11

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

一樣能夠這樣的將11變成-11

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

所以變換符號只須要取反後加1便可。完整代碼以下:

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

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。完整代碼以下:

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

如今再分析下。對於任何數,與0異或都會保持不變,與-1即0xFFFFFFFF異或就至關於取反。所以,a與i異或後再減i(由於i爲0或-1,因此減i便是要麼加0要麼加1)也能夠獲得絕對值。因此能夠對上面代碼優化下:

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

注意這種方法沒用任何判斷表達式,並且有些筆面試題就要求這樣作,所以建議讀者記住該方法(^_^講解事後應該是比較好記了)。

 

三. 位操做與空間壓縮

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

//by MoreWindows( http://blog.csdn.net/MoreWindows )
#include <stdio.h>
#include <memory.h>
const int MAXN = 100;
bool flag[MAXN];
int primes[MAXN / 3 + 1], pi;
//對每一個素數,它的倍數一定不是素數。
//有不少重複如flag[10]會在訪問flag[2]和flag[5]時各訪問一次
void GetPrime_1()
{
	int i, j;
	pi = 0;
	memset(flag, false, sizeof(flag));
	for (i = 2; i < MAXN; i++)
		if (!flag[i])
		{
			primes[pi++] = i;
			for (j = i; j < MAXN; j += i)
				flag[j] = true;
		}
}
void PrintfArray()
{
	for (int i = 0; i < pi; i++)
		printf("%d ", primes[i]);
	putchar('\n');
}
int main()
{
	printf("用篩素數法求100之內的素數\n-- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
	GetPrime_1();
	PrintfArray();
	return 0;
}

運行結果以下:

在上面程序是用bool數組來做標記的,bool型數據佔1個字節(8位),所以用位操做來壓縮下空間佔用將會使空間的佔用減小八分之七。

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

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

一樣,能夠1向左移位後與原數相與來判斷指定位上是0仍是1(也能夠將原數右移若干位再與1相與)。

//判斷指定位上是0仍是1
	int j = 1 << 10;
	if ((j & (1 << 10)) != 0)
		printf("指定位上爲1");
	else
		printf("指定位上爲0");

擴展到數組上,咱們能夠採用這種方法,由於數組在內存上也是連續分配的一段空間,徹底能夠「認爲」是一個很長的整數。先寫一份測試代碼,看看如何在數組中使用位操做:

//by MoreWindows( http://blog.csdn.net/MoreWindows )  
#include <stdio.h>
int main()
{
	printf("     對數組中指定位置上置位和判斷該位\n");
	printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");
	//在數組中在指定的位置上寫1
	int b[5] = {0};
	int i;
	//在第i個位置上寫1
	for (i = 0; i < 40; i += 3)
		b[i / 32] |= (1 << (i % 32));
	//輸出整個bitset
	for (i = 0; i < 40; i++)
	{
		if ((b[i / 32] >> (i % 32)) & 1)
			putchar('1');
		else 
			putchar('0');
	}
	putchar('\n');
	return 0;
}

運行結果以下:

能夠看出該數組每3個就置成了1,證實咱們上面對數組進行位操做的方法是正確的。所以能夠將上面篩素數方法改爲使用位操做壓縮後的篩素數方法:

//使用位操做壓縮後的篩素數方法
//by MoreWindows( http://blog.csdn.net/MoreWindows ) 
#include <stdio.h>
#include <memory.h>
const int MAXN = 100;
int flag[MAXN / 32 + 1];
int primes[MAXN / 3 + 1], pi;
void GetPrime_1()
{
	int i, j;
	pi = 0;
	memset(flag, 0, sizeof(flag));
	for (i = 2; i < MAXN; i++)
		if (!((flag[i / 32] >> (i % 32)) & 1))
		{
			primes[pi++] = i;
			for (j = i; j < MAXN; j += i)
				flag[j / 32] |= (1 << (j % 32));
		}
}
void PrintfArray()
{
	for (int i = 0; i < pi; i++)
		printf("%d ", primes[i]);
	putchar('\n');
}
int main()
{
	printf("用位操做壓縮後篩素數法求100之內的素數\n-- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
	GetPrime_1();
	PrintfArray();
	return 0;
}

一樣運行結果爲:

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

位操做的壓縮空間技巧也被用於strtok函數的實現,請參考《strtok源碼剖析 位操做與空間壓縮》(http://blog.csdn.net/morewindows/article/details/8740315

 

四. 位操做的趣味應用

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

1.  高低位交換

給出一個16位的無符號整數。稱這個二進制數的前8位爲「高位」,後8位爲「低位」。如今寫一程序將它的高低位交換。例如,數34520用二進制表示爲:

      10000110 11011000

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

      11011000 10000110

它便是十進制的55430。

這個問題用位操做解決起來很是方便,設x=34520=10000110 11011000(二進制) 因爲x爲無符號數,右移時會執行邏輯右移即高位補0,所以x右移8位將獲得00000000 10000110。而x左移8位將獲得11011000 00000000。能夠發現只要將x>>8與x<<8這兩個數相或就能夠獲得11011000 10000110。用代碼實現很是簡潔:

//高低位交換 by MoreWindows( http://blog.csdn.net/MoreWindows )  
#include <stdio.h>
template <class T>
void PrintfBinary(T a)
{
	int i;
	for (i = sizeof(a) * 8 - 1; i >= 0; --i)
	{
		if ((a >> i) & 1)
			putchar('1');
		else 
			putchar('0');
		if (i == 8)
			putchar(' ');
	}
	putchar('\n');
}
int main()
{
	printf("高低位交換 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");

	printf("交換前:    ");
	unsigned short a = 3344520;
	PrintfBinary(a);

	printf("交換後:    ");
	a = (a >> 8) | (a << 8);
	PrintfBinary(a);
	return 0;
}

運行結果以下:

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的奇數位和偶數位,空位如下劃線表示。

      原 數    10000110 11011000

      奇數位 1_0_0_1_ 1_0_1_0_

      偶數位  _0_0_1_0 _1_1_0_0

將下劃線用0填充,可得

      原 數    10000110 11011000

      奇數位 10000010 10001000

      偶數位 00000100 01010000

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

      原 數           10000110 11011000

      奇數位右移 01000001 01000100  

      偶數位左移 00001000 10100000

      相或獲得      01001001 11100100

能夠看出,結果徹底達到了奇偶位的數據交換,再來考慮代碼的實現——

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

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

所以,第一步就用代碼實現就是:

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

相似能夠獲得後三步的代碼。完整程序以下:

//二進制逆序 by MoreWindows( http://blog.csdn.net/MoreWindows )  
#include <stdio.h>
template <class T>
void PrintfBinary(T a)
{
	int i;
	for (i = sizeof(a) * 8 - 1; i >= 0; --i)
	{
		if ((a >> i) & 1)
			putchar('1');
		else 
			putchar('0');
		if (i == 8)
			putchar(' ');
	}
	putchar('\n');
}
int main()
{
	printf("二進制逆序 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");

	printf("逆序前:    ");
	unsigned short a = 34520;
	PrintfBinary(a);

	printf("逆序後:    ");	
	a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1);
	a = ((a & 0xCCCC) >> 2) | ((a & 0x3333) << 2);
	a = ((a & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4);
	a = ((a & 0xFF00) >> 8) | ((a & 0x00FF) << 8);
	PrintfBinary(a);
}

運行結果以下:

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的個數  by MoreWindows( http://blog.csdn.net/MoreWindows ) 
#include <stdio.h>
template <class T>
void PrintfBinary(T a)
{
	int i;
	for (i = sizeof(a) * 8 - 1; i >= 0; --i)
	{
		if ((a >> i) & 1)
			putchar('1');
		else 
			putchar('0');
		if (i == 8)
			putchar(' ');
	}
	putchar('\n');
}
int main()
{
	printf("二進制中1的個數 --- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");
	
	unsigned short a = 34520;
	printf("原數    %6d的二進制爲:  ", a);
	PrintfBinary(a);
	
	a = ((a & 0xAAAA) >> 1) + (a & 0x5555);
	a = ((a & 0xCCCC) >> 2) + (a & 0x3333);
	a = ((a & 0xF0F0) >> 4) + (a & 0x0F0F);
	a = ((a & 0xFF00) >> 8) + (a & 0x00FF);	
	printf("計算結果%6d的二進制爲:  ", a);	
	PrintfBinary(a);
	return 0;
}

運行結果以下:

能夠發現巧妙運用分組處理確實是解決不少二進制問題的靈丹妙藥。

4.  缺失的數字

不少成對出現數字保存在磁盤文件中,注意成對的數字不必定是相鄰的,如2, 3, 4, 3, 4, 2……,因爲意外有一個數字消失了,如何儘快的找到是哪一個數字消失了?

因爲有一個數字消失了,那一定有一個數只出現一次並且其它數字都出現了偶數次。用搜索來作就不必了,利用異或運算的兩個特性——1.本身與本身異或結果爲0,2.異或知足交換律。所以咱們將這些數字全異或一遍,結果就必定是那個僅出現一個的那個數。 示例代碼以下:

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

在這個題目中有一個數字丟失了,若是有兩個數字丟失了應該怎麼作了,請看《【白話經典算法系列之十二】數組中只出現1次的兩個數字(百度面試題)》 

地址:http://blog.csdn.net/morewindows/article/details/8214003

 

位操做是一種高效優美的方法,同時因爲其高效的運算性能和掌握難度較大,位操做運算一直是筆試面試時的熱門話題之一。本文詳細總結了位操做的方法與技巧並列出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

若是以爲本文對您有幫助,請點擊‘頂’支持一下,您的支持是我寫做最大的動力,謝謝。

相關文章
相關標籤/搜索