位操作之美

数在计算机中是以二进制形式表示的,分为正数和负数。而原码反码补码都是有符号定点数的表示方法。

数值有正负之分,计算机就用一个数的最高位存放符号(0为正、1为负),这就是机器数的原码了。有了数值的表示方法就可以对数进行算术运算,但是很快就发现用带符号位的原码进行乘除运算时结果正确,而在加减运算的时候就出现了问题。假如字长为8个位,那么:
110 - 110 = 110 + (-1)10 = 010,而
00000001 + 10000001 = 10000010 = (-2)10,这就不正确了。

容易发现问题出现在带符号位的负数身上,于是就出现了反码。正整数的反码就是其自身,而负整数的反码可以通过对其绝对值逐位求反来求得。我们再看刚才的问题:
110 - 210 = 110 + (-2)10 = (-1)10
00000001 + 11111101 = 11111110 = 10000001 = (-1)10
110 - 110 = 110 + (-1)10 = 010
00000001 + 11111110 = 11111111 = 10000000 = (-0)10,这就有点问题了。

在人们的计算概念中零是没有正负之分的,所以为了避免这种歧义,就引入了补码的概念。正数的补码,与原码相同;负数的补码,符号位为1,其余位为该数绝对值的原码按位取反;然后整个数加1,也就是在反码的机会上加1。我们再看上面的例子:
110 - 110 = 110 + (-1)10 = 010
00000001 + 11111111 = 0000000 = 010,就正确了,当然这里溢出了一位。

所以补码的设计目的是:

  • 使符号位能与有效值部分一起参加运算,从而简化运算规则;
  • 使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计。

注意在位操作时,所有的操作数都会转化为Big-endian的补码的形式再做运算,而我们看到的是计算机把补码又转为原码的表示。。补码转原码的过程和原码转补码的过程一致,即对现在的补码再求一次补码。在Javascript中支持的位运算见下表:

操作符使用描述
按位与a & b如果a、b对应位都为1则为1,否则为0
按位或a | b如果a、b对应位其中有一个为1,则为1,否则为0
按位异或a ^ b如果a、b对应位其中只有一个为1,则为1,否则为0
按位非~ a对a所有位取反,1变0,0变1
左移a << b每移左一位,a高阶位被移除,右边补0
有符号右移a >> b每移右一位,a高阶位补充a对应的符号位
无符号右移a >>> b每移右一位,a的高阶位用0填充

弄清楚了这些基本知识,我就可以讲一下位运算的应用。如果你信二进制,信位运算,就一定要有种试图改变你思维的气概,放弃一些你原来的想法吧,现在就放空你的大脑,这就好比一杯装满水的杯子,再怎么也无法容纳新的东西了。和我一起进入神奇的位运算之旅吧!

利用按位与&取特定位,比如num & 1,就可以用于判断num是否为偶数,33 & 1 = 118 & 1 = 0
还有一个有意思的是与2的幂的数做模运算,比如:17 % 4 = 1,如果改用位运算要快得多,17 & (4-1) = 1,规律是:左操作数 & (右操作数2的幂数-1)

利用按位或|可以强行给特定位赋值,比如num | 1,可以得到不小于本身最近的奇数,-34 | 1 = -33

利用按位异或^可以强行给特定位取反,有联想吗?没有想法的,说明你没把水到干净。我觉得你至少要联想到奇数 ^ 1,可以得到不大于本身最近的偶数,而偶数 ^ 1,可以得到不小于自身最近的奇数。^还有一个特性,就是两次对同一个数作异或会还原,也就是说a ^ b ^ b = a,同时大家不知道发现没有,对于二元位运算时满足交换律的,那就是说a ^ b ^ a = b ^ a ^ a = b,那其实可以写出一个很诡异的把a、b的值对调的过程:

var a = 1, b = 2;
//开始对调
a ^= b; //a = 1 ^ 2;
b ^= a; //b = (a ^ b) ^ b = a = 1;
a ^= b; //a = (a ^ b) ^ (a) = b ^ a ^ a = b = 2;
//对调完成
//其实只要是一对相反的操作
a += b; //a = a + b; 加法符号交换律
b = a - b; //b = (a+b) - b = a; 这里由于+ -不符合交换律
a -= b; //a = (a+b) - a = b;
//发挥你的想象力吧!

按位非~,其实就是让a变为-a-1,这里对有符号的整数,在Javascript都是有符号的整数。那如果让一个数反过来,那就直接~num+1就好了。

判断一个二进制数里面的1的总数是奇数还是偶数,可以这样:

var num = 1234567, b = 1;
while (b < 17) {
    num ^= (num >> b);
    b <<= 1;
}
print(num & 1); //1,说明有奇数个1
num.toString(2); //100101101011010000111,一共11个1
/*
简单解释下:
  0100101101011010000111 ① 注意这里由于1234567是个正数,所以补码和原码一样
^ 0010010110101101000011 ② 右移1位
= 0110111011110111000100 ③ 这个数字的从右边数起第一位代表了①从右边数起前两位中1的个数的奇偶性(1为奇,0为偶)

  0110111011110111000100 ①
^ 0001101110111101110001 ② 右移2位
= 0111010101001010110101 ③ 这个数字的从右边数起第一位代表①从右边数起前四位中1的个数的奇偶性
...以此类推,到了第五次操作后的num,最后一位就代表了原数整个32位中有1个数的奇偶性。
*/

怎么样?位运算用起来会有点爱不释手吧,再来一个难的,如果我要计算二进制数中1的个数,怎么办呢?建议你自己先想下,下面是答案:

var x = 1234567;
x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
x = (x & 0x0000FFFF) + ((x >> 16) & 0x0000FFFF);
print(x); //11
/*
典型的分治法,不要看了二进制就忘了分治法了
第一步我先把原数的补码分成2个起共16份,数出里面1的个数,最多就是10即2个。第二部就是合并第一步中的2分份,那就直接做加法,以此类推...
*/

求绝对值可以这样:(x ^ (x >> 31)) - (x >> 31);。这里x >> 31就是取符号位,如果是正数,那补码也是原码,所以x >> 31位0,而(x^0) - 1显然为x。如果是负数,那么x^1,就相当于按位取反,这时得到一个正数的补码,由于原数的原码到补码过程是加了1的,所以这里要减去1,即(x^1) - 1。怎么样,位运算很精彩,很美妙吧。按位异或还可以这样玩,我们知道a*b的正负性与a和b是否同号相关,这里就正好用上^,那么a*b的正负性就和a^b一致。怎么样,很好玩吧,记得运用到你的工作中去,这才是王道。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值