如何灵活的使用位运算?

给推荐一个牛逼up:孙哥
孙哥B站地址

位运算是计算机的核心基础,数据的表示和计算几乎都少不了,在VM以及很多高性能代码里大量使
用,甚至很多算法本身就是基于位进行的。在算法方面,很多位相关的算法有很多技巧,不学真不知
道。另外很多算法虽然看起来与位运算无关,但是用位操作优化一下,性能会提升很多,所以位运算的
问题值得好好学习。
位运算的另外一个重要场景是如果给定的元素数量很大时进行查找或者处理,这个在后面《超大规模数
据场景常见问题》一章单独说明。

  • 理解位运算的规则
  • 1. 理解位运算的基本规则
  • 2. 理解移位的原理以及乘除的关系
  • 3. 掌握位运算的常用技巧

数字在计算机中的表示

机器数一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用一个
数的最高位存放符号,正数为0,负数为1。比如,十进制中的数+3,计算机字长为8位,转换成二进制就
是00000011。如果是-3,就是10000011。这里的00000011和10000011就是机器数。
真值因为机器数第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数
10000011,其最高位1代表负,其真正数值是-3而不是形式值131(10000011转换成十进制等于
131)。所以,为了好区别,将带符号位的机器数对应的真正数值称为机器数的真值。例:00000001的真
值=+0000001=+1,10000001的真值=-0000001=-1。
计算机对机器数的表示进一步细化:原码,反码,补码。
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值,比如如果是8位二进制:

[+1] 原码 =0000 0001
[-1] 原码 =1000 0001

第一位是符号位,因为第一位是符号位,所以8位二进制数的取值范围就是:
[11111111,01111111],也即[-127,127]
反码的表示方法是:正数的反码是其本身,而负数的反码是在其原码的基础上,符号位不变,其余各个位
取反。例如:

[+1]  =原码 0000 0001=反码 0000 0001
[-1]  =原码 1000 0001=反码 1111 1110

可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值,通常要将其转换成原码再计算。
在应用中,因为补码能保持加和减运算的统一,因此应用更广,其表示方法是:

  • 正数的补码就是其本身:
  • 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+)。
[+1]  =原码 0000 0001=反码 0000 0001=补码 0000 0001
[-1]  =原码 1000 0001=反码 1111 1110=补码 1111 1111

对于负数,补码表示方式也是人脑无法直观看出其数值的,通常也需要转换成原码在计算其数值。
拓展为何会有原码,反码和补码
既然原码就能表示数据,那为什么实际软件中更多使用的是补码呢?接下来我们就看一看。
现在我们知道了计算机可以有三种编码方式表示一个数,对于正数因为三种编码方式的结果都相同:
对于正数,它的三种编码方式的结果都相同:
[+1] =原码 0000 0001=反码 0000 0001=补码 0000 0001
对于负数:
[-1] =原码 1000 0001=反码 1111 1110=补码 1111 1111
可见原码,反码和补码是完全不同的。既然原码才是被人脑直接识别并用于计算表示方式,为何还会有反
码和补码呢?

首先,因为人脑可以知道第一位是符号位,在计算的时候我们会根据符号位选择对真值区域的加减。但是
计算机要辨别"符号位"就必须获得全部的位的数据才可以,显然会让计算机的基础电路设计变得十分复
杂!于是人们想出了将符号位也参与运算的方法。我们知道,根据运算法则减去一个正数等于加上一个
负数,即:1-1=1+(-1)=0,所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。
于是人们开始深索将符号位参与运算,并且只保留加法的方法。

看个例子,计算十进制的表达式:1-1=0,首先看原码的表示:
1-1=1+(-1)=[00000001]原+[10000001]原=[10000010]原=-2
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的,这也是为何计算机内部不
使用原码表示一个数。
为了解决原码做减法的问题就出现了反码,此时计算十进制的表达式为:1-1=0

1-1=1+(-1)
=[00000001]原+[10000001]原
=[00000001]反+[11111110]反
=[11111111]反=[10000000]原
=-0

可以看到用反码计算减法结果的真值部分是正确的,但是"0"的表示有点奇怪,+0和-0是一样的,而且0带
符号是没有任何意义,而且要浪费[00000000]原和[10000000]原两个编码来表示0。于是补码的出现,
解决了0的符号以及两个编码的问题:

1-1=1+(-1)=
[00000001]原+[10000001]原
=[00000001]补+[11111111]补
=[00000000]补=[00000000]原

这样0用[00000000]表示,而以前出现问题的-0则不存在了,而且可以用[10000000]表示-128:

(-1)+(-127)=
[10000001]原+[11111111]原
=[11111111]补+[10000001]补
=[10000000]补

位运算规则

本节的内容很多你可能学过,但是请再认真思考一遍,因为大量的算法解决思路都是从这里引申出来的
计算机采用的是二进制,二进制包括两个数码:0,1。在计算机的底层,一切运算都是基于位运算实现
的,所以研究清楚位运算可以加深我们对很多基础原理的理解程度。
在算法方面,不少题目都是基于位运算拓展而来的,而且还有一定的技巧,如果不提前学一学,面试时很
难想到。
位运算主要有:与、或、异或、取反、左移和右移,其中左移和右移统称移位运算,移位运算又分为算术
移位和逻辑移位。

与,或,异或,取反

与运算的符号是&,运算规则是:对于每个二进制位,当两个数对应的位都为1时,结果才为1,否则结
果为0。

0&0=0
0&1=0
1&0=0
181=1

或运算的符号是,运算规则是:对于每个二进制位,当两个数对应的位都为0时,结果才为0,否则结果
为1。

0|0=0
0|1=1
1|0=1
1|1=1

异或运算的符号是⊕(在代码中用Λ表示异或),运算规则是:对于每个二进制位,当两个数对应的位相
同时,结果为0,否则结果为1。

0^0=0
0^1=1
1^0=1
1^1=0

取反运算的符号是~,运算规则是:对一个数的每个二进制位进行取反操作,0变成1,1变成0。

~0=1
~1=0

位移运算

移位运算按照移位方向分类可以分成左移和右移,按照是否带符号分类可以分成算术移位和逻辑移位。
原始:000001106
右移一次:000000113相当于除以2
左移一次:0000110012相当于乘以2

位移运算与乘除法的关系

观察上面的例子可以看到,移位运算可以实现乘除操作。由于计算机的底层的一切运算都是基于位运算实
现的,因比使用移位运算实现乘除法的效率显著高于直接乘除法的。
左移运算对应乘法运算。将一个数左移k位,等价于将这个数乘以2^k。例如,29左移2位的结果是
116,等价于29×4。当乘数不是2的整数次幂时,可以将乘数拆成若干项2的整数次幂之和,例如,
a×6等价于(a<<2)+(a<<1)。对于任意整数,乘法运算都可以用左移运算实现,但是需要注意益出的情
况,例如在8位二进制表示下,29左移3位就会出现溢出。
算术右移运算对应除法运算,将一个数右移k位,相当于将这个数除以2^k。例如,50右移2位的结果
是12,等价于50/4,结果向下取整。
从程序实现的角度,考虑程序中的整数除法,是否可以说,将一个数(算术)右移k位,和将这个数除以
2k等价?对于0和正数,上述说法是成立的,整数除法是向0取整,右移运算是向下取整,也是向0取
整。但是对于负数,上述说法就不成立了,整数除法是向0取整,右移运算是向下取整,两者就不相同
了。例如,(-50)>>2的结果是-13,而(-50)/4的结果是-12,两者是不相等的。因此,将一个数(算
术)右移k位,和将这个数除以2^k是不等价的。算法出题这早就考虑到了这一点,因比在大部分算法题
都将测试数据部限制在正数和0的情况,因此可以放心的左移或者右移。

位运算常用技巧

位运算的性质有很多,比处列举一些常见性质,假设以下出现的变量都是有符号整数。
·幂等律:a&a=a,a|a=a(注意异或不满足幂等律);
·交换律:a&b=b&a,a|b=b|a,a⊕b=b⊕a;
·结合律:(a&b)&c=a&(b&c),(a|b)|c=a|b|c),(a⊕b)⊕c=a⊕(b⊕c);
·分配律:(a&b)|c=(aIc)&(b|c),(aIb)&c=(a&c)I(b&c),(a⊕b)&c=(a&c)⊕(b&c);
·德摩根律:w(a&b)=(a)I(b),(a|b)=(a)&(~b);
·取反运算性质:-1=0,-a=(a-1):
·与运算性质:a&0=0,a&(-1)=a,a&(wa)=0;
·或运算性质:a|0=a;
·异或运算性质:a⊕0=a,a⊕a=0;

根据上面的性质,可以得到很多处理技巧,这里列举几个:
·a&(a-1)的结果为将a的二进制表示的最后一个1变成0;
·(补码)a&(-a)的结果为只保留a的二进制表示的最后一个1,其余的1都变成0。
处理位操作时,还有很多技巧,不要死记硬背,理解其原理对解决相关问题有很大帮助。下面的示例中,
1s和0s分别表示与等长的一串1和一串0:

获取

该方法是将1左移位,得到形如00010000的值。接着堆这个值与um执行”位与"操作,从而将位之外
的所有位清零,最后检查该结果是否为零。不为零说明位为1,否则位为0。代码如下:

boolean getBit(int num,int i){
return ((num&(1<<i)!=0));
}

设置(讲某一位设置为1)

setBit先将1左移位,得到形如00010000的值,接着堆这个值和um执行”位或“操作,这样只会改变i位
的数据。这样除i位外的位均为零,故不会影响um的其余位。代码如下:

int setBit(int num,int i){
 return num|(1<<i);
}

清零(讲某一位设置为0)

该方法与setBit相反,首先将1左移位获得形如00010000的值,对这个值取反进而得到类似11101111的
值,接着对该值和num执行”位与“,故而不会影响到num的其余位,只会清零i位。

int clearBit(int num,int i){
 int mask=~(1<<i);
    return num&mask;
}

更新

这个方法是将setBiti和clearBiti合二为一,首先用诸如11101111的值将num的第i位清零。接着将待写入值v
左移位,得到一个i位为v但其余位都为0的数。最后对之前的结果执行”位或“操作,v为1这um的i位更
新为1,否则为0:

int updateBit(int num,int i,int v){
	int mask=~(1<<i);
    return (num&mask)|(v<<i);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值