数在计算机中是以二进制形式表示的,分为正数和负数。而原码、反码、补码都是有符号定点数的表示方法。
数值有正负之分,计算机就用一个数的最高位存放符号(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 = 1
而18 & 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
一致。怎么样,很好玩吧,记得运用到你的工作中去,这才是王道。