从我们开始上学的时候就知道,如果要实现加法运算就要使用“+”符号,如果要实现减法运算就要使用“-”符号……,甚至在今天的计算机中也是一样的,我们只知道怎么使用,但很少去关注他的底层是怎么实现的。如果突然哪天给你一道面试题,让你不使用"+"来实现两个数相加,你该怎么做呢,今天我们就来看一下该怎么实现。

一:不使用“+”实现两个数相加

我们先来看一道非常简单的题,在计算机中数字是由二进制位表示的,也就是说是由0和1组成的,如果我们要实现0和1之间的加法该怎么实现呢,他会有4种组合方式

1,0+0=00
2,0+1=01
3,1+0=01
4,1+1=10

我们发现一个很重要的规律,就是只有1+1有进位,其他的都没进位。所以我们判断有没有进位只需要判断a&b是否等于1即可,而a+b的值(不考虑进位)只需要计算a|b即可,看明白了这点,代码就呼之欲出了

private static int add1(int a, int b) {
    int c = (a & b) << 1;//进位的值
    int d = a ^ b;//不考虑进位,相加的值
    return c | d;//或者 return c ^ d;
}

a和b要么是1要么是0,所以这里最多也只有一个进位,很好理解。但我们计算二进制的加减法不光只有1个0或1,可能会有好多个1或0,那我们该怎么实现呢。比如a=13(1101),b=9(1001),我们该怎么计算a+b的结果。首先如果我们不考虑进位问题,那么a+b的运算会是下面这样

java不使用add直接给list集合赋值 java不用加号算结果_System


但实际上最前面和最后面的1都有了进位。

1, 我们看到如果不考虑进位,那么a+b的结果其实就是a^b的结果,我们该怎么把进位问题也考虑在内呢,实际上只有1+1的时候才会出现进位,1+0或者0+0都不会出现进位,所以我们首先想到的是&运算

2, 这里我们计算一下a&b的结果是1001,我们知道当&运算的结果为1的时候,说明参与&运算的两个都是1,既然两个都是1,那么相加的时候就肯定会有进位,所以他们进位的值实际上是10010((a&b)<<1),然后在和0100相加就是我们要求的结果,10010+00100=10110,10110实际上就是22,也就是13+9的结果

3, 但我们好像忽略了一个问题,就是这道题要求不能使用加减乘除符号,而上面我们分析的时候使用了加号,所以明显不行。通过上面的分析实际上我们已经发现了一个规律,就是a+b通过^和&运算之后又再执行相加操作,所以我们首先想到的是递归,我们来看下代码

private static int add2(int a, int b) {
    if (a == 0 || b == 0)
        return a ^ b;
    return add2(a ^ b, (a & b) << 1);
}

第3行表示的是如果a== 0就返回b,如果b==0就返回a,这种写法少了一个if语句的判断会更简洁。为了验证代码的准确性我们随便找几个数据测试一下

int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, -Integer.MAX_VALUE, Integer.MAX_VALUE, -8, -9};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "+" + array[(i << 1) + 1] + "=" + add2(array[i << 1], array[(i << 1) + 1]));
    }

上面的代码可以不用看,我们来看一下运行结果

java不使用add直接给list集合赋值 java不用加号算结果_i++_02


经过测试,发现我们的代码完全正确,没有使用“+”实现了两个数相加。上面的递归我们还可以改为非递归的方式

private static int add3(int a, int b) {
    while (b != 0) {
        int temp = a ^ b;
        b = (a & b) << 1;
        a = temp;
    }
    return a;
}

这个也很好理解,每次计算的时候要对a和b进行重新赋值,然后再不断的循环,直到b等于0的时候停止循环,我们知道这里在运算的时候b表示的是进位的值,当b等于0的时候就表示没有进位,没有进位就退出循环,这就是使用位运算来实现加法。我们假设a=13,b=9来画个图加深一下理解

java不使用add直接给list集合赋值 java不用加号算结果_i++_03


二:不使用“-”实现两个数相减

既然加法都实现了,那么减法就更容易了,a-b,直接改为a+(-b)即可,那么请等一下,我们不是说不能使用“-”吗,这里明显有了“-”符号,肯定不符合规则,那么别着急,在计算机中一个数的相反数还可以用另一种方式来表示,那就是
a的相反数是~a+1
上面“+”我们已经实现了,“~”不属于四则运算符,所以代码也很容易写出

private static int subtraction(int a, int b) {
    return add3(a, add3(~b, 1));
}

这种实现就更简洁了,直接一行代码搞定,代码中add3(~b,1)表示的是-b。如果不使用加法是否能实现两个数相减呢,其实也是可以的,我们这样来思考一下,比如a-b

1, 如果b等于0,我们直接返回a即可。如果b不等于0,我们可以先把a和b上同为1的数字给去掉,那么怎么去掉呢,其实很简单,我们先要计算c=a&b,那么c中为1的位置在a和b中相对应的位置上也是1,然后再通过异或运算就可以把它给移除。

2, 在经过第一步执行之后,a和b在相同的位置上要么都是0,要么一个0一个1,不可能全是1了,那么下面就要会分为3种情况了(我们先不考虑因不够减而借位的问题

(1), 如果a和b对应的位置上都是0,那么结果对应的位置上也是0。

(2), 如果a对应的位置是1,b对应的位置是0,结果对应的位置是1。

(3), 如果a对应的位置是0,b对应的位置是1,结果对应的位置是1。(向前借一位1)

所以在不考虑借位的情况下,对应位置上的结果其实就是a|b(对应位置都为1的在第一步就已经被踢出了),那么实际计算的时候我们不可能不考虑借位的问题,所以实际结果是(a|b)-(b<<1),但这里又出现了“-”符号,所以不符合要求,这时我们可以使用递归的方式来解决,代码如下

private static int subtraction2(int a, int b) {
    if (b == 0)
        return a;
    int c = a & b;
    //下面两行是把a和b中相同位置为1的都消去
    a ^= c;
    b ^= c;
    return subtraction2(a | b, b << 1);
}

当然我们还可以把它改为非递归的方式,像下面这样

private static int subtraction3(int a, int b) {
     while (b != 0) {
         int c = a & b;
         a ^= c;
         b ^= c;
         a |= b;
         b <<= 1;
     }
     return a;
}

我们还是找几组数据来测试一下吧

int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, Integer.MAX_VALUE, Integer.MAX_VALUE, -8, -9, 100, Integer.MAX_VALUE};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "-" + array[(i << 1) + 1] + "=" + subtraction2(array[i << 1], array[(i << 1) + 1]));
    }

上面的我们可以不用看,直接看运行结果就行了

java不使用add直接给list集合赋值 java不用加号算结果_i++_04


我们以a=13,b=10来画个图加深一下理解

java不使用add直接给list集合赋值 java不用加号算结果_i++_05


java不使用add直接给list集合赋值 java不用加号算结果_i++_06

三:不使用“×”实现两个数相乘

我们先来看个例子,比如13*9,计算方式如下

java不使用add直接给list集合赋值 java不用加号算结果_System_07


由上面公式我们可以看出只有b的某一位是1的时候和a相乘才有意义,如果b的某一位是0,那么和a相乘则永远都是0,所以我们计算的时候逐步遍历b的每一位,只有当他为1的时候才进行运算,我们来看下代码

//求一个数的相反数
 private static int negative(int a) {
     return add3(~a, 1);
 }
 
 private static int mult(int a, int b) {
     int x = a < 0 ? negative(a) : a;//如果是负数,先转为正数再参与计算
     int y = b < 0 ? negative(b) : b;
     int res = 0;
    while (y != 0) {
        if ((y & 1) == 1)
            res = add3(res, x);
        x <<= 1;
        y >>= 1;
    }
    return (a ^ b) >= 0 ? res : negative(res);
}

我们还是来找一组数据测试一下,验证我们代码的正确性

int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, -8, -9, 100, 99};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "×" + array[(i << 1) + 1] + "=" + mult(array[i << 1], array[(i << 1) + 1]));
    }

上面代码不用看,我们直接来看一下打印的数据,结果丝毫不差

java不使用add直接给list集合赋值 java不用加号算结果_d3_08


四:不使用“÷”实现两个数相除

a÷b的含义是a中包含多少个b,比如6÷3=2,7÷3=2,这里我们实现的除法和计算机中两个int类型相除结果是一样的,只记录商的值,余数会被舍去,所以我们想到的一种解法是用a不断的减去b,并记录减了多少次,所以代码很容易想到,我们来看下

private static int div1(int a, int b) {
    int x = a < 0 ? negative(a) : a;
    int y = b < 0 ? negative(b) : b;
    if (x < y)
        return 0;
    return (a ^ b) >= 0 ? div1(subtraction(a, b), b) + 1 : div1(add3(a, b), b) - 1;
}

上面的+1,-1直接改为上面的加法和减法即可,这里我为了方便阅读代码就没写。这种递归的实现效率不是很高,如果a非常大,b又比较小,很容易出现堆栈溢出异常,所以我们还可以把它改为非递归

private static int div2(int a, int b) {
     int x = a < 0 ? negative(a) : a;
     int y = b < 0 ? negative(b) : b;
     int ocunt = 0;
     while (x >= y) {
         x = subtraction3(x, y);
         ocunt++;
     }
     return (a ^ b) >= 0 ? ocunt : -ocunt;
}

这种虽然不会出现堆栈溢出异常了,但如果b是1,a是一个非常非常大的数,这样一直减下去也是非常慢的,我们还可以换种思路,每次减去的不是b,而是b的倍数,我们来看下代码

private static int div3(int a, int b) {
     if (a == 0 || b == 0)
         return 0;//b不能为0,如果b是0我们应该抛异常的,这里简单处理就没抛
     int x = a < 0 ? negative(a) : a;
     int y = b < 0 ? negative(b) : b;
     int result = 0;
     for (int i = 31; i >= 0; i--) {
         if ((x >> i) >= y) {
             result = add3(result, 1 << i);
            x = subtraction3(x, y << i);
        }
    }
    return (a ^ b) >= 0 ? result : -result;
}

我们找一组非常极端的数据来测一下上面两种方法,看一下效率到底相差多少倍

int a = Integer.MAX_VALUE;
    int b = 1;
    long time = System.nanoTime();
    System.out.println(a + "÷" + b + "=" + div2(a, b));
    System.out.println("优化之前的时间:" + (System.nanoTime() - time));
    time = System.nanoTime();
    System.out.println(a + "÷" + b + "=" + div3(a, b));
    System.out.println("优化之后的时间:" + (System.nanoTime() - time));

我们来看一下结果

java不使用add直接给list集合赋值 java不用加号算结果_System_09


这个时间相差还是非常大的,一个30多亿纳秒,一个两万多纳秒,相差十几万倍。最后我们再找一组数据测试一下我们的代码是否正确

int[] array = {1, 1, 0, 1, 13, 9, 40, 3, 1, -1, -8, -9, 100, 99};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println("div2方法测试:" + array[i << 1] + "÷" + array[(i << 1) + 1] + "=" + div2(array[i << 1], array[(i << 1) + 1]));
    }
    System.out.println("----------------------------------------");
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println("div3方法测试:" + array[i << 1] + "÷" + array[(i << 1) + 1] + "=" + div3(array[i << 1], array[(i << 1) + 1]));
    }

我们来看下运行结果

java不使用add直接给list集合赋值 java不用加号算结果_i++_10


结果和我们预想的完全一致。