Loading

使用位运算技巧实现加减乘除

使用位运算技巧实现加减乘除

作者:Grey

原文地址:

博客园:使用位运算技巧实现加减乘除

CSDN:使用位运算技巧实现加减乘除

说明

题目描述见:LeetCode 29. Divide Two Integers

原题目是:要求不使用乘法、除法和取模运算符实现除法。

我们把题目要求提高一点,不用加减乘除和取模运算符号,只使用位运算,不仅实现除法,也实现加减乘法。

实现加法

异或(^)运算就是两个数对应二进制值的无进位相加,比如a = 13b = 20a ^ b的结果如下(用二进制表示)

13 = \(2^3\) + \(2^2\) + \(2^0\),即:01101

20 = \(2^4\) + \(2^2\),即:10100

两个数异或结果如下

  01101
^ 10100
--------
  11001

结果就是: \(2^0\) + \(2^3\) + \(2^4\) = 25

思路可以转换一下,把加法用异或替换,得到两个数二进制无进位信息相加的结果。然后把这个结果加上进位信息,就是两个数相加的最终结果。

如上例,a ^ b = 25, ab相加的进位信息是01000(十进制就是 8)。25 + 8 = 33,正好是a + b的结果。

抽象一下:

要计算a + b

先算a ^ b = a'

然后得到 a 和 b 相加的进位信息 b'

a + b = a' + b'。由于不能用加号,所以,我们只能逐个把进位信息叠加。

那么问题就变成:何时会产生进位信息?

a 和 b 的二进制对应位置上都是 1,则会产生进位,即:每次处理的进位信息为:(a & b) << 1

实现代码和详细注释信息如下

// 原始加法就是:无进位信息(异或) 结合(+) 进位信息
public int add(int a, int b) {
    int sum = a;
    while (b != 0) {
        // 第一次进入这个循环,得到的是原始 a 和 原始 b 的异或结果,即无进位信息相加的结果
        // 除了第一次,后面都是把a 和 b 相加的进位信息累加到 sum 中
        sum = a ^ b;
        // a & b -> 只有 a 和 b 对应的位置都是 1 的情况下,才会是1,其他情况都是0
        // 而 a 和 b 对应位置都是 1 的情况下,也正好是进位信息会产生的地方
        // << 1 表示把进位信息进位到高位进行累加
        // 如果得到的结果不为 0 说明肯定有进位信息
        b = (a & b) << 1;
        a = sum;
    }
    return sum;
}

实现减法

a - b = a + (-b)

由于不能出现减号,所以,可以用加法来模拟一个数的相反数,因为

x的相反数等于~x + 1,即add(~x,1)

所以,减法实现如下

// 实现减法
public int minus(int a, int b) {
    return add(a, negNum(b));
}
// 某个数n的相反数就是 ~n + 1,由于不能用+号
// 所以是 add(~n,1)
public int negNum(int n) {
    return add(~n, 1);
}

实现乘法

小学算术计算两个数的乘法用的是如下方法,

比如 a = 12b = 22a * b通过如下方式计算:

  19 <--- a
x 22 <--- b
------
  38
 38
------
 418

同样方法也适用于二进制,19 的二进制是 10011,22 的二进制是 10110 ,

     10011 <--- a
x    10110 <--- b
-------------
     00000
    10011
   10011
  00000
 10011
------------
 110100010

110100010 就是 418。

其本质就是:

b 的二进制值(10110)从右往左开始,如果 b 的某一位是 1 ,则把 a 左移一位的值加到结果中,模拟 1 * a,如果 b 的某一位是 0,则 a 左移一位的值不加入结果中。 最后累加的结果就是a * b的答案。

位运算实现乘法的完整代码和注释信息如下

public int multi(int a, int b) {
    int res = 0;
    while (b != 0) {
        // b 的 二进制从右往左开始
        if ((b & 1) != 0) {
            // b 的某位是 1,则把 a 右移动一位的值加入进来
            res = add(res, a);
        }
        a <<= 1;
        // 带符号右移
        b >>>= 1;
    }
    return res;
}

实现除法

实现除法的时候,为了防止溢出,我们首先把所有数先转换成正数来算。最后在判断两个数的符号决定是否把结果取其相反数。

假设 \(a / b = c\),则 \(a = b * c\)

用二进制来说明,如果:

\(a = b * 2^7 + b * 2^4 + b * 2^1\)

则 c 的二进制一定是\(10010010\)

同理,如果:

\(a = b * 2^3 + b * 2^0\)

则 c 的二进制一定是\(1001\)

抽象一下,如果\(a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z\),则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。

所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,

使用位运算实现除法的核心代码如下:

public boolean isNeg(int n) {
    return n < 0;
}

// 实现除法
// 假设 $a / b = c$,则 $a = b * c$,
//用二进制来说明,如果:
//$a = b * 2^7 + b * 2^4 + b * 2^1$,
//则 c 的二进制一定是$10010010$。
//同理,如果:
//$a = b * 2^3 + b * 2^0$,
//则 c 的二进制一定是$1001$。
//抽象一下,如果$a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z$,则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。
//所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,
public int div(int x, int y) {
    // 把复数全部转换为正数来算
    int a = isNeg(x) ? negNum(x) : x;
    int b = isNeg(y) ? negNum(y) : y;
    int res = 0;
    // 以下就是:for (int i = 31; i > -1; i--)
    for (int i = 31; i > negNum(1); i = minus(i, 1)) {
        if ((a >> i) >= b) {
            res |= (1 << i);
            a = minus(a, b << i);
        }
    }
    return isNeg(x) ^ isNeg(y) ? negNum(res) : res;
}

其中

for (int i = 31; i > negNum(1); i = minus(i, 1))

就是

for (int i = 31; i > -1; i--)

循环体内的

if ((a >> i) >= b) {
    res |= (1 << i);
    a = minus(a, b << i);
}

就是让 a 不断尝试其值是否由【b * 2的某个次方】相加得到。

由于有一些特殊情况,比如在 Java 中,int 类型的系统最小值Integer.MIN_VALUE的相反数依然是Integer.MIN_VALUE

如果a = Integer.MIN_VALUEb != -1 && b != Integer.MIN_VALUE,则在调用div(a,b)的时候,应该考虑到 a 取相反数还是其自身,所以需要特殊处理以下,即

\(a / b\)应该通过如下方式来计算,先让\(a + 1\),这个操作目的就是调用div的时候可以正常取相反数,

然后可以正常调用div方法,得到:\(c = (a + 1) / b\)的结果,

接下来就是想办法把\((a + 1) / b = c\)这个结论转换成题目要求的\(a/b\)的结果,

接着\(a - (b * c) = d\)

然后\(d / b = e\)

最后\(c + e = (((b * c) / b) + ((a - (b * c)) / b)) = (b * c + a - (b*c))/b = a / b\)

即得到\(a / b\)的值。

根据 LeetCode 题目要求,有如下结论:

Integer.MIN_VALUE / (-1) == Integer.MAX_VALUE

所以除法的主流程代码如下(主要是根据题目要求和系统最小值的特殊情况进行了一些边界讨论,见注释说明内容)

public int divide(int a, int b) {
    if (b == Integer.MIN_VALUE) {
        return a == Integer.MIN_VALUE ? 1 : 0;
    }
    // 除数不是系统最小
    if (a == Integer.MIN_VALUE) {
        if (b == negNum(1)) {
            // leetcode的题目要求
            return Integer.MAX_VALUE;
        }
        // 求 a / b
        // 先算 (a + 1)/b = c
        // 然后算 a - (b*c) = d
        // 然后 d / b = e
        // c + e = (a+1)/b + (a-(b*c))/b = a / b
        int c = div(add(a, 1), b);
        return add(c, div(minus(a, multi(c, b)), b));
    }
    // dividend不是系统最小,divisor也不是系统最小
    return div(a, b);
}

完整代码见

class Solution {
    // 主方法
    public int divide(int a, int b) {
        if (b == Integer.MIN_VALUE) {
            return a == Integer.MIN_VALUE ? 1 : 0;
        }
        // 除数不是系统最小
        if (a == Integer.MIN_VALUE) {
            if (b == negNum(1)) {
                // leetcode的题目要求
                return Integer.MAX_VALUE;
            }
            // 求 a / b
            // 先算 (a + 1)/b = c
            // 然后算 a - (b*c) = d
            // 然后 d / b = e
            // c + e = (a+1)/b + (a-(b*c))/b = a / b
            int c = div(add(a, 1), b);
            return add(c, div(minus(a, multi(c, b)), b));
        }
        // dividend不是系统最小,divisor也不是系统最小
        return div(a, b);
    }
    public int add(int a, int b) {
        int sum = a;
        while (b != 0) {
            // 第一次进入这个循环,得到的是原始 a 和 原始 b 的异或结果,即无进位信息相加的结果
            // 除了第一次,后面都是把a 和 b 相加的进位信息累加到 sum 中
            sum = a ^ b;
            // a & b -> 只有 a 和 b 对应的位置都是 1 的情况下,才会是1,其他情况都是0
            // 而 a 和 b 对应位置都是 1 的情况下,也正好是进位信息会产生的地方
            // << 1 表示把进位信息进位到高位进行累加
            // 如果得到的结果不为 0 说明肯定有进位信息
            b = (a & b) << 1;
            a = sum;
        }
        return sum;
    }

    // 某个数n的相反数就是 ~n + 1,由于不能用+号
    // 所以是 add(~n,1)
    public int negNum(int n) {
        return add(~n, 1);
    }

    public int minus(int a, int b) {
        return add(a, negNum(b));
    }

    // 参考小学算乘法的过程。
    // 比如 `a = 12`,`b = 22`,`a * b`通过如下方式计算:
    // **b 的二进制值(10110)从右往左开始,如果 b 的某一位是 1 ,
    // 则把 a 左移一位的值加到结果中,
    // 模拟 1 * a,如果 b 的某一位是 0,
    // 则 a 左移一位的值不加入结果中。** 最后累加的结果就是`a * b`的答案。
    public int multi(int a, int b) {
        int res = 0;
        while (b != 0) {
            // b 的 二进制从右往左开始
            if ((b & 1) != 0) {
                // b 的某位是 1,则把 a 右移动一位的值加入进来
                res = add(res, a);
            }
            a <<= 1;
            // 带符号右移
            b >>>= 1;
        }
        return res;
    }

    public boolean isNeg(int n) {
        return n < 0;
    }

    // 实现除法
    // 假设 $a / b = c$,则 $a = b * c$,
    //用二进制来说明,如果:
    //$a = b * 2^7 + b * 2^4 + b * 2^1$,
    //则 c 的二进制一定是$10010010$。
    //同理,如果:
    //$a = b * 2^3 + b * 2^0$,
    //则 c 的二进制一定是$1001$。
    //抽象一下,如果$a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z$,则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。
    //所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,
    public int div(int x, int y) {
        // 把复数全部转换为正数来算
        int a = isNeg(x) ? negNum(x) : x;
        int b = isNeg(y) ? negNum(y) : y;
        int res = 0;
        for (int i = 31; i > negNum(1); i = minus(i, 1)) {
            if ((a >> i) >= b) {
                res |= (1 << i);
                a = minus(a, b << i);
            }
        }
        return isNeg(x) ^ isNeg(y) ? negNum(res) : res;
    }

}

更多

算法和数据结构学习笔记

算法和数据结构学习代码

参考资料

算法和数据结构体系班-左程云

posted @ 2022-08-29 21:35  Grey Zeng  阅读(824)  评论(2编辑  收藏  举报