不使用加减运算符实现整数相加(详解)

问题

描述
给出两个整数 aa 和 bb , 求他们的和。

你不需要从输入流读入数据,只需要根据aplusb的两个参数a和b,计算他们的和并返回就行。

说明
a和b都是 32位 整数么?

是的
我可以使用位运算符么?

当然可以
样例
如果 a=1 并且 b=2,返回3。

挑战
显然你可以直接 return a + b,但是你是否可以挑战一下不这样做?(不使用++等算数运算符)

解决

之前有转过一篇位运算实现加减乘除的博客,但是其实自己一直没有细看,只是觉得写得很易懂,很详细。正好为了解决问题,就再次重温一下。文章关于位运算实现加法的方法描述大致如下:

13 + 9 = 22

  1 3
+   9
——————
  1 2

sum = 12 (不考虑进位的结果)
carry = 10 (只考虑进位的进位值)

carry 不为0,重复操作

12 + 10 = 22

  1 2
+ 1 0
——————
  2 2

sum = 22 (不考虑进位的结果)
carry = 0 (只考虑进位的进位值)

最终结果为22,其实这也就是小学学的加法进位。我们把这个方法应用于二进制的加法:

0000 1101 + 0000 1001

  0000 1101
+ 0000 1001
———————————
  0000 0100

  0000 1101
+ 0000 1001
———————————
  0001 0010

sum = 0000 0100 (不考虑进位的结果)
carry = 0001 0010 (只考虑进位的进位值)

carry 不为0,重复操作

0000 0100 + 0001 0010 

  0000 0100
+ 0001 0010
———————————
  0001 0110

  0000 0100
+ 0001 0010
———————————
  0000 0000

sum = 0001 0110 (不考虑进位的结果)
carry = 0000 0000 (只考虑进位的进位值) 

换算能够得到sum = 22。当然你会发现,二进制的这两种加法其实就是
1. 亦或运算
2. 与运算后左移一位

写成js是accepted(通过)。

拓展

但是同样的算法js上是accepted,python确是在某些用例上报内存溢出,为什么?因为题目并没有说是正整数相加,换言之是存在负整数的。所以我们要讨论两个问题:
1. 位运算加法包含负整数的解决方案
2. 位运算时的内存溢出问题来源

位运算加法包含负整数的解决方案

首先,为了区分二进制的正负,我们需要给一个符号位,0表示正,1表示负。有了符号位的二进制码我们称为原码,比如-100的原码就是:11100100,有了加法的位运算,我们就可以不用考虑减法,毕竟减法要考虑借位啊。但是统一放到加法里面还是有问题啊,正整数和负整数怎么加啊,符号都不同。所以我们就要转化成一种统一的表示方式来进行加法运算。所以大神们就想到了补码这种表示方式,补码的转换方式简单来说就是正整数原码不变,负整数原码取反加一(符号位不变),所以:

100的原码:01100100
100的补码:01100100
-100的原码:11100100
-100的补码:10011100

为什么表示成补码就可以统一加减法运算了?不妨先做个实例运算:25-13:

[25]原:`0`11001
[25]补:`0`11001
[-13]原:`1`01101
[-13]补:`1`10011

  011001
+ 110011
————————
 1001100 

我觉得这篇文章就讲得很清楚了:补码,模运算和溢出,归根结底其实是取模同余的原理,就好像钟表最多只能到12点,8点之后的8个小时和之前的4个小时的时间是一样的:

(8 + 8) mod 12 = (8 + (-4)) mod 12

为什么,因为钟表所能表示的最大的数就是12,仔细观察不难发现,两个n-1位二进制整数的绝对值相加不可能超过n位二进制整数所能表示的范围,所以我们完全可以把1个n位二进制整数作为这两个整数的模,记为A = B Mod M。对于某一个确定的模,某数A减去小于模的另一个数B,可以用A加上(-B)的补码来代替。其实我们完全能够发现,[-13]原+[-13]补=100000。

好的,搞清楚了方案我们要开始写代码了,其实代码方面也是有一定的难度的,因为js并没有什么方法会给你直接转化成补码,这是底层该干的事,不需要在软件层面再还原这个过程。这里的关键还是求出补码,所以我们写一个求补码的函数:

function getComplementCode(number){
    num = Math.abs(number);
    let i = 31;
    let arr = [];
    for(let m=31;m>=0;m--){
        arr.push(0)
    }
    if (number==0) {
        return arr;
    }
    while(num!=0) //求出其绝对值原码
    {
        arr[i]=num%2;
        i--;
        num=parseInt(num/2);
    }
    if (number>0) {
        return arr;
    }
    //这里是通过原码求补码的一种算法 
    for(i=31;i>=0;i--)
    {
        if(arr[i]==1)
        {
            for(let j=0;j<i;j++)    //取反 
                if(arr[j]==1)
                    arr[j]=0;
                else
                    arr[j]=1;
            break;
        }
    }
    return arr;
}

因为题中有给出运算最高涉及到32位整数,最高位是符号位,所以我们构造二进制补码也以32位为标准。
完整的代码:

function getSum(a,b) {
    a = getComplementCode(a);
    b = getComplementCode(b);
    // console.log(aplusbC(a,b))
    return aplusbC(a,b)
}
function aplusbC(a,b) {

    let sum = [];
    let carry = [];
    for(let i=0;i<32;i++){
        sum.push(a[i]^b[i])
        if (i<31) {
           carry.push(a[i+1]&b[i+1]) 
        }

    }
    carry.push(0)
    console.log("sum",sum.join(""))
    console.log("carry",carry.join(""))
    if (carry.join("")!=0) {
        return aplusbC(sum,carry) 
    }
    else{
        console.log("result",sum.join(""))
        if(sum[0]==1) {
            // 负数补码速算法,由最低位(右)向高位(左)查找到第一个1与符号位之间的所有数字按位取反的逆运算
            let mark = 0;
            for (let index = 31; index > 0; index--) {
                if (sum[index]==1) {
                    mark = index;
                    break;
                }

            }
            for (let index = 1; index < mark; index++) {
                if (sum[index]==0) {
                    sum[index]=1;
                }
                else{
                    sum[index]=0;
                }

            }
            sum[0]=0;
            // console.log(parseInt(sum.join(""),2)*-1)
            return parseInt(sum.join(""),2)*-1;

        }
        else{
            // console.log(parseInt(sum.join(""),2))
            return parseInt(sum.join(''),2)
        }
    }
}

代码的关键点还是十进制求补码以及补码转原码再转十进制,都已经写清楚,在leetcode也能打败75%的JavaScript提交。

位运算时的内存溢出问题来源

我们贴出最初的代码:

function aplusb(a,b) {
    let sum,carry;
    sum = a^b;
    carry = (a&b)<<1;
    console.log(sum.toString(2),carry.toString(2))
    if (carry == 0) {
        return sum;
    }
    else{
        return aplusb(sum,carry)
    }
}

运行aplusb(8,-8)的结果:

-10000 10000
-100000 100000
-1000000 1000000
-10000000 10000000
-100000000 100000000
-1000000000 1000000000
-10000000000 10000000000
-100000000000 100000000000
-1000000000000 1000000000000
-10000000000000 10000000000000
-100000000000000 100000000000000
-1000000000000000 1000000000000000
-10000000000000000 10000000000000000
-100000000000000000 100000000000000000
-1000000000000000000 1000000000000000000
-10000000000000000000 10000000000000000000
-100000000000000000000 100000000000000000000
-1000000000000000000000 1000000000000000000000
-10000000000000000000000 10000000000000000000000
-100000000000000000000000 100000000000000000000000
-1000000000000000000000000 1000000000000000000000000
-10000000000000000000000000 10000000000000000000000000
-100000000000000000000000000 100000000000000000000000000
-1000000000000000000000000000 1000000000000000000000000000
-10000000000000000000000000000 10000000000000000000000000000
-100000000000000000000000000000 100000000000000000000000000000
-1000000000000000000000000000000 1000000000000000000000000000000
-10000000000000000000000000000000 -10000000000000000000000000000000
0 0
0

结果虽然是对的,但从中间打印的sum和carry来看,正是因为直接拿十进制做位运算,负整数js是用原码(有符号位)来表示的,也就是说没有统一两个整数的编码格式,这样的计算自然也就没有意义了,虽然是通过了,效率也很低下(不必要的运算),只打败了8%的js提交。至于python发生了内存溢出,也是因为编码格式的问题,这里就不细究了。

猜你喜欢

转载自blog.csdn.net/YPJMFC/article/details/80921906