1、位操作運算符的種類:&(與)、|(或)、~(取反)、^(異或)、<<(左移)、>>(右移)、>>>(無符號右移)。
2、位運算符操作不會短路。
3、位運算符操作的是補碼,所以~后正負號會發生變化。
4、位運算符只能用於整型。
5、反碼、補碼是相對於有符號數而言的,且不改變符號位。無符號數沒有反碼、補碼。
其中移位運算符需要注意的地方:
三個移位運算符的相同點:當移位的位數超出數值的位數則會取模后再移位。
<<(左移):左移所操作數字的補碼,二進制補碼整體左移(包括符號位),溢出則舍棄,低位補0。這意味着正數在左移的過程中是可以變成負數的。以Java中int型作為測試,Java中int型為4個字節,所以移位的位數為0~31,用32作為測試。如下
public class Tests{ public static void main(String[] args) { System.out.println(1<<32); //結果為1 System.out.println(8>>33); //結果為4 System.out.println(8>>>33); //結果為4
System.out.println(1<<31); //結果為-2147483648 } }
由運行結果運行結果可以看出,對32進行了模運算。
但是如果我們一次不進行過長的移位就不會進行模運算,做如下測試
public class Tests{ public static void main(String[] args) { int x = 1<<31; int y = 1<<31; System.out.println(x); //輸出為-2147483648 x = x<<31; System.out.println(x); //輸出為0 y = y<<1; System.out.println(y); //輸出為0 } }
有上面的測試可以驗證上述的點。左移操作是沒有符號位一說的,直接對補碼的整體進行操作。所以也就沒有無符號左移<<<這樣的運算符。
>>(右移或者說是 有符號右移):右移所操作數字的補碼,二進制補碼整體右移(包括符號位),舍棄右側多出部分,高位補符號位。即相當於正數高位補0,負數高位補1。這樣的操作對於負數來說,會讓二進制數中的1的個數持續增多。但是得到的值是正確的。以整數-1來說。作如下測試。
public class Tests{ public static void main(String[] args) { int x=-1; for(int i=0; i<10; i++){ System.out.println(x>>1); //輸出永遠是-1; } } }
在這個測試中-1所對應的原碼為 ,為方便查看簡單到8位,-1的原碼為1000 0001 補碼為1111 1111 所以無論移動多少次,其結果永遠是-1。進一步可以以-8作為一個測試。一下完整的32位的-8、-4、-2、-1的原碼和補碼
-8原碼:1000 0000 0000 0000 0000 0000 0000 1000 -8補碼:1111 1111 1111 1111 1111 1111 1111 1000 -4補碼:1111 1111 1111 1111 1111 1111 1111 1100 -2補碼:1111 1111 1111 1111 1111 1111 1111 1110 -1補碼:1111 1111 1111 1111 1111 1111 1111 1111
由上述的內容我們可以看出,-8右移一位則除2,補碼中的1會多一個。但結果正確。
>>>(無符號右移):右移做操作數字的補碼,高位補0,低位超出則舍棄。由於高位補零,所以一個負數一旦移位則會變成正數。測試如下
public class Tests{ public static void main(String[] args) { System.out.println(-1>>>1); //結果為2147483647 } }
一下為一些運用
統計十進制數對應的二進制中1的個數(統計的是補碼)
public class Tests{ public static void main(String[] args) { int sum = 0; int n = -8; do{ sum += n&1; }while((n >>>= 1)!=0); System.out.println(sum); //29 n取任何數 } }
還有一種
public class Tests{ public static void main(String[] args) { int n = -8; int sum = 0; while(n != 0){ sum++; n = n&(n-1); } System.out.println(sum); //29 n取任何數 } }
下面這種方法對於正數很好理解,就是每次都取n-1的值,這樣就會影響最低位的1及其更低位的值,影響的效果為取反,而高位不會影響。每次的循環都會利用&運算將低位的1去除。例如0111 1000,減一的值為0111 0111,&運算后為0111 0000。在負數的情況下,我們可以直接看補碼的關系,1111 1000的補碼為1000 1000,減一后(注意是負數加相反數)1111 1001的補碼為1000 0111,直接看補碼間的關系可以看出其實就是去掉符號位后,其余的值當作正數做減一運算與正數情況相同。
負數在變位補碼時,只需要確定最低的一位1,比最低的1高的位依次求反(不包括最低位的1)。所以可以看作,在最低的一位1前的數會受影響。這里負數的減一即絕對值加1后,在變為補碼,相當於從最低位就影響補碼的值。這樣可以認為,n(第一個1的高位取反)n-1(全部取反,相對於n)這樣的&操作,會使n的最低位1及其更低位變成0。
異或交換元素值
public class Tests{ public static void main(String[] args) { int a=2; int b=3; a = a^b; b = a^b; a = a^b; System.out.println(a+" "+b); //3 2 } }
快速冪
快速冪的主要思想為,將一個數的冪的指數,表示為二進制的科學計數法形式。
於是我們可以利用a來計算a的平方,再利用a的平方計算a的四次方。於是可以重復利用上一次的結果。二進制是可以表示任何整數的。於是指數就被拆分。簡化理解為8421碼,當a的5次方就有,0101,於是就有a的0次方乘a的4次方可得到。
public class Tests{ public static void main(String[] args) { int x = 2, n = 5; //x的n次方 int ans = 1; //保存結果 while(n!=0){ if((n&1)==1){ ans *= x; //該8421碼對應的位是1,就乘進結果 } x *= x; //計算8421碼對應的基數, n>>=1; //准備8421碼的下一位 } System.out.println(ans); } }
遞歸實現
int pow(int m,int n){ //m^n if(n==1) return m; int temp=pow(m,n/2); return (n%2==0 ? 1 : m)*temp*temp; }
這個遞歸實現我是這樣想的,以a的11次方為例(想成一條11米的繩子)。我們每一層的任務是,判斷自己是不是在最后一層,如果不是就把自己的一半分到下一層,讓下一層計算,留在自己這一層的要么和下一層一樣大,要么就比下一層大一個。下一層計算結束,就把原來的自己還原回來。