Loading

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

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

作者:Grey

原文地址:

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

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

说明#

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

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

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

实现加法#

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

13 = 23 + 22 + 20,即:01101

20 = 24 + 22,即:10100

两个数异或结果如下

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

结果就是: 20 + 23 + 24 = 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=bc

用二进制来说明,如果:

a=b27+b24+b21

则 c 的二进制一定是10010010

同理,如果:

a=b23+b20

则 c 的二进制一定是1001

抽象一下,如果a=b2x+b2y+b2z,则 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(bc)=d

然后d/b=e

最后c+e=(((bc)/b)+((a(bc))/b))=(bc+a(bc))/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;
    }

}

更多#

算法和数据结构学习笔记

算法和数据结构学习代码

参考资料#

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

作者:GreyZeng

出处:https://www.cnblogs.com/greyzeng/p/16637476.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

你可以在这里自定义其他内容

posted @   Grey Zeng  阅读(1141)  评论(2编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu