算法通关村十一关:位运算规则

位运算:理解位运算的规则

概念说明

机器数
最高位为符号位,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
左移10000 1100    12
右移10000 0011    3

原始    0001 1101 29
左移20111 0100 116
左移31110 1000(补码) -24

原始    0011 0010 50
右移10001 1001 25
右移20000 1100 12

原始           1100 1110(补码)    -50
算术右移21111 0011(补码)    -13
逻辑右移20011 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))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值