位运算基础及基本应用
- 在处理整形数值时,可以直接对组成整形数值的各个位进行操作。这意味着可以使用屏蔽技术获得整数中的各个位(??)
- 位运算是针对整数的二进制进行的位移操作
- 整数 32位 , 正数符号为0,负数符号为1。十进制转二进制 不足32位的,最高位补符号位,其余补零
- 在Java中,整数的二进制是以补码的形式存在的
- 位运算计算完,还是补码的形式,要转成原码,再得出十进制值
- 正数:原码=反码=补码 负数:反码=原码忽略符号位取反, 补码=反码+1 例如:十进制4 转二进制在计算机中表示为(补码) 00000000 00000000 00000000 00000100
例如:十进制-4 转二进制在计算机中表示为(补码) 11111111 11111111 11111111 11111100
位运算符
&(与)
与( & )每一位进行比较,两位都为1,结果为1,否则为0
|(或)
或( | )每一位进行比较,两位有一位是1,结果就是1
^(异或)
每一位进行比较,相同为0,不同为1
规律
- 异或可以理解为不进位加法: 1+1=0 0+0=0 1+0=1
- 交换律。可任意交换运算因子的位置,结果不变 a ^ b = b ^ a
- 结合律 a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c
- 对于任何数x,(即同自己求异或为0,同0求异或为自己) x ^ x = 0, x ^ 0 = x
- 自反性(即连续和同一个因子做异或运算,最终结果为自己) a ^ b ^ b = a ^ 0 = a
~ (非/取反)
每一位进行比较,按位取反(符号位也要取反)
<<(向左位移)
左移( << ) 整体左移,右边空出位补零,左边位舍弃 (-4 << 1 = -8)
>>(向右位移)
整体右移,左边空出位补零或补1(负数补1,整数补0),右边位舍弃 (-4 >> 1 = -2)
>>>(无符号右移)
同>>,但不管正数还是负数都左边位都补0 (-4 >>> 1 = 2147483646)
机器数和机器数的真值
机器数
一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用机器数的最高位存放符号,正数为0,负数为1。
机器数的真值
由于机器数的第一位是符号位,所以机器数的形式值就不等于真正的数值.为了区别起见,将带符号的机器数对应的真正数值成为机器数的真值。比如0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1
原码、反码、补码
对于计算机而言,万物皆0、1,所有的数字最终都会转换成0、1的表示,有3种机器存储一个具体数字的编码方式,分别是:原码、反码和补码。
原码
原码表示法在数字前面增加了一位符号位,即最高位为符号位,正数位该位为0,负数位该位为1.比如十进制的5如果用8个二进制位来表示就是00000101,-5就是10000101。
反码
正数的反码是其本身,负数的反码在其原码的基础上,符号位不变,其余各个位取反。5的反码就是00000101,而-5的则为11111010。
补码
正数的补码是其本身,负数的补码在其原码的基础上,符号位不变,其余各位取反,最后+1。即在反码的基础上+1。5的反码就是00000101,而-5的则为11111011。在计算机中负数采用二进制的补码表示,10进制转为二进制得到的是源码,将源码按位取反得到的是反码,反码加1得到补码
位运算应用
1.判断奇偶数
方法一
num%2 取模为0是偶数,反之则为奇数
String result=(num%2==0?)"偶数":"奇数";
方法二
偶数最低位是0.奇数最低位是1。对最后一位&1,为0是偶数,为1是奇数
String result=(num&1==0)?"偶数":"奇数";
2.获取二进制位是1还是0
实例:判断第五位的二进制位是1还是0
//方法一——将1左移4位,判断第五位是1还是0,然后右移4位判断0还是1)
String result1=(num&(1<<4)>>4)==0?"0":"1";
//方法二——第五位二级制数右移4位,&1判断0还是1
String result2=((num>>4)&1==0)?"0":"1";
问题:方法一中,左移4位判断为1还是0后,还有必要在右移4位吗?
3.交换两个变量的值
可参考我的另一篇博客(Java版)算法——交换两个基本数据类型的变量值和数组中元素调换位置
代码及运算过程
int a = 50; //二进制 110010
int b = 60; //二进制 111100
a = a^b; //110010,111100——>001110
b = a^b; //001110,111100——>110010 ——>50
a = a^b; //001110,110010——>111100 ——>60
System.out.println(f+" "+g);//输出结果是:60 50
利用异或的规律证明
a = a^b;
b = a^b; //这里 b=a^b=(a^b)^b=a^b^b=a
a = a^b; //这里 a=a^b=(a^b)^a=b
4.不用判断语句,求整数的绝对值
num>>31,有符号右移,正数为0,负数为-1 num>>>31,无符号右移,正数为0,负数为1 num^0,为本身(同0求异或为自己) num^-1,相当于取反;取反在+1,相当于是绝对值
int result=(num^(num>>31))+(num>>>31);
位运算实现加减乘除
加法
十进制的加法运算:
13 + 9 = 22
拆分运算过程:
- 不考虑进位,分别对各位数进行相加,结果为sum: 个位数3加上9为2;十位数1加上0为1; 最终结果为12;
- 考虑进位,结果为carry: 3 + 9 有进位,进位的值为10;
- 如果步骤2所得进位结果carry不为0,对步骤1所得sum,步骤2所得carry重复步骤1、 2、3;如果carry为0则结束,最终结果为步骤1所得sum: 这里即是对sum = 12 和carry = 10重复以上三个步骤,
- 不考虑进位,分别对各位数进行相加:sum = 22;
- 只考虑进位: 上一步没有进位,所以carry = 0;
- 步骤2carry = 0,结束,结果为sum = 22.
二进制实现上述过程:
13的二进制为0000 1101,9的二进制为0000 1001:
- 不考虑进位,分别对各位数进行相加: sum = 0000 1101 + 0000 1001 = 0000 0100
- 考虑进位: 有两处进位,第0位和第3位,只考虑进位的结果为: carry = 0001 0010
- 步骤2carry == 0 ?,不为0,重复步骤1 、2 、3;为0则结束,结果为sum: 本例中,
- 不考虑进位sum = 0001 0110;
- 只考虑进位carry = 0;
- carry == 0,结束,结果为sum = 0001 0110 转换成十进制刚好是22.
伪代码推理:
以3+9为例
a = 0011, b = 1001;
start;
first loop;
1.1 sum = 1010
1.2 carry = 0010
1.3 carry != 0 , go on;
second loop;
2.1 sum = 1000;
2.2 carry = 0100;
2.3 carry != 0, go on;
third loop;
3.1 sum = 1100;
3.2 carry = 0000;
3.3 carry == 0, stop; result = sum;
end
有的加法操作是有连续进位的情况的,所以这里要在第三步检测carry是不是为0,如果为0则表示没有进位了,第一步的sum即为最终的结果。
代码实现
/**
* 功能描述:递归形式实现加法
*/
int add(int a ,int b) {
if (b == 0) {
return a;
} else {
//进位
int carry = (a & b) << 1;
//不进位加法
a = a ^ b;
return add(a, carry);
}
}
/**
* 功能描述:非递归形式实现加法
*/
int add2(int a ,int b){
int carry;
while (b != 0){
//进位
carry = (a & b) << 1;
//不进位加法
a = a ^b;
b = carry;
}
return a;
}
减法
思路:
减法操作,可以利用加法操作实现。例如:a+b=a+(-b)。需要将b由正数转为负数(二进制方式),然后执行加法操作。
为什么不实现减法器的原因:
减法比加法来的复杂,实现起来比较困难。加法运算其实只有两个操作,加、 进位,而减法呢,减法会有借位操作,如果当前位不够减那就从高位借位来做减法,这里就会问题了,借位怎么表示呢?加法运算中,进位通过与运算并左移一位实现,而借位就真的不好表示了。所以我们自然的想到将减法运算转变成加法运算。
正数变为负数,二进制如何改变?
通过2的补码来表示负数的,将数字的正负号变号(即取反+1)
- 第一步,每一个二进制位都取相反值,0变成1,1变成0(即反码)。
- 第二步,将上一步得到的值(反码)加1。
代码实现
/**
* 功能描述:减法(加上一个负数,负数=正数取反+1)
*/
int subtraction(int a ,int b){
//b由正数转为负数
b = add(~b,1);
return add(a,b);
}
乘法
实现方式一——利用加法累加
思路
乘数加上乘数倍的自己,然后处理正负号的问题。
- 处理乘数和被乘数的绝对值的乘积。
- 根据它们的符号确定最终结果的符号。
代码实现
/**
* 功能描述:乘法(利用加法实现)
* @param a 被乘数
* @param b 乘数
* @return 乘法结果
*/
int multiplication(int a,int b){
// 取绝对值,如果为负则取反加一得其补码,即正数
int multiplicand = a < 0 ? add(~a, 1) : a;
int multiplier = b < 0 ? add(~b , 1) : b;
// 计算绝对值的乘积
int product = 0;
int count = 0;
while(count < multiplier) {
product = add(product, multiplicand);
// 这里可别用count++,都说了这里是位运算实现加法
count = add(count, 1);
}
// 确定乘积的符号
// 只考虑最高位,如果a,b异号,则异或后最高位为1;如果同号,则异或后最高位为0;
if((a ^ b) < 0) {
product = add(~product, 1);
}
return product;
}
缺点
第一步对绝对值作乘积运算我们是通过不断累加的方式来求乘积的,这在乘数比较小的情况下还是可以接受的,但在乘数比较大的时候,累加的次数也会增多,这样的效率不是很高
实现方式二——求乘积
思路
以13*14为例
如果乘数当前位为1,则取被乘数左移一位的结果加到最终结果中;如果当前位为0,则取0加到乘积中(加0也就是什么也不做);
实现步骤
- 判断乘数是否为0,为0跳转至步骤4
- 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
- 被乘数左移一位,乘数右移一位;回到步骤1
- 确定符号位,输出结果;
代码实现
/**
* 功能描述:乘法(推荐)
* 考虑符号问题
* @param a 被乘数
* @param b 乘数
* @return 乘法结果
*/
int multiplication(int a,int b){
//将乘数和被乘数都取绝对值
int multiplicand = a < 0 ? add(~a, 1) : a;
int multiplier = b < 0 ? add(~b , 1) : b;
//计算绝对值的乘积
int product = 0;
while(multiplier > 0) {
// 每次考察乘数的最后一位
if((multiplier & 0x1) > 0) {
product = add(product, multiplicand);
}
// 每运算一次,被乘数要左移一位
multiplicand = multiplicand << 1;
// 每运算一次,乘数要右移一位(可对照上图理解)
multiplier = multiplier >> 1;
}
//计算乘积的符号
if((a ^ b) < 0) {
product = add(~product, 1);
}
return product;
}
除法
实现方式一——利用减法累减
思路
除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。
注意
需注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为正,异号为负;余数的符号和被除数一样。
代码实现
/**
* 功能描述:除法(减法实现)
*/
int division(int a,int b){
// 先取被除数和除数的绝对值
int dividend = a > 0 ? a : add(~a, 1);
int divisor = b > 0 ? b : add(~b, 1);
int quotient = 0;// 商
int remainder = 0;// 余数
// 不断用除数去减被除数,直到被除数小于被除数(即除不尽了),直到商小于被除数
while(dividend >= divisor){
dividend = subtraction(dividend, divisor);
quotient = add(quotient, 1);
}
// 确定商的符号,如果除数和被除数异号,则商为负数
if((a ^ b) < 0){
quotient = add(~quotient, 1);
}
// 确定余数符号
remainder = b > 0 ? dividend : add(~dividend, 1);
// 返回商
return quotient;
}
缺点
如果被除数非常大,除数非常小,那就要进行很多次减法运算,效率低。
实现方式二一增大步长使用减法累减
思路
所有的int型数据都可以用[2 ^ 0, 2 ^ 1,…,2 ^ 31]这样一组基来表示(int型最高31位)。不难想到用除数的[2 ^ 31,2 ^ 30,…,2 ^ 2,2 ^ 1,2 ^ 0]倍尝试去减被除数,如果减得动,则把相应的倍数加到商中;如果减不动,则依次尝试更小的倍数。这样就可以快速逼近最终的结果。
2的i次方其实就相当于左移i位,因为int型数据最大值就是2^31,所以从31位开始
代码实现
/**
* 功能描述:除法(推荐)
*/
int division(int a,int b){
// 先取被除数和除数的绝对值
int dividend = a > 0 ? a : add(~a, 1);
int divisor = b > 0 ? b : add(~b, 1);
// 商
int quotient = 0;
// 余数
int remainder = 0;
for(int i = 31; i >= 0; i--) {
//比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较,
//效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor
if((dividend >> i) >= divisor) {
quotient = add(quotient, 1 << i);
dividend = subtraction(dividend, divisor << i);
}
}
// 确定商的符号
if((a ^ b) < 0){
// 如果除数和被除数异号,则商为负数
quotient = add(~quotient, 1);
}
// 确定余数符号
remainder = b > 0 ? dividend : add(~dividend, 1);
// 返回商
return quotient;
}
下篇文章总结下位运算在算法解题中具体如何使用:(Java)算法——位运算在算法题中的应用