位运算:理解位运算的规则
概念说明
机器数
最高位为符号位,0 整数,1 负数;
二进制表示
真值
机器数对应的真正数值
原码
符号位加上真值的绝对值
反码
正数的反码:本身
负数的反码:符号位不变,其余各个位取反
补码
正数的补码:本身
负数的补码:符号位不变,其余各个位取反,最后+1(即在反码的基础上+1)
拓展与思考
为什么会有原码,反码和补码?
引入反码
人:辨别符号位,用真值进行计算
计算机:辨别符号位,需要获取全部位的数据,造成设计复杂
=> 让符号位也参与运算,让减法变加法(减相当于加上一个负数),只保留加法运算
举例:1-1=0
原码计算:
1-1=1+(-1)
=[00000001]原+[10000001]原
=[10000010]原
=-2
反码计算:
1-1=1+(-1)
=[00000001]原+[10000001]原
=[00000001]反+[11111110]反
=[11111111]反
=[10000000]原
=-0
引入补码
用反码计算减法,结果的真值部分是正确的,但是0的表示有点奇怪。+0和-0是一样的,0带符号没有意义,[00000000] 和 [10000000]都是表示0
引入补码解决0的符号以及两个编码的问题
0 用 [00000000] 表示,-0不存在了
可以用 [10000000] 表示 -128
8位二进制,补码表示范围 [-128, 127],原码表示范围 [-127, 127]
32位补码表示范围[-2^31, 2^31-1]
举例:1-1=0 -1-127=-128
补码计算:
1-1=1+(-1)
=[00000001]原+[10000001]原
=[00000001]补+[11111111]补
=[00000000]补
=[00000000]原
=0
-1-127=(-1)+(-127)
=[10000001]原+[11111111]原
=[11111111]补+[10000001]补
=[10000000]补
位运算规则
逻辑运算:与、或、异或、取反
移位运算:左移和右移(注:逻辑运算有分为算术移位和逻辑移位)
逻辑运算
与(&)、或(|)、异或(^)、取反(~)
移位运算
按照移位方向分为:左移和右移
按照是否带符号分为:算术移位(带符号)和逻辑移位(不带符号)
左移 <<
高位丢弃,低位补0
算术移位和逻辑移位相同
右移 >>
算术右移:高位补最高位,低位丢弃
逻辑右移:高位补0,低位丢弃
对于0和正数,算术右移和逻辑右移的结果是相同的
移位示例
原始 0000 0110 6
左移1位 0000 1100 12
右移1位 0000 0011 3
原始 0001 1101 29
左移2位 0111 0100 116
左移3位 1110 1000(补码) -24
原始 0011 0010 50
右移1位 0001 1001 25
右移2位 0000 1100 12
原始 1100 1110(补码) -50
算术右移2位 1111 0011(补码) -13
逻辑右移2位 0011 0011(补码) 51
对于C/C++
数据类型包含有符号(signed)和无符号类型(unsigned),无声明时,默认是有符号类型;
有符号类型,右移运算为算术右移;
无符号类型,右移运算为逻辑右移;
对于Java
所有表示整数的类型都是有符号类型,算术右移和逻辑右移需要区分
算术右移 >>
逻辑右移 >>>
移位运算与乘除
移位运算可以实现乘除操作
移位运算实现乘除,效率显著高于直接使用乘除法
左移运算对应乘法运算
将一个数左移k位,等价于将这个数乘以 2^k
例:29左移2位,等价于29*(2^2)=29*4=116
乘数不是2的整数次幂时,可以拆分成若干2的整次幂之和
对于任意整数,乘法运算都可以用左移运算实现,但是需要注意溢出的情况
例:
6 = 2^2 + 2^1
a*6 = (a<<2)+(a<<1)
算术右移运算对应除法运算
将一个数算术右移k位,等价于将这个数除以 2^k,结果向下取整(注:仅对0和正数适用)
例:50算术右移2位,等价于 50/(2^2)=50/4=12(注:结果向下取整)
对于0和正数,上述说法成立;
对于负数,上述说法不成立,整数除法是向0取整,右移运算是向下取整
例:(-50)/4=-12 (-50)>>2=-13
注:大部分算法题都会将测试数据限制在正数和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=(a|c)&(b|c), (a|b)&c=(a&c)|(b&c), (a^b)&c=(a&c)^(b&c)
> 德摩根律 ~(a&b)=(~a)|(\~b), ~(a|b)=(~a)&(~b)
> 取反运算性质 -1=~0, -a=~(a-1)
> 与运算性质 a&0=0, a&(-1)=a, a&(~a)=0
> 或运算性质 a|0=a
> 异或运算性质 a^0=a, a^a=0
根据上面性质,可以得到处理技巧
- 将a的二进制表示的最后一个1变成0:a&(a-1)
- 只保留a的二进制表示的最后一个1,其余的1都变成0
还有很多技巧
1s 和 0s 分别表示与x等长的一串1和一串0
x ^ 0s = 0
x ^ 1s = ~x
x ^ x = 0
x & 0s = 0
x & 1s = x
x & x = x
x | 0s = x
x | 1s = 1s
x | x = x
获取、设置、更新某个位的数据
获取
将1左移i位,得到形如00010000的值
让这个值与num执行 位与 操作,最后检查该结果是否为0
若为零,i位为0,若不为零,i位为1
设置(将某一位设置为1)
将1左移i位,得到形如00010000的值
让这个值与num执行 位或 操作
清零(将某一位设置为0)
将1左移i位,得到形如 00010000 的值
取反,得到形如 11101111 的值
让这个值与num执行 位与 操作
更新
将 setBit 和 clearBit 合二为一
首先用形如 11101111 的值将num的第i位清零
接着将待写入值v左移i位,得到形如 000v0000 的值
最后将形如 000v0000 的值 与 处理后的清零后的num值进行 位或 操作
# 获取
def get_bit(nums, i):
# return (nums & (1 << i)) != 0
return int((nums & (1 << i)) != 0)
# 设置
def set_bit(nums, i):
return nums | (1 << i)
# 清零
def clear_bit(nums, i):
mask = ~(1 << i)
return nums & mask
# 更新
def update_bit(nums, i, v):
mask = ~(1 << i)
nums = nums & mask
return nums | (v << i)
if __name__ == '__main__':
print(get_bit(1, 1))
print(get_bit(1, 0))
print(set_bit(0, 0))
print(clear_bit(1, 0))
print(update_bit(0, 0, 1))