程序中的所有数在计算机内存中都是以二进制的形式储存的,位运算就是直接对整数在内存中的二进制位进行操作。
位运算是更底层一些的操作,位运算通常相比其他运算有着更高的效率。
运算符号
含义 | C\C++ | Java |
---|---|---|
按位与(AND) | a & b | a & b |
按位或(OR) | a | b | a | b |
按位异或(XOR) | a ^ b | a ^ b |
按位取反(NOT) | ~ a | ~ a |
左移 | a << b | a << b |
右移 | a >> b | a >> b |
无符号右移 | 无 | a >>> b |
按位与(AND)
按位与处理两个长度相同的二进制数,两个相应的二进位都为1,该位的结果值才为1,否则为0。例如:
1 | 0101(十进制5) |
按位或(OR)
按位或处理两个长度相同的二进制数,两个相应的二进位中只要有一个为1,该位的结果值为1。例如:
1 | 0101(十进制5) |
按位异或(XOR)
按位异或运算,对等长二进制模式按位或二进制数的每一位执行逻辑按位异或操作。
操作的结果是如果某位不同则该位为1,否则该位为0。例如:
1 | 0101(十进制5) |
按位取反(NOT)
取反是一元运算符,对一个二进制数的每一位执行逻辑反操作。使数字1成为0,0成为1。例如:
1 | NOT 0111(十进制7) |
左移
左操作数按位左移右操作数指定的位数,移位后空缺的部分全部填0。
1 | 0001(十进制1) |
右移
左操作数按位右移右操作数指定的位数,左边的用原有标志位补充,右边超出的部分舍弃。
1 | 1010(十进制10) |
无符号右移
相比 C、C++、JAVA中有一个特有的无符号右移操作符“>>>”,此操作将忽略操作数的符号 同样的还有>>>=。
按位右移补零操作符。左操作数的值按右操作数指定的位数右移,左边部分总是以0填充,右边超出的部分舍弃。
1 | int a = -8; |
小结
计算机中所有数都是以补码形式存储的(正数的补码是本身)。
原码:10进制转换成2进制是原码
反码: 正数的反码是本身,负数的反码是负数的原码0变为1,1变为0 (负数求反码时候的符号位不参与变换)
补码: 正数的补码是本身,负数的补码就是负数的反码加一
总结: 正数的原码,反码 ,补码三值合一, 负数的原码,反码,补码不同。
补码的意义:
- 使符号位能与有效值部分一起参与运算,从而简化运算规则
- 使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计。
-8
原码 1000 0000 0000 0000 0000 0000 0000 1000
反码 1111 1111 1111 1111 1111 1111 1111 0111
补码 1111 1111 1111 1111 1111 1111 1111 1000
带符号右移(左边的用原有标志位补充,右边超出的部分舍弃),即
1111 1111 1111 1111 1111 1111 1111 1000 变成:
1111 1111 1111 1111 1111 1111 1111 1111
补码 1111 1111 1111 1111 1111 1111 1111 1111
反码 1111 1111 1111 1111 1111 1111 1111 1110
原码 1000 0000 0000 0000 0000 0000 0000 0001 转化为十进制也就是 -1
无符号右移(左边部分总是以0填充,右边超出的部分舍弃),即
1111 1111 1111 1111 1111 1111 1111 1000 变成:
0001 1111 1111 1111 1111 1111 1111 1111
补码 0001 1111 1111 1111 1111 1111 1111 1111 (正数的原码,反码 ,补码三值合一)
反码 0001 1111 1111 1111 1111 1111 1111 1111 (正数的原码,反码 ,补码三值合一)
原码 0001 1111 1111 1111 1111 1111 1111 1111 转化为十进制也就是 53687091
位运算应用
很多源码中有关于使用位运算的优化。但一般的业务场景不建议使用这些优化,因为大多数编译器会帮我们优化代码。还有就是这样代码的可读性也会大大降低。我们更应该关注的是整体算法的效率,把一个时间复杂度为 O(n^2) 的代码优化到 O(n*logn) 的意义会更大。
乘法
a << n 等价于 a * pow(2, n)
除法
a >> n 等价于 a / pow(2, n)
取余
等价于 a & (pow(2, n) - 1) 等价于 a % pow(2, n)
交换两个数
1 | a ^= b |
等价于
1 | a += b |
等价于
1 | temp = a |
绝对值
1 | int a = -100; |
等价于
1 | int a = -100; |
判断奇偶性
(a & 1) == 0 等价于 a % 2 != 0
判断大小
1 | public static boolean less(long n1, long n2) { |
等价于
n1 < n2
bit状态位
在一个系统中,用户一般有查询(Select)、新增(Insert)、修改(Update)、删除(Selete)四种权限,四种权限有多种组合方式,也就是有16中不同的权限状态(2的4次方)。每一位代表一个状态是true还是false。
1 | public class NewPermission { |
位计数
基于bit状态位,某个状态数里面,有多少个状态是true,就是计算这个状态数里面多少位是1。
比较朴素的方法就是:先判断n的奇偶性,为奇数时计数器增加1,然后将n右移一位,重复上面的步骤,直到移位完毕。
1 | int n = Integer.MAX_VALUE; |
高效一点的方法通过:
n & (n - 1)
可以移除最后一位1 (假设最后一位本来是0, 减一后必为1,0 & 1为 0, 最后一位本来是1,减一后必为0,0 & 1为 0)- 移除了最后一位1之后,计数加1,如果结果不为零,则用结果继续第一步。
1 | int n = Integer.MAX_VALUE; |
更高效的方法:
1 | n = n - ((n >> 1) & 0x55555555); |
将一个数向上取整为2的幂
HashMap 中的源码:
1 | static final int tableSizeFor(int cap) { |
扰动函数
HashMap 中的源码:
1 | static final int hash(Object key) { |
参考:HashMap这次是真的懂了,扰动函数、负载因子、扩容拆分全搞定
为什么1字节表示的范围是[-128,127]
一个字节8位,即可表示2^8 =256,有符号数那就可以表示2^7 =128个正数、2^7 =128个负数了,但是1000 0000和0000 0000其实都是0,所以理论上它只能表示[-127, 127]。
如果计算机用原码或者反码,会多占用一个表达(+0、-0都会有各自的原码和反码)。但计算机中使用的是补码,+0、和-0的补码都是0000 0000,所以1000 0000可以用来多表达一个数,用来表示-128最合理。
还可以从另外一个角度来理解: -127 的补码是1000 0001.再减去1 就是1000 0000 。那-127-1=-128。
总结:补码不仅解决了符号的表示的问题,还统一了符号位和数值位,使得符号位可以和数值位一起直接参与运算。