強大的工具--位運算
近日來在看書的過程當中被這樣的一句話 假設hi和low是兩個整數,它們的值介於0到15之間,如果r是一個8位整數,且r的低四位與low各位上的數一致,而r的高4位與hi各位上的數一致,很自然會想到要這樣寫:
r = hi << (4 + low); 整的迷惑,說實話,位運算也學過,但使用最多的也就是在交換數值當中使用異或運算,而當我明白書中這句話裏表達式的含義時,深深被其中的精妙所折服,於是在網上搜集了關於位運算的一些使用技巧,記錄以下,以供後日參考(其中借鑒了csdn和百度上的一些問答,因為數量繁雜,記得不是太全,向這些博主表示衷心的感謝!):
首先還是從最基礎的說起,寫程序位運算是必要的嗎,以我的理解並不是,但是位運算正由於其本身能夠直接操作底層二進制數的特性,能提高近百分之60左右的運算效率,也是值得去學習和深入研究的,在學校的學習中位運算,課堂的知識講解的比較少,把c/c++中 位運算 的基礎知識先羅列一下
運算符 | 含義 | 功能 |
& | 按位與 | 如果兩個相應的二進制位都為1,則該位的結果值為1;否則為0。 |
| | 按位或 | 兩個相應的二進制位中只要有一個為1,該位的結果值為1。 |
∧ | 按位異或 | 若參加運算的兩個二進制位同號則結果為0(假)異號則結果為1(真) |
~ | 取反 | ~是一個單目(元)運算符,用來對一個二進制數按位取反,即將0變1,將1變0。 |
<< | 左移 | 左移運算符是用來將一個數的各二進制位全部左移N位,右補0。 |
>> | 右移 | 表示將a的各二進制位右移N位,移到右端的低位被舍棄,對無符號數,高位補0。 |
在基礎知識方面需要註意的是,在計算機中二進制數都是以補碼的形式存在的,方便機器進行運算,正數的原碼和補碼是相同的,而負數的補碼需要原碼進行取反加一,在位運算時需要多加註意
有位大神將位運算的使用總結成一句口訣:
清零取反要用與,某位置一可用或
若要取反和交換,輕輕松松用異或
接下來就是位運算的各種使用技巧:
(1) 按位與-- &
1 清零特定位 (mask中特定位置0,其它位為1,s=s&mask)
2 取某數中指定位 (mask中特定位置1,其它位為0,s=s&mask)
(2) 按位或-- |
常用來將源操作數某些位置1,其它位不變。 (mask中特定位置1,其它位為0 s=s | mask)
(3) 位異或-- ^
1 使特定位的值取反 (mask中特定位置1,其它位為0 s=s^mask)
2 不引入第三變量,交換兩個變量的值 (設 a=a1,b=b1)
目 標 操 作 操作後狀態
a=a1^b1 a=a^b a=a1^b1,b=b1
b=a1^b1^b1 b=a^b a=a1^b1,b=a1
a=b1^a1^a1 a=a^b a=b1,b=a1
位運算實際應用:
(1) 判斷int型變量a是奇數還是偶數
a&1 = 0 偶數
a&1 = 1 奇數
(2) 取int型變量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1
(3) 將int型變量a的第k位清0,即a=a&~(1 << k)
(4) 將int型變量a的第k位置1, 即a=a|(1 << k)
(5) int型變量循環左移k次,即a=a < >16-k (設sizeof(int)=16)
(6) int型變量a循環右移k次,即a=a>>k|a < <16-k (設sizeof(int)=16)
(7)整數的平均值
對於兩個整數x,y,如果用 (x+y)/2 求平均值,會產生溢出,因為 x+y 可能會大於INT_MAX,但是我們知道它們的平均值是肯定不會溢出的,我們用如下算法:
int average(int x, int y) //返回X,Y 的平均值
{
return (x&y)+((x^y)>>1);
}
(8)判斷一個整數是不是2的冪,對於一個數 x >= 0,判斷他是不是2的冪
boolean power2(int x)
{
return ((x&(x-1))==0)&&(x!=0);
}
(9)不用temp交換兩個整數
void swap(int x , int y)
{
x ^= y;
y ^= x;
x ^= y;
}
(10)計算絕對值
int abs( int x )
{
int y ;
y = x >> 31 ;
return (x^y)-y ; //or: (x+y)^y
}
(11)取模運算轉化成位運算 (在不產生溢出的情況下)
a % (2^n) 等價於 a & (2^n - 1)
(12)乘法運算轉化成位運算 (在不產生溢出的情況下)
a * (2^n) 等價於 a < < n
(13)除法運算轉化成位運算 (在不產生溢出的情況下)
a / (2^n) 等價於 a>> n
例: 12/8 == 12>>3
(14) a % 2 等價於 a & 1
(15) if (x == a) x= b;
else x= a;
等價於 x= a ^ b ^ x;
(16) x 的 相反數 表示為 (~x+1)
"奇技淫巧" :
技巧一:用於消去x的最後一位的1
1 x & (x-1) 2 x = 1100 3 x-1 = 1011 4 x & (x-1) = 1000
1.1.應用一 用O(1)時間檢測整數n是否是2的冪次.
思路解析:N如果是2的冪次,則N滿足兩個條件。
1.N>0
2.N的二進制表示中只有一個1
一位N的二進制表示中只有一個1,所以使用N&(N-1)將唯一的一個1消去。
如果N是2的冪次,那麽N&(N-1)得到結果為0,即可判斷。
1.2.應用二 計算在一個 32 位的整數的二進制表示中有多少個 1.
思路解析:
由 x & (x-1) 消去x最後一位知。循環使用x & (x-1)消去最後一位1,計算總共消去了多少次即可。
1.3.將整數A轉換為B,需要改變多少個bit位
思路解析
這個應用是上面一個應用的拓展。
思考將整數A轉換為B,如果A和B在第i(0<=i<32)個位上相等,則不需要改變這個BIT位,如果在第i位上不相等,則需要改變這個BIT位。所以問題轉化為了A和B有多少個BIT位不相同。聯想到位運算有一個異或操作,相同為0,相異為1,所以問題轉變成了計算A異或B之後這個數中1的個數。
技巧二 使用二進制進行子集枚舉
應用.給定一個含不同整數的集合,返回其所有的子集
樣例
如果 S = [1,2,3],有如下的解:
[ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2] ]
思路
思路就是使用一個正整數二進制表示的第i位是1還是0,代表集合的第i個數取或者不取。所以從0到2n-1總共2n個整數,正好對應集合的2^n個子集。
1 S = {1,2,3} 2 N bit Combination 3 0 000 {} 4 1 001 {1} 5 2 010 {2} 6 3 011 {1,2} 7 4 100 {3} 8 5 101 {1,3} 9 6 110 {2,3} 10 7 111 {1,2,3}
技巧三.a^b^b=a
3.1.應用一 數組中,只有一個數出現一次,剩下都出現三次,找出出現一次的。
問題
Given [1,2,2,1,3,4,3], return 4
解題思路
因為只有一個數恰好出現一個,剩下的都出現過兩次,所以只要將所有的數異或起來,就可以得到唯一的那個數。
1 #include<stdio.h> 2 int main() 3 { 4 int a[7]={1,2,2,1,3,4,3}; 5 int ans=0; 6 for(int i=0;i<7;i++){ 7 ans^=a[i]; 8 } 9 printf("%d\n",ans); 10 }
3.2.應用二 數組中,只有一個數出現一次,剩下都出現三次,找出出現一次的。(還是很蒙蔽)
問題
Given [1,1,2,3,3,3,2,2,4,1] return 4
解題思路
因為數是出現三次的,也就是說,對於每一個二進制位,如果只出現一次的數在該二進制位為1,那麽這個二進制位在全部數字中出現次數無法被3整除。
模3運算只有三種狀態:00,01,10,因此我們可以使用兩個位來表示當前位%3,對於每一位,我們讓Two,One表示當前位的狀態,B表示輸入數字的對應位,Two+和One+表示輸出狀態。
0 0 0 0 0 0 0 1 0 1 0 1 0 0 1 0 1 1 1 0 1 0 0 1 0 1 0 1 0 0 One+ = (One ^ B) & (~Two) Two+ = (~One+) & (Two ^ B)
1 #include<stdio.h> 2 3 void findNum(int *a,int n) 4 { 5 int ans=0; 6 int bits[32]={0}; 7 for(int i=0;i<n;i++){ 8 for(int j=0;j<32;j++){ 9 bits[j]+=((a[i]>>j)&1); 10 } 11 } 12 for(int i=0;i<32;i++){ 13 if(bits[i]%3==1) ans+=1<<i; 14 } 15 printf("%d\n",ans); 16 } 17 int main() 18 { 19 int a[10]={1,1,2,3,3,3,2,2,4,1}; 20 findNum(a,10); 21 }
3.3.應用三 數組中,只有兩個數出現一次,剩下都出現兩次,找出出現一次的
問題
Given [1,2,2,3,4,4,5,3] return 1 and 5
解題思路
有了第一題的基本的思路,我們不妨假設出現一個的兩個元素是x,y,那麽最終所有的元素異或的結果就是res = x^y。並且res!=0,那麽我們可以找出res二進制表示中的某一位是1,那麽這一位1對於這兩個數x,y只有一個數的該位置是1。對於原來的數組,我們可以根據這個位置是不是1就可以將數組分成兩個部分。求出x,y其中一個,我們就能求出兩個了。
1 #include<stdio.h> 2 3 void findNum(int *a,int n) 4 { 5 int ans=0; 6 int pos=0; 7 int x=0,y=0; 8 for(int i=0;i<n;i++) 9 ans^=a[i]; 10 int tmp=ans; 11 while((tmp&1)==0){ 12 //終止條件是二進制tmp最低位是1 13 pos++; 14 tmp>>=1; 15 } 16 for(int i=0;i<n;i++){ 17 if((a[i]>>pos)&1){//取出第pos位的值 18 x^=a[i]; 19 } 20 } 21 y=x^ans; 22 if(x>y) swap(x,y);//從大到小輸出x,y 23 printf("%d %d\n",x,y); 24 } 25 int main() 26 { 27 int a[8]={1,2,2,3,4,4,5,3}; 28 findNum(a,8); 29 }
2019-05-07 12:05:42 編程小菜鳥自我反省,大佬勿噴,謝謝!!!
強大的工具--位運算