java基础:位运算的魅力

本文深入讲解了位运算的基础知识及应用场景,包括位与、位或、位异或、位非等运算符的具体使用方法,并提供了丰富的示例代码,帮助读者理解和掌握位运算的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开篇先来个口诀,此口诀来自网上:

              清零取反要用与,某位置一可用或
              若要取反和交换,轻轻松松用异或

1) 清零 

清零:快速对某一段数据单元的数据清零,即将其全部的二进制位为0。例如整型数a=321对其全部数据清零的操作为a=a&0x0 321=0000 0001 0100 0001 &0=0000 0000 0000 0000

= 0000 0000 0000 0000

(2) 获取一个数据的指定位 

获取一个数据的指定位。例如获得整型数a=的低八位数据的操作为a=a&0xFF321=

0000 0001 0100 0001 & 0xFF =0000 0000 1111 11111

= 0000 0000 0100 0001

获得整型数a=的高八位数据的操作为a=a&0xFF00==a&0XFF00==

321=0000 0001 0100 0001 & 0XFF00=1111 1111 0000 0000

= 0000 0001 0000 0000

(3)保留数据区的特定位  

保留数据区的特定位。例如获得整型数a=的第7-8位(从0开始)位的数据操作为: 110000000

321=0000 0001 0100 0001 & 384=0000 0001 1000 0000

=0000 0001 0000 0000

2. | 位或运算 

1) 运算规则 

位或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑或运算。例如:int型常量57进行位或运算的表达式为5|7,结果如下:5= 0000 0000 0000 0101

| 7= 0000 0000 0000 0111=0000 0000 0000 0111

2) 主要用途 

(1) 设定一个数据的指定位。例如整型数a=321,将其低八位数据置为1的操作为a=a|0XFF321= 0000 0001 0100 0001 | 0000 0000 1111 1111=0000 0000 1111 1111

逻辑运算符||与位或运算符|的区别 

条件“或”运算符 (||) 执行 bool 操作数的逻辑“或”运算,但仅在必要时才计算第二个操作数。 x || y , x | y 不同的是,如果 x true,则不计算 y(因为不论 y 为何值,“或”操作的结果都为 true)。这被称作为“短路”计算。

3. ^ 位异或 

 1) 运算规则 

位异或运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑异或运算。只有当对应位的二进制数互斥的时候,对应位的结果才为真。例如:int型常量57进行位异或运算的表达式为5^7,结果如下:5=0000 0000 0000 0101^7=0000 0000 0000 0111

= 0000 0000 0000 0010

2) 典型应用 

 (1)定位翻转 

定位翻转:设定一个数据的指定位,将1换为00换为1。例如整型数a=321,,将其低八位数据进行翻位的操作为a=a^0XFF;

(2)数值交换 

数值交换。例如a=3,b=4。在例11-1中,无须引入第三个变量,利用位运算即可实现数据交换。以下的操作可以实现a,b两个数据的交换:

a=a^b;

b=b^a;

a=a^b;

4~ 位非 

位非运算的实质是将参与运算的两个数据,按对应的二进制数逐位进行逻辑非运算。

 

实例

  功能 ¦ 示例 ¦ 位运算
----------------------+---------------------------+--------------------
去掉最后一位 ¦ (101101->10110) ¦ x >> 1
在最后加一个0 ¦ (101101->1011010) ¦ x < < 1
在最后加一个1 ¦ (101101->1011011) ¦ x < < 1+1
把最后一位变成1 ¦ (101100->101101) ¦ x ¦ 1
把最后一位变成0 ¦ (101101->101100) ¦ x ¦ 1-1
最后一位取反 ¦ (101101->101100) ¦ x ^ 1
把右数第k位变成1 ¦ (101001->101101,k=3) ¦ x ¦ (1 < < (k-1))
把右数第k位变成0 ¦ (101101->101001,k=3) ¦ x & ~ (1 < < (k-1))
右数第k位取反 ¦ (101001->101101,k=3) ¦ x ^ (1 < < (k-1))
取末三位 ¦ (1101101->101) ¦ x & 7
取末k位 ¦ (1101101->1101,k=5) ¦ x & ((1 < < k)-1)

取右数第k位 ¦ (1101101->1,k=4) ¦ x >> (k-1) & 1

把末k位变成1 ¦ (101001->101111,k=4) ¦ x ¦ (1 < < k-1)
末k位取反 ¦ (101001->100110,k=4) ¦ x ^ (1 < < k-1)
把右边连续的1变成0 ¦ (100101111->100100000) ¦ x & (x+1)
把右起第一个0变成1 ¦ (100101111->100111111) ¦ x ¦ (x+1)
把右边连续的0变成1 ¦ (11011000->11011111) ¦ x ¦ (x-1)
取右边连续的1 ¦ (100101111->1111) ¦ (x ^ (x+1)) >> 1
去掉右起第一个1的左边 ¦ (100101000->1000) ¦ x & (x ^ (x-1))
判断奇数 (x&1)==1
判断偶数 (x&1)==0 

 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1
将int型变量a的第k位清0,即a=a&~(1<<k)
 将int型变量a的第k位置1,即a=a|(1<<k)
 int型变量循环左移k次,即a=a<<k|a>>16-k   (设sizeof(int)=16)
 int型变量a循环右移k次,即a=a>>k|a<<16-k   (设sizeof(int)=16)
取模运算转化成位运算 (在不产生溢出的情况下)a % (2^n) 等价于 a & (2^n - 1)

if (x == a) x= b; else x= a;  等价于 x= a ^ b ^ x;

x 的 相反数 表示为 (~x+1)

从x位(高)到y位(低)间共有多少个1

public static int FindChessNum(int x, int y, ushort k)
  {
  int re = 0;
  for (int i = y; i <= x; i++)
  {
  re += ((k >> (i - 1)) & 1);
  }
  return re;
  }

对于两个整数x,y,如果用 (x+y)/2 求平均值,会产生溢出,因为 x+y 可能会大于INT_MAX,但是我们知道它们的平均值是肯定不会溢出的,我们用如下算法:

int average(int x, int y)   //返回X,Y 的平均值
{   
     return (x&y)+((x^y)>>1);
}
判断一个整数是不是2的幂,对于一个数 x >= 0,判断他是不是2的幂

boolean power2(int x)
{
    return ((x&(x-1))==0)&&(x!=0);
}

计算绝对值

int abs( int x )
{
int y ;
y = x >> 31 ;
return (x^y)-y ;        //or: (x+y)^y
}

完整demo

 #include <stdio.h>
//设置x的第y位为1
#define setbit(x,y) (x)|=(1<<(y-1))
//得到x的第y位的值
#define BitGet(Number,pos) ((Number)>>(pos-1)&1)
//打印x的值
#define print(x) printf("%d\n",x)
//将整数(4个字节)循环右移动k位
#define Rot(a,k) ((a)<<(k)|(a)>>(32-k))
//判断a是否为2的幂次数
#define POW2(a) ((((a)&(a-1))==0)&&(a!=0))
#define OPPX(x) (~(x)+1)
//返回X,Y 的平均值
int average(int x, int y)
{    
return (x&y)+((x^y)>>1);
}
//判断a是否为2的幂次数

bool power2(int x)
{
    return ((x&(x-1))==0)&&(x!=0);
}
//x与y互换
void swap(int& x , int& y)
{
     x ^= y;
     y ^= x;
     x ^= y;
}

int main()
{
int a=0x000D;
print(a);
int b=BitGet(a,2);
print(b);

setbit(a,2);
print(a);
print(BitGet(a,2));
int c=Rot(a,33);
print(c);
print(BitGet(c,5));
printf("8+5=%d\n",average(8,692));

int i;
for (i=0;i<1000;i++)
{
   if (POW2(i))//调用power2(i)
    {
     printf("%-5d",i);
    }
}
printf("\n");

int x=10,y=90;
swap(x,y);
print(x);
print(y);
print(OPPX(-705));
return 0;
}
 
来来再看看HashMap中的应用:

//计算hash值的方法 通过键的hashCode来计算
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }<pre name="code" class="java">static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
      return h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出  
}





所有的整类型以二进制的变化及其宽度来表示。例如,byte 型值42的二进制代码是00101010 ,其中每个置在此代表2的次方,在最边的以20开始。向左下一个置将是21,或2,依次向左是22,或4,然后是8,16,32等等,依此类推。因此42在其置1,3,5的值为1(从边以0开始);这样42是21+23+25的和,也即是2+8+32 。 所有的整类型(除了char 类型之外)都是有符号的整。这意味着他们既能表示正,又能表示负Java 使用大家知道的2的补码(two's complement )这种编码来表示负,也就是通过将与其对应的正的二进制代码取反(即将1变成0,将0变成1),然后对其结果加1。例如,-42就是通过将42的二进制代码的各个取反,即对00101010 取反得到11010101 ,然后再加1,得到11010110 ,即-42 。要对一个负解码,首先对其所有的取反,然后加1。例如-42,或11010110 取反后为00101001 ,或41,然后加1,这样就得到了42。 如果考虑到零的交叉(zero crossing )问题,你就容易理解Java (以及其他绝大多语言)这样用2的补码的原因。假定byte 类型的值零用00000000 代表。它的补码是仅仅将它的每一取反,即生成11111111 ,它代表负零。但问题是负零在整学中是无效的。为了解决负零的问题,在使用2的补码代表负的值时,对其值加1。即负零11111111 加1后为100000000 。但这样使1太靠左而不适合返回到byte 类型的值,因此人们规定,-0和0的表示方法一样,-1的解码为11111111 。尽管我们在这个例子使用了byte 类型的值,但同样的基本的原则也适用于所有Java 的整类型。 因为Java 使用2的补码来存储负,并且因为Java 中的所有整都是有符号的,这样应用位运算符可以容易地达到意想不到的结果。例如,不管你如何打算,Java 用高来代表负。为避免这个讨厌的意外,请记住不管高的顺序如何,它决定一个整的符号。 二 逻辑运算符 逻辑运算符有“与”(AND)、“或”(OR)、“异或(XOR )”、“非(NOT)”,分别用“&”、“|”、“^”、“~”表示,4-3 表显示了每个逻辑运算的结果。在继续讨论之前,请记住位运算符应用于每个运算内的每个单独的。 表4-3 逻辑运算符的结果 A 0 1 0 1 B 0 0 1 1 A | B 0 1 1 1 A & B 0 0 0 1 A ^ B 0 1 1 0 ~A 1 0 1 0 按非(NOT) 按非也叫做补,一元运算符NOT“~”是对其运算的每一取反。例如,字42,它的二进制代码为: 00101010 经过按非运算成为 11010101 按与(AND) 按与运算符“&”,如果两个运算都是1,则结果为1。其他情况下,结果均为零。看下面的例子: 00101010 42 &00001111 15 00001010 10 按或(OR) 按或运算符“|”,任何一个运算为1,则结果为1。如下面的例子所示: 00101010 42 | 00001111 15 00101111 47 按异或(XOR) 按异或运算符“^”,只有在两个比较的不同时其结果是 1。否则,结果是零。下面的例子显示了“^”运算符的效果。这个例子也表明了XOR 运算符的一个有用的属性。注意第二个运算字1的,42对应二进制代码的对应是如何被转换的。第二个运算字0的,第一个运算对应字不变。当对某些类型进行位运算时,你将会看到这个属性的用处。 00101010 42 ^ 00001111 15 00100101 37 逻辑运算符的应用 下面的例子说明了逻辑运算符: // Demonstrate the bitwise logical operators. class BitLogic { public static void main(String args[]) { String binary[] = {"0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111" }; int a = 3; // 0 + 2 + 1 or 0011 in binary int b = 6; // 4 + 2 + 0 or 0110 in binary int c = a | b; int d = a & b; int e = a ^ b; int f = (~a & b) | (a & ~b); int g = ~a & 0x0f; System.out.println(" a = " + binary[a]); System.out.println(" b = " + binary[b]); System.out.println(" a|b = " + binary[c]); System.out.println(" a&b = " + binary[d]); System.out.println(" a^b = " + binary[e]); System.out.println("~a&b|a&~b = " + binary[f]); System.out.println(" ~a = " + binary[g]); } } 在本例中,变量a与b对应的组合代表了二进制所有的 4 种组合模式:0-0,0-1,1-0 ,和1-1 。“|”运算符和“&”运算符分别对变量a与b各个对应的运算得到了变量c和变量d的值。对变量e和f的赋值说明了“^”运算符的功能。字符串组binary 代表了0到15 对应的二进制的值。在本例中,组各元素的排列顺序显示了变量对应值的二进制代码。组之所以这样构造是因为变量的值n对应的二进制代码可以被正确的存储在组对应元素binary[n] 中。例如变量a的值为3,则它的二进制代码对应地存储在组元素binary[3] 中。~a的值与字0x0f (对应二进制为0000 1111 )进行按与运算的目的是减小~a的值,保证变量g的结果小于16。因此该程序的运行结果可以用组binary 对应的元素来表示。该程序的输出如下: a = 0011 b = 0110 a|b = 0111 a&b = 0010 a^b = 0101 ~a&b|a&~b = 0101 ~a = 1100 三 左移运算符 左移运算符<<使指定值的所有都左移规定的次。它的通用格式如下所示: value << num 这里,num 指定要移值value 移动的。也就是,左移运算符<<使指定值的所有都左移num。每左移一个,高阶都被移出(并且丢弃),并用0填充边。这意味着当左移的运算是int 类型时,每移动1它的第31就要被移出并且丢弃;当左移的运算是long 类型时,每移动1它的第63就要被移出并且丢弃。 在对byte 和short类型的值进行移位运算时,你必须小心。因为你知道Java 在对表达式求值时,将自动把这些类型扩大为 int 型,而且,表达式的值也是int 型。对byte 和short类型的值进行移位运算的结果是int 型,而且如果左移不超过31,原来对应各的值也不会丢弃。但是,如果你对一个负的byte 或者short类型的值进行移位运算,它被扩大为int 型后,它的符号也被扩展。这样,整值结果的高就会被1填充。因此,为了得到正确的结果,你就要舍弃得到结果的高。这样做的最简单办法是将结果转换为byte 型。下面的程序说明了这一点: // Left shifting a byte value. class ByteShift { public static void main(String args[]) { byte a = 64, b; int i; i = a << 2; b = (byte) (a << 2); System.out.println("Original value of a: " + a); System.out.println("i and b: " + i + " " + b); } } 该程序产生的输出下所示: Original value of a: 64 i and b: 256 0 因变量a在赋值表达式中,故被扩大为int 型,64(0100 0000 )被左移两次生成值256 (10000 0000 )被赋给变量i。然而,经过左移后,变量b中惟一的1被移出,低全部成了0,因此b的值也变成了0。 既然每次左移都可以使原来的操作翻倍,程序员们经常使用这个办法来进行快速的2 的乘法。但是你要小心,如果你将1移进高阶(31或63),那么该值将变为负值。下面的程序说明了这一点: // Left shifting as a quick way to multiply by 2. class MultByTwo { public static void main(String args[]) { int i; int num = 0xFFFFFFE; for(i=0; i<4; i++) { num = num << 1; System.out.println(num); } } 这里,num 指定要移值value 移动的。也就是,左移运算符<<使指定值的所有都左移num。每左移一个,高阶都被移出(并且丢弃),并用0填充边。这意味着当左移的运算是int 类型时,每移动1它的第31就要被移出并且丢弃;当左移的运算是long 类型时,每移动1它的第63就要被移出并且丢弃。 在对byte 和short类型的值进行移位运算时,你必须小心。因为你知道Java 在对表达式求值时,将自动把这些类型扩大为 int 型,而且,表达式的值也是int 型。对byte 和short类型的值进行移位运算的结果是int 型,而且如果左移不超过31,原来对应各的值也不会丢弃。但是,如果你对一个负的byte 或者short类型的值进行移位运算,它被扩大为int 型后,它的符号也被扩展。这样,整值结果的高就会被1填充。因此,为了得到正确的结果,你就要舍弃得到结果的高。这样做的最简单办法是将结果转换为byte 型。下面的程序说明了这一点: // Left shifting a byte value. class ByteShift { public static void main(String args[]) { byte a = 64, b; int i; i = a << 2; b = (byte) (a << 2); System.out.println("Original value of a: " + a); System.out.println("i and b: " + i + " " + b); } } 该程序产生的输出下所示: Original value of a: 64 i and b: 256 0 因变量a在赋值表达式中,故被扩大为int 型,64(0100 0000 )被左移两次生成值256 (10000 0000 )被赋给变量i。然而,经过左移后,变量b中惟一的1被移出,低全部成了0,因此b的值也变成了0。 既然每次左移都可以使原来的操作翻倍,程序员们经常使用这个办法来进行快速的2 的乘法。但是你要小心,如果你将1移进高阶(31或63),那么该值将变为负值。下面的程序说明了这一点: // Left shifting as a quick way to multiply by 2. class MultByTwo { public static void main(String args[]) { int i; int num = 0xFFFFFFE; for(i=0; i<4; i++) { num = num << 1; System.out.println(num); } } } 该程序的输出如下所示: 536870908 1073741816 2147483632 -32 初值经过仔细选择,以便在左移 4 后,它会产生-32。正如你看到的,当1被移进31 时,字被解释为负值。 四 移运算符 移运算符>>使指定值的所有移规定的次。它的通用格式如下所示: value >> num 这里,num 指定要移值value 移动的。也就是,移运算符>>使指定值的所有移num。下面的程序片段将值32移2次,将结果8赋给变量a: int a = 32; a = a >> 2; // a now contains 8 当值中的某些被“移出”时,这些的值将丢弃。例如,下面的程序片段将35移2 次,它的2个低被移出丢弃,也将结果8赋给变量a: int a = 35; a = a >> 2; // a still contains 8 用二进制表示该过程可以更清楚地看到程序的运行过程: 00100011 35 >> 2 00001000 8 将值每移一次,就相当于将该值除以2并且舍弃了余。你可以利用这个特点将一个整进行快速的2的除法。当然,你一定要确保你不会将该原有的任何一移出。 移时,被移走的最高(最左边的)由原来最高字补充。例如,如果要移走的值为负,每一次移都在左边补1,如果要移走的值为正,每一次移都在左边补0,这叫做符号扩展(保留符号)(sign extension ),在进行移操作时用来保持负的符号。例如,–8 >> 1 是–4,用二进制表示如下: 11111000 –8 >>1 11111100 –4 一个要注意的有趣问题是,由于符号扩展(保留符号)每次都会在高补1,因此-1移的结果总是–1。有时你不希望在移时保留符号。例如,下面的例子将一个byte 型的值转换为用十六 进制表示。注意移后的值与0x0f进行按与运算,这样可以舍弃任何的符号扩展,以便得到的值可以作为定义组的下标,从而得到对应组元素代表的十六进制字符。 // Masking sign extension. class HexByte { static public void main(String args[]) { char hex[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'' }; byte b = (byte) 0xf1; System.out.println("b = 0x" + hex[(b >> 4) & 0x0f] + hex[b & 0x0f]);}} 该程序的输出如下: b = 0xf1 五 无符号移 正如上面刚刚看到的,每一次移,>>运算符总是自动地用它的先前最高的内容补它的最高。这样做保留了原值的符号。但有时这并不是我们想要的。例如,如果你进行移操作的运算不是字值,你就不希望进行符号扩展(保留符号)。当你处理像素值或图形时,这种情况是相当普遍的。在这种情况下,不管运算的初值是什么,你希望移后总是在高(最左边)补0。这就是人们所说的无符号移动(unsigned shift )。这时你可以使用Java 的无符号移运算符>>> ,它总是在左边补0。 下面的程序段说明了无符号移运算符>>> 。在本例中,变量a被赋值为-1,用二进制表示就是32全是1。这个值然后被无符号移24,当然它忽略了符号扩展,在它的左边总是补0。这样得到的值255被赋给变量a。 int a = -1; a = a >>> 24; 下面用二进制形式进一步说明该操作: 11111111 11111111 11111111 11111111 int型-1的二进制代码>>> 24 无符号移2400000000 00000000 00000000 11111111 int型255的二进制代码 由于无符号移运算符>>> 只是对32和64的值有意义,所以它并不像你想象的那样有用。因为你要记住,在表达式中过小的值总是被自动扩大为int 型。这意味着符号扩展和移动总是发生在32而不是8或16。这样,对第7以0开始的byte 型的值进行无符号移动是不可能的,因为在实际移动运算时,是对扩大后的32值进行操作。下面的例子说明了这一点: // Unsigned shifting a byte value. class ByteUShift { static public void main(String args[]) { int b = 2; int c = 3; a |= 4; b >>= 1; c <<= 1; a ^= c; System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c); } } 该程序的输出如下所示: a = 3 b = 1 c = 6 还不清楚,移位运算有多大的用处,可是面试时经常会考。现在重温一下子。 原文地址:(http://www.ddvip.net/program/java/index1/28.htm)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值