只使用位运算实现加减乘除

news2024/11/18 1:32:57

在线OJ: LeetCode 29. 两数相除

原题目的要求是不能使用乘法, 除法和取余运算符实现除法.

在本篇博客中把题目要求提高一点, 这里只使用位运算来实现, 顺便的也就把只使用位运算实现加减乘除实现了.

img

1 . 实现加法

首先我们需要知道两数之和可以是两个数位相加和不进位相加之和, 而两数进行异或(^)运算其实就是两个数对应二进制值的无进位相加.

我们可以把思路可以转换一下, 把加法用异或替换, 如果我们能够得到两个数相加的二进制进位信息为0, 那么此时的无进位结果就是两数相加的最终结果了.

只有二进制无进位信息, 相加的结果; 然后把这个结果加上进位信息, 就是两个数相加的最终结果.

抽象一下:

我们要计算 a + b

先算 a ^ b = a' 得到的结果就是无进位相加的结果, 然后我们再得到 a 和 b 相加的进位信息 b’, 那么此时 a + b = a' + b' 就是两数之和, 但我们这里是不能用加号, 所以我们需要逐个把进位信息再继续叠加 (即让a’ 和 b’ 再继续运算, 得到进位信息和不进位信息…), 直到进位信息为 0 结束.

那么进位信息如何计算?

只有当 a 和 b 的二进制对应位置上都是1, 才会产生进位, 只需要 通过 (a & b) << 1 就可得到进位结果.

所以, 只使用位运算实现加法的代码如下:

// 不使用算数运算实现两数相加
public static int add (int a, int b) {
    // 两个数的和等于两个数不进位相加和进位相加的和
    // a ^ b 得到的就是两数不进位相加的和
    // (a & b) << 1 得到的就是两数只相加进位的值

    // 一直循环至进位值为0计算结束
    int sum = a;
    while (b != 0) {
        sum = a ^ b;
        b = (a & b) << 1;
        a = sum;
    }
    return sum;
}

2 . 实现减法

两数相减, 等价于一个数加上另一个数的相反数, 即 a - b = a + (-b), 但这里是不能不能出现减号的, 所以, 可以用加法来模拟一个数的相反数, 因为 x 的相反数等于 x 取反再加 1 (~x + 1), 即 add(~x, 1).

所以, 只使用位运算实现减法可以在加法代码的基础上进行:

// 计算一个数字的相反数
public static int oppNum (int num) {
    return add(~num, 1);
}

// 不使用算数运算实现两数相减
public static int minus(int a, int b) {
    // a - b 就相当于 a + (-b)
    // 一个数的相反数等于这个数取反再加1
    return add(a, oppNum(b));
}

3 . 实现乘法

看下面计算两个十进制数的乘法用的方法是不是很熟悉, 以 a = 12, b = 22 为例, a * b 通过如下方式计算;

  19
x 22
------
  38
 38
------
 418

这样的方法也适用于二进制, 19 的二进制是 10011, 22 的二进制是 10110, 计算过程如下;

     10011
x    10110
-------------
     00000
    10011
   10011
  00000
 10011
------------
 110100010

得到的计算结果 110100010 就是 418.

其实这里的二进制计算就对应着这样一个过程, 要求 a * b 的结果, 就从右往左开始遍历 b 的每一位二进制值, 如果 b 的第 i 位是 1, 则把 a 左移 i 位的累值加到结果中; 如果 b 的第 i 位是 0, 不需要处理, 最后累加完的结果就是 a * b 的答案.

只使用位运算实现乘法的代码如下:

// 不使用算数运算实现两数相乘
public static int mulit (int a, int b) {
    // 就小学手算十进制类似
    // 让 a 的每一位去乘 b 的每一位
    int res = 0;
    while (b != 0) {
        if ((b & 1) != 0) {
            res = add(res, a);
        }
        a <<= 1;
        // 要注意 b 必须是无符号右移
        b >>>= 1;
    }
    return res;
}

4 . 实现除法

在实现除法的时候, 为了防止溢出, 我们可以先把两数转换成正数来进行计算; 最后在计算末尾再判断两个数的符号决定是否把由正数计算出来的结果取其相反数.

假设 a / b = c, 则 a = b * c, 使用位运算就需要结合二进制来思考, 这里假设:

a = b * (2^7) + b * (2^4) + b * (2^1), 则 c 的二进制一定是10010010.

抽象一下, 如果a = b * (2 ^ m1) + b * (2 ^ m2) + b * (2 ^ m3), 则 c 的 m1 位置, m2 位置, m3 位置一定是 1, 其他位置都是 0.

所以, 我们实现除法的思路可以转换成 a 是由几个 ( b * 2 的某次方) 的结果组成.

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

// 判断是不是负数
public static boolean isNegavit(int num) {
    return num < 0;
}
// 这个适用于a和b都不是系统最小值的情况下
public static int div(int a, int b) {
    // 这里的除法一定要保证两个数都是正数, 如果有负数在计算逻辑最后加上即可
    int x = isNegavit(a) ? oppNum(a) : a;
    int y = isNegavit(b) ? oppNum(b) : b;
    int res = 0;
    // 计算的是 x / y
    // 每次循环 y 向左移动到和 x 最接近的值, 但要满足 y <= x, 如果是这个条件下, 也就是让 y 左移, 可能会溢出到符号位, 就可能会出错
    // 实际上将 x 向右移动到和 y 最接近的值, 移动的位数和上面也是一样的, 不过要满足 x >= y;
    for (int i = 30; i >= 0; i = minus(i, 1)) {
        if ((x >> i) >= y) {
            // 这个比特位一定是1
            res |= (1 << i);
            // x 减去 y << i;
            x = minus(x, y << i);
        }
    }
    // isNegavit(a) != isNegavit(b) 这个也可以用 isNegavit(a) ^ isNegavit(b) 来实现效果
    return isNegavit(a) != isNegavit(b) ? oppNum(res) : res;
}

其中

if ((x >> i) >= y) {
    // 这个比特位一定是1
    res |= (1 << i);
    // x 减去 y << i;
    x = minus(x, y << i);
}

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

但只有上面的实现还不够, 这里是有一些特殊情况需要考虑的, 比如在 Java 中, int 类型的系统最小值Integer.MIN_VALUE取相反数的结果依然是Integer.MIN_VALUE.

所以, 除法的两个操作数如果有系统最小值的话需要单独的进行计算处理.

  1. 如果两操作数都是系统最小值, 结果就是 1;
  2. 如果只有除数 (b) 为系统最小值, 结果就是 0;
  3. 如果被除数 (a) 为系统最小值, 除数为 -1 时, 根据 LeetCode 题目要求, 结果就是Integer.MIN_VALUE / (-1) == Integer.MAX_VALUE;
  4. 如果被除数 (a) 为系统最小值, 除数为 -1Integer.MIN_VALUE时 (即a = Integer.MIN_VALUEb != -1 && b != Integer.MIN_VALUE), a / b应该通过如下方式来计算;
  • 可以先让先让系统最小值 +1 (a + 1), 此时就可以按照正常上面的正常流程去计算除法了, 即(a + 1) / b = c.
  • 接着计算a - (b * c) = d, 得到由于 a + 1 少/多算的.
  • 然后d / b = e
  • 最后将 ce 相加就得到了 a / b 的值, c + e = ((a + 1)/b) + ((a - (b * c)) / b) = a / b

除了这些特殊, 就是正常的流程了, 所以, 加上针对系统最小值的特殊判断的代码如下:

// 除法的计算如果有系统最小值需要进行特殊判断(因为Integer.MAX_VALUE取反再+1得到的还是原来值), 也就是没办法计算相反数
public static int divide(int a, int b) {
    if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
        // 如果两数都是系统最小值
        return 1;
    } else if (b == Integer.MIN_VALUE){
        // 如果除数为系统最小值
        return 0;
    } else if (a == Integer.MIN_VALUE) {
        // 被除数为系统最小值

        // 且除数为-1
        if (b == oppNum(1)) {
            // 答案应该是 Integer.MAX_VALUE + 1 的这个值的, 但力扣系统返回 Integer.MAX_VALUE 就行了
            return Integer.MAX_VALUE;
        } else {
            // 系统最小值没法取相反数计算
            // 1. c = (Integer.MAX_VALUE + 1) / b , 先让系统最小值 +1 后再除以 b
            // 2. (Integer.MAX_VALUE - c * b) / b
            // 3. 再将 1 和 2 的结果相加节课
            int c = div(add(a, 1), b);
            return add(c, div(minus(a, mulit(c, b)), b));
        }
    } else {
        // 两数都不是系统最小值
        return div(a, b);
    }
}

完整实现的代码如下:

class Solution {
    // 不使用算数运算实现两数相加
    public static int add (int a, int b) {
        // 两个数的和等于两个数不进位相加和进位相加的和
        // a ^ b 得到的就是两数不进位相加的和
        // (a & b) << 1 得到的就是两数只相加进位的值

        // 一直循环至进位值为0计算结束
        int sum = a;
        while (b != 0) {
            sum = a ^ b;
            b = (a & b) << 1;
            a = sum;
        }
        return sum;
    }

    // 计算一个数字的相反数
    public static int oppNum (int num) {
        return add(~num, 1);
    }


    // 不使用算数运算实现两数相减
    public static int minus(int a, int b) {
        // a - b 就相当于 a + (-b)
        // 一个数的相反数等于这个数取反再加1
        return add(a, oppNum(b));
    }

    // 不使用算数运算实现两数相乘
    public static int mulit (int a, int b) {
        // 就和小学手算十进制类似
        // 让 a 的每一位去乘 b 的每一位
        int res = 0;
        while (b != 0) {
            if ((b & 1) != 0) {
                res = add(res, a);
            }
            a <<= 1;
            // 要注意 b 必须是无符号右移
            b >>>= 1;
        }
        return res;
    }

    // 不使用算数运算实现除法

    // 判断是不是负数
    public static boolean isNegavit(int num) {
        return num < 0;
    }
    // 这个适用于a和b都不是系统最小值的情况下
    public static int div(int a, int b) {
        // 这里的除法一定要保证两个数都是正数, 如果有负数在计算逻辑最后加上即可
        int x = isNegavit(a) ? oppNum(a) : a;
        int y = isNegavit(b) ? oppNum(b) : b;
        int res = 0;
        // 计算的是 x / y
        // 每次循环 y 向左移动到和 x 最接近的值, 但要满足 y <= x, 如果是这个条件下, 也就是让 y 左移, 可能会溢出到符号位, 就可能会出错
        // 实际上将 x 向右移动到和 y 最接近的值, 移动的位数和上面也是一样的, 不过要满足 x >= y;
        for (int i = 30; i >= 0; i = minus(i, 1)) {
            if ((x >> i) >= y) {
                // 这个比特位一定是1
                res |= (1 << i);
                // x 减去 y << i;
                x = minus(x, y << i);
            }
        }
        // isNegavit(a) != isNegavit(b) 这个也可以用 isNegavit(a) ^ isNegavit(b) 来实现效果
        return isNegavit(a) != isNegavit(b) ? oppNum(res) : res;
    }
    // 除法的计算如果有系统最小值需要进行特殊判断(因为Integer.MAX_VALUE取反再+1得到的还是原来值), 也就是没办法计算相反数
    public static int divide(int a, int b) {
        if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
            // 如果两数都是系统最小值
            return 1;
        } else if (b == Integer.MIN_VALUE){
            // 如果除数为系统最小值
            return 0;
        } else if (a == Integer.MIN_VALUE) {
            // 被除数为系统最小值

            // 且除数为-1
            if (b == oppNum(1)) {
                // 答案应该是 Integer.MAX_VALUE + 1 的这个值的, 但力扣系统返回 Integer.MAX_VALUE 就行了
                return Integer.MAX_VALUE;
            } else {
                // 系统最小值没法取相反数计算
                // 1. c = (Integer.MAX_VALUE + 1) / b , 先让系统最小值 +1 后再除以 b
                // 2. (Integer.MAX_VALUE - c * b) / b
                // 3. 再将 1 和 2 的结果相加节课
                int c = div(add(a, 1), b);
                return add(c, div(minus(a, mulit(c, b)), b));
            }
        } else {
            // 两数都不是系统最小值
            return div(a, b);
        }
    }
}

当然, 按照原本的题意, 只是不能使用乘法, 除法和取余运算, 其他可以正常使用, 代码就简单了不少, 思路是一样的, 代码实现如下:

class Solution29 {
    public static boolean isNegavit(int num) {
        return num < 0;
    }
    public static int oppNum (int num) {
        return (~num) + 1;
    }

    public static int mulit (int a, int b) {
        // 就和小学手算十进制类似
        // 让 a 的每一位去乘 b 的每一位
        int res = 0;
        while (b != 0) {
            if ((b & 1) != 0) {
                res += a;
            }
            a <<= 1;
            // 要注意 b 必须是无符号右移
            b >>>= 1;
        }
        return res;
    }
    public static int div(int a, int b) {
        // 这里的除法一定要保证两个数都是正数, 如果有负数在计算逻辑最后加上即可
        int x = isNegavit(a) ? oppNum(a) : a;
        int y = isNegavit(b) ? oppNum(b) : b;
        int res = 0;
        // 计算的是 x / y
        // 每次循环 y 向左移动到和 x 最接近的值, 但要满足 y <= x, 如果是这个条件下, 也就是让 y 左移, 可能会溢出到符号位, 就可能会出错
        // 实际上将 x 向右移动到和 y 最接近的值, 移动的位数和上面也是一样的, 不过要满足 x >= y;
        for (int i = 30; i >= 0; i--) {
            if ((x >> i) >= y) {
                // 这个比特位一定是1
                res |= (1 << i);
                // x 减去 y << i;
                x -= (y << i);
            }
        }
        // isNegavit(a) != isNegavit(b) 这个也可以用 isNegavit(a) ^ isNegavit(b) 来实现效果
        return isNegavit(a) != isNegavit(b) ? oppNum(res) : res;
    }

    // 除法的计算如果有系统最小值需要进行特殊判断(因为Integer.MAX_VALUE取反再+1得到的还是原来值), 也就是没办法计算相反数
    public static int divide(int a, int b) {
        if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
            // 如果两数都是系统最小值
            return 1;
        } else if (b == Integer.MIN_VALUE) {
            // 如果除数为系统最小值
            return 0;
        } else if (a == Integer.MIN_VALUE) {
            // 被除数为系统最小值

            // 且除数为-1
            if (b == -1) {
                // 答案应该是 Integer.MAX_VALUE + 1 的这个值的, 但力扣系统返回 Integer.MAX_VALUE 就行了
                return Integer.MAX_VALUE;
            } else {
                // 系统最小值没法取相反数计算
                // 1. c = (Integer.MAX_VALUE + 1) / b , 先让系统最小值 +1 后再除以 b
                // 2. (Integer.MAX_VALUE - c * b) / b
                // 3. 再将 1 和 2 的结果相加节课
                int c = div(a + 1, b);
                return c + ((a - mulit(c, b)) / b);
            }
        } else {
            // 两数都不是系统最小值
            return div(a, b);
        }
    }
}

上述代码都是可以通过OJ的

img

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/492791.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++ 入门概述

c 入门概述 1. c 关键字2. c 命名空间3. c 输入与输出4. c 缺省参数5. c 函数重载6. c 引用6.1 引用概念6.2 引用特性6.3 常引用6.4 引用与指针区别 7. c 内联函数8. c auto 关键字9. 范围 for 循环 1. c 关键字 c 98中&#xff0c;规定的关键字总共有63个&#xff1a; 2. c…

排序算法 - 插入排序

文章目录 插入排序介绍插入排序实现插入排序的时间复杂度和稳定性插入排序时间复杂度插入排序稳定性 代码实现核心&总结 每日一道算法&#xff0c;提高脑力。第三天&#xff0c;插入排序。 插入排序介绍 插入排序(Insertion Sort)的基本思想是: 把n个待排序的元素看成为一…

全网最火,Web自动化测试驱动模型详全,一语点通超实用...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 自动化测试模型&a…

我记不住的那些Git的操作

背景&#xff1a;其实接触Git也很长时间了&#xff0c;自打上学那会就用Git作为版本控制工具&#xff0c;感触颇深。写这篇文章也是记录一下自己的理解留作日后词典进行查询&#xff0c;另外也是想把这些内容分享给大家。本篇将以问题为导向来阐述相关的知识&#xff0c;面对的…

mid360激光雷达跑Point-LIO算法

在商场里面上下楼穿梭,使用mid360激光雷达,完成建图 以下是建图的运行过程及参数配置 mid360激光雷达驱动 安装(ubuntu20.4 ) /ws_livox/src/livox_ros_driver2$source /opt/ros/noetic/setup.sh /ws_livox/src/livox_ros_driver2$./build.sh ROS1配置修改MID360_confi…

《花雕学AI》06:ChatGPT,一种新型的对话生成模型的机遇、挑战与评估

最近ChatGPT持续大火&#xff0c;大家们是不是在网上看到各种和ChatGPT有趣聊天的截图&#xff0c;奈何自己实力不够&#xff0c;被网络拒之门外&#xff0c;只能眼馋别人的东西。看别人在体验&#xff0c;看别人玩&#xff0c;肯定不如自己玩一把舒服的啊。 上一期&#xff0…

( “图“ 之 二分图 ) 785. 判断二分图 ——【Leetcode每日一题】

❓785. 判断二分图 难度&#xff1a;中等 存在一个 无向图 &#xff0c;图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph &#xff0c;其中 graph[u] 是一个节点数组&#xff0c;由节点 u 的邻接节点组成。形式上&#xff0c;…

ios 在windows chrome 联调

必要条件 1、iOS设备、数据线 2、Node.js 环境 3、Chrome 浏览器 4、电脑登录iTunes 5、手机 Safari 浏览器环境准备 1、安装Node环境参考Node安装的教程&#xff0c;确保终端输入node时可正常使用 2、安装 scoope 以及相关配置为了安装后续需要用的工具 remotedebug-ios-web…

c# 数据保存为PDF(三) (PdfSharp篇)

PdfSharp 使用&#xff0c;创建PDF文档 前言关于 PdfSharp下载 PdfSharp使用PdfSharp常用命名空间和类1 创建一个简单的PDF文档2 创建一个带页脚的PDF文档2.1 创建临时数据表2.2 创建页脚2.3 完整代码 小结附录&#xff1a; 往期文章&#xff1a; 1、 c# 数据保存为PDF&#x…

java并发编程之美第四章读书笔记

第四章java并发包中原子操作类原理剖析 JUC包提供了一系列的原子类操作,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子操作在性能上有很大提高 本章只讲解最简单的AtomicLong类的实现原理以及JDK8中新增的LongAdder和LongAccumulator类的原理 原子变量的操作类 At…

java并发编程之美第三章读书笔记

java并发包中ThreadLocalRandom类原理剖析 该ilei是JDK7在JUC包下面新增的随机数生成器吗,弥补了Random类在多线程下的缺陷 Random类及其局限性 public class RandomTest {public static void main(String[] args) {Random randomnew Random();for (int i0;i<10;i){Syste…

Facebook 用户量十分庞大,为什么还使用 MySQL 数据库?

Facebook是一个拥有超30亿用户的互联网公司&#xff0c;拥有海量数据&#xff0c;而且增长很快&#xff0c;单机数据库完全无法满足这种需求。 这时需要对数据进行分片&#xff0c;存储到多个数据库节点中&#xff0c;这个时候如果使用IOE作为其中一个节点&#xff0c;肯定可以…

MySQL Order by对各种排序算法的应用

通常我们实现的排序算法&#xff0c;都是在”纯内存“环境中进行。 MySQL 作为数据库难道是在先将所有要排序的数据加载到内存&#xff0c;再应用排序算法吗&#xff1f; 一、什么是内排序&#xff1f;什么是外排序&#xff1f; 内排序&#xff1a;全称为内部排序。内部排序是…

力扣题库刷题笔记647-回文子串

1、题目如下&#xff1a; 2、个人Python代码实现 思路如下&#xff1a; a、以切片的形式&#xff0c;判断每个子字符串是否为回文字符串。这里如何确定切片的起始下标就很重要了 b、首先需要知道的是字符串s&#xff0c;s[i,j]&#xff0c;指的是从下标i开始&#xff0c;到下标…

cout源码浅析

目录 cout源码浅析 那么对于没有定义在这之中的要怎么办呢&#xff1f; 实际使用 结语 首先来看我从cplusplus中截取的这张图&#xff1a; 注意最下面这一行字。cout其实是ostream的一个标准对象object。而上面则演示了一些继承关系。 好的&#xff0c;理解了之后&#xf…

算法DAY52 动态规划10 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组

300.最长递增子序列 五部曲&#xff1a; 1、dp数组的含义&#xff1a; dp[ i ] : 代表 截至到nums[i] (包括 nums[i]) 的序列中&#xff0c;以nums[i] 结尾的&#xff0c;最长递增子序列的长度。这里强调以nums[i] 结尾&#xff0c;是因为还要跟nums[j]做对比&#xff0c;确定…

ACG-crcme1(★★★)

运行程序 info exit 查壳 没壳 载入OD分析 刚载入OD发现要使用 ACG.key 搜一下字符串看看 发现这有貌似成功相关的字符串 进去看看 可以找到关键跳 爆破的话直接在这就可以完成 上面就该是算法了 算法分析 开始先判断文件存在和文件内容大小 读取文件内容&am…

微前端 qiankun@2.10.5 源码分析(二)

微前端 qiankun2.10.5 源码分析&#xff08;二&#xff09; 我们继续上一节的内容。 loadApp 方法 找到 src/loader.ts 文件的第 244 行&#xff1a; export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfi…

uniapp - 实现微信小程序电子签名板,横屏手写姓名签名专用写字画板(详细运行示例,一键复制开箱即用)

效果图 实现了在uniapp项目中,微信小程序平台流畅的写字签名板(也可以绘图)功能源码,复制粘贴,改改样式几分钟即可搞定! 支持自动横屏、持预览,真机运行测试非常流畅不卡顿。 基础模板 如下代码所示。 <template><view class=

vue3.2+vite+vant4+sass搭建笔记

1、确定node版本 1、下载nvm安装包 官方下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/releases 双击安装 2、在node官网下载安装多个node 3、切换node 2、创建项目 1、安装依赖 pnpm i 2、启动项目 npm run dev 3、配置指向src import { defineC…