剑指offer:Python不用加减乘除做加法 最详细的解答 位运算做加法 二进制实现加法 按位取反

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

思路

在实现这题前,先要系统的了解下“位运算”的知识

二进制中的:按位与(&) 按位或(|) 异或运算(^)

  • “&” 参加运算的两个数据,按照二进制进行按位与的运算。

    运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
    即:两位同时为“1”,结果才为“1”,否则为0。

    例如:3 & 5 即 0000 0011 & 0000 0101 = 0000 0001 所以,3 & 5 的值得1。
    0000 0011
    0000 0101
    ↓ ↓ ↓
    0000 0001 -> 1

  • “|” 参加运算的两个对象,按二进制位进行“或”运算。

    运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;

    即 :参加运算的两个对象只要有一个为1,其值为1。

    例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111 因此,3|5的值得7。
    0000 0011
    0000 0101
    ↓ ↓ ↓
    0000 0111 -> 7

  • “^” 参加运算的两个数据,按二进制位进行“异或”运算。

    运算规则:0 ^ 0 = 0; 0 ^ 1 = 1; 1 ^ 0 = 1; 1 ^ 1 = 0;

    即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为 0

    “异或运算”的特殊作用:↓↓↓

    使特定位翻转:找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。

    例:X=1010 1110,使X低4位翻转,用X ^ 0000 1111 = 1010 0001即可得到。
    当然还有接下来要用到的:二进制实现 加法

二进制 实现 加法 详解

  • 基本原理(不进位情况)

    以5+2为例:
    5 的二进制表示: 101
    2 的二进制表示: 010
    两个数字“异或” ^ 的结果是: 111 => 7
    上面的到的结果是就是 5 + 2 的实际结果

    以6+17为例:
    6 的二进制表示: 000110
    17 的二进制表示: 010001
    6 ^ 17 的结果是: 010111 => 23

    以7+14为例:
    7 的二进制表示为: 000111
    14的二进制表示为: 001110
    7 ^ 14 的结果为: 001001 => 9

    如果直接按位异或的话结果为9,明显不等于7+14=21 ?那么该怎么操作呢?

    通过上面的三个例子仔细观察可以发现:当二进制数的每一位加法中不发生进位时,按位异或的结果就是最终的加法结果,如5+2。如果从十进制的角度来考虑进位的话,按理说 6+17也要进位,为啥,就能直接 ^ 实现呢?为啥 7+14 就不行呢?一切疑问看下面的详解 ↓↓↓

  • 通用原理(进位详解)
    先看例子:13+19
    13 的二进制表示:01101
    19 的二进制表示:10011
    13^19 的结果是: 11110 => 30
    如直接按位异或的话结果为30,不等于13+19=32 !!!

    在上述13+19的例子中,按位异或的结果是11110,这是不考虑进位的结果。如果考虑进位的话,那该如何操作呢?

    进位值如何确定?
    13+19 的进位值为10 ,它是这样等到的:
    13的二进制 & 19的二进制 再左移1位( 01101 & 10011)<<1 -> 10

    通用方式
    所以,二进制的加法操作的通用方式是:
    先不考虑进位,对两个数的二进制数 做 ^ 操作,得到一个结果;再求进位值;
    求出的进位值 需要 和之前的得到的结果 再不考虑进位的情况 按位取异或,再取这两个的进位值,不断重复着两步操作;直到 进位值 为 0 就不需要再进行下去了,到达此时的 ^ 结果就是,两数相加的结果。

    下面是详细过程:
    先不考虑进位,对两个数的二进制进行按位异或,得到:

    0 0 1 1 0 1 ——> 13
    0 1 0 0 1 1 ——> 19
    ……^ 操作……
    0 1 1 1 1 0

    计算进位值:原来两个二进制数 作 & 操作,再左移1位,得到:

    0 0 1 1 0 1
    0 1 0 0 1 1
    ……& 操作……
    0 0 0 0 0 1 << 1
    ↓ ↓ ↓
    0 0 0 0 1 0
    原理:要是能进位,取&后肯定要是1,那么两个二进制数 对应位 肯定两个都是1,联想到二进制进位的原理,如果一位是1你要再想加1,是不是向前移一位,后面那位变成0啊!
    以上两步就是 基本步骤!!!
    …………………………

    重复第一步第二步,将第一步和第二步得到的1 1 1 1 0 和 0 0 0 1 0
    同样先不考虑进位,即再按位异或等到一个值,再求两者的进位值(按位作&再左移一位)得到:
    0 1 1 1 1 0
    0 0 0 0 1 0
    …………………………
    不进位: 0 1 1 1 0 0
    进位值: 0 0 0 1 0 0
    …………………………
    再次重复上述步骤。得到:
    不进位: 0 1 1 0 0 0
    进位值: 0 0 1 0 0 0
    …………………………
    不进位: 0 1 0 0 0 0
    进位值: 0 1 0 0 0 0
    …………………………
    不进位: 0 0 0 0 0 0
    进位值: 1 0 0 0 0 0
    …………………………
    不进位: 1 0 0 0 0 0
    进位值: 0 0 0 0 0 0
    …………………………
    最后的进位值为0,停止循环,得到最后结果:
    1 0 0 0 0 0 => 32,即为最后结果。

def add (a,b):
    num1 = a ^ b
    num2 = (a & b) << 1
    while num2 != 0:
        temp1  = num1 ^ num2
        temp2 = (num1 & num2) << 1
        num1 = temp1
        num2 = temp2
    return num1
print(add(7,14)) # 输出为21

Python实现

  • 其他语言 最后只要直接 return xor_num 即可,但是 Python是可变长的,没有无符号右移操作,所以需要越界检查,具体加的判断检查的两种写法是什么意思?下面进行详解↓↓↓

第一种写法: xor_num <= 0x7FFFFFFF 和 xor_num - 0x100000000

class Solution:
    def Add(self, num1, num2):
        xor_num = num1 ^ num2
        and_num = (num1 & num2) << 1
        while and_num != 0:
            tmp1 = xor_num ^ and_num
            tmp2 = (xor_num & and_num) << 1
            tmp1 = tmp1 & 0xFFFFFFFF
            xor_num = tmp1
            and_num = tmp2
        return xor_num if xor_num <= 0x7FFFFFFF else xor_num - 0x100000000


obj = Solution()
print(obj.Add(-5, -7)) # -12
'''
最后一步严格意义上还是用了“-”但是,能通过,
主要是了解下 正数和负数的范围到底是如何确定的
'''

xor_num <= 0x7FFFFFFF 确定 xor_num是一个正数

0xFFFF FFFF = 1111 1111 1111 1111 1111 1111 1111 1111 让最高位变成0 那么它就是正数了
即是0111 1111 1111 1111 1111 1111 1111 1111 这个是16进制为: 0x 7FFF FFFF
所以<= 0x7FFFFFFF 就能确定一个正数了

else xor_num - 0x100000000 这句话又是什么含义?

  • 先回顾下 补码 的特性
    在这里插入图片描述
    模是什么?“模”是指一个计量系统的计数范围,
    表示n位的计算机计量范围是:0~ 2 ^ n - 1 ,模 = 2 ^ n ;比如32位,模就是 2 ^ 32

    再来看特性1,利用特性1:如果是负数,那么该怎么确定它是负数呢?
    xor_num + 原数字(负数是以补码表示的) = 模
    所以“用该数字减去模” 即可,32位是,2^32 最大是1111 1111…1111 ->0xffff ffff ,
    再加1,进一位就是 0x1 0000 0000(注意这里的16进制是个9位数),所以作差得到的必定是负数,这样就能确定一个负数了

第二种写法: ~ 按位取反

class Solution:
    def Add(self, num1, num2):
        xor_num = num1 ^ num2 & 0xffffffff
        and_num = ((num1 & num2) << 1) & 0xffffffff
        while and_num:
            temp1 = (xor_num ^ and_num) & 0xffffffff
            temp2 = ((xor_num & and_num) << 1) & 0xffffffff
            xor_num = temp1
            and_num = temp2
        return xor_num if xor_num <= 0x7fffffff else ~(xor_num ^ 0xffffffff)


obj = Solution()
print(obj.Add(-5, -7)) # -12

~(xor_num ^ 0xffffffff) 这个是什么意思?
补充知识:~ 按位取反

  • 数据在内存中始终是以二进制形式存放的;数值是以补码表示的。
    二进制数在内存中以补码的形式存储?(额,大概都是这个意思)

  • 按位取反:二进制每一位取反,0变1,1变0。

    ~9 的计算步骤:
    ↓ ↓ ↓
    转二进制:0 1001
    ↓ ↓ ↓
    计算补码:0 1001
    ↓ ↓ ↓
    按位取反:1 0110 -> - 6

    转为原码:
    ↓ ↓ ↓
    按位取反:1 1001 -> - 9
    ↓ ↓ ↓
    末位加一:1 1010
    ↓ ↓ ↓
    符号位为1是负数,即 -10

  • 在计算机中一个整型数4字节,1字节占8位,假设var x = 10;
    所以数字10在计算机中存储占32位,即:

    00000000 00000000 00000000 00001010,

    按位取反,得:

    11111111 11111111 11111111 11110101,
    ↑ ↑ ↑
    至此这个二进制数据就是“~10”,最高位是1表示它是个负数;

    那么我们如何再转化为十进制数呢?

    这里又涉及到了负数在计算机里的存储问题,计算机里,负数以其 正值 的 补码 形式存在。

  • 再看一个例子:

    -10 ,二进制表示为:

    10000000 00000000 00000000 00001010

    原码,取其绝对值也就是10,即:

    00000000 00000000 00000000 00001010

    反码,按位取反,得:

    11111111 11111111 11111111 11110101

    补码,即将反码加1,得

    11111111 11111111 11111111 11110110
    ↑ ↑ ↑
    至此,我们得到了计算机中 - 10 的二进制存储形式。

  • 然后我们再回到上一个问题,我们怎么根据计算机中的补码得到这个负数呢?

    我们可以按原路返回,就是将计算机中存储的二进制补码减1,然后取反,再得到原码,换成相应负数即可,不过这样有点麻烦,因为涉及到了减法操作。

  • 另一种方法,将负数的补码先取反,然后加1,最高位置换为1即可。

    对于 ~10,在计算机中存储为

    11111111 11111111 11111111 11110101 (这是10取反的结果,但却是未知数X的补码形式)

    先取反,得

    00000000 00000000 00000000 00001010 (此处,再次取反,返回10)

    再加1,得

    00000000 00000000 00000000 00001011 (10+1得11)

    最高位变1,即

    10000000 00000000 00000000 00001011 (取相反数即-11)

    结果是“-11”

    由此我们可以看出规律:“~x”的结果为 “ -(x+1)”

  • math.abs(~2016) = 2017

    ~表示按位取反,math.abs函数表示取绝对值.

    10进制数2016,转32位2进制数为:0000 0111 1110 0000

    ~按位取反:1111 1000 0001 1111,对应十进制数:-2017

    math.abs(-2017)=2017

    所以“~2018”就等于“-2019”,math.abs(-2019)即2019

至此 我们在回到 上面的代码

return xor_num if xor_num <= 0x7fffffff else ~(xor_num ^ 0xffffffff)
'''
else: 表示当负数时,先 xor_num ^ 0xffffffff 表示 先去掉“-” ;
然后~(xor_num ^ 0xffffffff) 按位取反,其结果就为 -(xor_num + 1) 
是不是和 第一种方法的:0x100000000 (9位数,进了一个1)异曲同工之妙呢?
'''

最后总结一句话:一个简单的题,背后居然有这么多的小知识点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值