Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何通过二进制位运算实现加减乘除

如何通过二进制位运算实现加减乘除

作者头像
用户3147702
发布于 2022-06-27 06:08:11
发布于 2022-06-27 06:08:11
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

1. 引言

众所周知,计算机是通过 bit 位来存储数字的,因为每个 bit 位只能存储 0 或 1,因此,计算机底层的所有计算都是基于二进制来实现的。 那么,仅仅通过位运算,如何才能计算出数字的加减乘除呢?这是一个非常有意思的问题。 本文我们就来详细介绍一下。

2. 为什么要有原码和补码 — 计算机中数字的表示

对于一个非负数,用二进制来表示他是非常简单的,例如二进制的 0 就是十进制的 0,二进制的 1101 就是十进制的 13。 但是负数要如何表示呢?你一定会想,既然正数可以简单地表示,那么我们直接通过最高位上置 1 表示负数,0 表示正数就可以了。 也就是说,对于一个 byte 来说,0b 0000 0101 表示 5,0b 1000 0101 表示 -5,看上去确实是实现了负数的表示,但是这引入了一个新的问题,我们都知道 5 + (-5) 结果是 0,那么对于二进制加法 0b 0000 0101 + 0b 1000 0101 的结果也必须是 0,此时我们有两种办法来解决这个问题:

  1. 实现加法、减法两套算法,在加法运算前先判断两个加数最高位的取值,然后将他们转换为整数的加减法再最终决定结果的最高位取值
  2. 重新定义正数或负数的二进制表示方法,让 5 的二进制表示与 -5 的二进制表示只和刚好位 0

方法一的好处是负数的表示十分易于理解,但缺点是算法实现复杂度比方法二高很多。 方法二的好处是完全可以通过加法的算法来直接实现减法,只需要在进行加法前按照规则将减数转换为其相反数即可,但是缺点也很明显,就是负数的表示不再那么直观了。 综合考虑,由于二进制的表示仅仅是计算机在各种计算时使用的,在给用户呈现最终的结果时,用户无需了解其内在的二进制表现形式,因此方法二的缺点也就并不怎么明显了,所以最终计算机的实现都选择了方法二。 那么,我们如何定义一套新的负数表示方法呢?很简单,既然 0b 0000 0101 + (-5) == 0b 0000 0000,所以 -5 == 0b 0000 0000 - 0b 0000 0101 == 0b 1111 1111 + 1 - 0b 0000 0101 == 0b 1111 1011。 从 0b 1111 1111 + 1 - 0b 0000 0101 可以直接看出规律,因为 0xff - x 就是 x 的按位取反,整个过程就是正数的按位取反再加 1,这就是计算机中补码的计算方法。

因此取反操作为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func neg(num int) int {
    return add(int(^uint(num)), 1)
}

2.1. 特殊情况

需要注意的是,因为 int 型的位数是有限的,因此会出现溢出的情况,例如对于 8 位 int 型来说 0b 1000 0000 表示 -128,将他按照计算补码的方式计算出其相反数即:0b 0111 1111 + 1 = 0b 1000 0000,仍然是 -128。

3. 加法的实现

有了上述的补码,我们计算加法时就非常轻松了,无需关注两个加数的符号问题,直接按位相加即可,那么我们如何来实现按位相加呢? 我们来看看对于两个只有 1 位的加数其和符合什么规律呢:

加数1

加数2

0

0

0

1

0

1

0

1

1

1

1

10

上表中只有两个加数均为 1 时才比较特殊,如果我们不考虑进位,你会发现,和的结果刚好是异或的结果,而进位位的值和两个加数相与的结果是一致的,这样只要递归计算进位位与不考虑进位位的和的结果就可以得到最终的求和结果了。 接下来,我们需要考虑这个递归过程的终止条件以及是否会出现无限递归的可能呢?很显然,如果两个加数的任何一个为 0,我们直接返回另一个加数即可,这就是递归的终止条件,而两个二进制位的和可能出现的进位位只有可能是 10 或 00,即使每次递归,进位均不为 0,下一次递归的进位会以低位补 0 向高位移动,最终溢出使进位位最终为 0,最终实现递归的终止,从而不会出现无限递归的可能。 有了这个规律,我们就可以编写代码了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func add(num1 int, num2 int) int {
    unum1, unum2 := uint(num1), uint(num2)
    if num1 == 0 || num2 == 0 {
        return int(unum1 | unum2)
    }
    return add(int(unum1^unum2), int((unum1&unum2)<<1))
}

4. 减法的实现

我们上面已经介绍了补码的表示与计算方法,根据补码的计算规则,一个数的相反数即按位取反再加 1。 对于减法而言,将减数转换为其相反数,再将他与被减数相加即可实现减法运算。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func sub(num1 int, num2 int) int {
    return add(num1, neg(num2))
}

5. 乘法的实现

乘法看上去非常简单,当两个乘数都是非负数时,用一个乘数循环加自己,加的次数是另一个乘数的值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func mul1(num1 int, num2 int) int {
    result := 0
    isneg := num1 > 0 && num2 < 0 || num1 < 0 && num2 > 0
    if num1 < 0 {
        num1 = neg(num1)
    }
    if num2 < 0 {
        num2 = neg(num2)
    }
    for i:=0; i<num2; i=add(i, 1) {
        result = add(result, num1)
    }
    if isneg {
        return neg(result)
    }
    return result
}

这个算法的时间复杂度是 O(num2),我们能不能在 O(1) 时间复杂度下计算出两个数的积呢?

5.1. 改进算法

我们知道,在我们计算十进制的乘法时,我们并不是通过反复增加被加数来实现的,而是通过列竖式的方法来实现的,那么二进制的乘法可以通过列竖式来解决吗?当然是可以的:

通过列竖式的方法,我们看到,通过每次将被加数左移一位,将加数右移一位,如果加数最低位为 1,那么就将移位后的被加数加到最终的结果中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func mul2(num1 int, num2 int) int {
    isneg := num1 > 0 && num2 < 0 || num1 < 0 && num2 > 0
    if num1 < 0 {
        num1 = neg(num1)
    }
    if num2 < 0 {
        num2 = neg(num2)
    }
    unum1, unum2 := uint(num1), uint(num2)
    result := 0
    for unum1 != 0 && unum2 != 0 {
        if unum2 & 1 != 0 {
            result = add(result, int(unum1))
        }
        unum2 >>= 1
        unum1 <<= 1
    }
    if isneg {
        return neg(result)
    }
    return result
}

因为 int 型的 bit 位数是一个常数,所以上述算法时间复杂度是 O(1)

6. 除法的实现

当被除数和除数都是非负数时,除法的原理是用被除数反复减去除数,一旦结果小于被除数,那么减的次数就是最终的商,而这个结果就是余数。 最终,商的符号取决于被除数和除数的符号,而余数的符号总数与除数的符号相同的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func div1(num1 int, num2 int) (int, int) {
    if num2 == 0 {
        return 0, 0
    }
    isneg, quotneg := num1 > 0 && num2 < 0 || num1 < 0 && num2 > 0, false
    if num1 < 0 {
        num1 = neg(num1)
    }
    if num2 < 0 {
        quotneg = true
        num2 = neg(num2)
    }

    result := 0
    for num1 >= num2 {
        result = add(result, 1)
        num1 = sub(num1, num2)
    }
    if isneg {
        result = neg(result)
    }
    if quotneg {
        num1 = neg(num1)
    }
    return result, num1
}

这个算法的时间复杂度是 O(num1) 的。

6.1. 改进算法

上述算法循环中,每次都用被除数减去除数,那么如果我们可以用被除数减去 n 倍的除数,商在原有基础上就可以直接加 n,算法时间复杂度就会大为缩减,那么,我们如何来确定 n 呢? n 作为一个自然数,同样可以表示成 2 进制,例如 0b 1011 0101 = 2^7 + 2^5 + 2^3 + 2^2 + 2^0,也就是说,任何一个自然数,都可以表示成若干个 2^m 之和,而 m 的取值均不相同。 因此,根据乘法结合律,我们可以尝试用被除数依次减去 2^i,i 从大到小递减,一旦结果大于 0,那么就将 2^i 累加到商中,被除数再减去这个结果,直到这个结果小于除数,他就是余数,这样,对于有限位数的 int 型来说,算法时间复杂度就是 O(1) 根据乘法的结合律 0b 1011 0101 2 = (2^7 + 2^5 + 2^3 + 2^2 + 2^0) 2 = 2^8 + 2^6 + 2^4 + 2^3 + 2^1 = 0b 0110 1010,也就是说,乘以 2 的操作相当于左移一位。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func div2(num1 int, num2 int) (int, int) {
    if num2 == 0 {
        return 0, 0
    }
    isneg, quotneg := num1 > 0 && num2 < 0 || num1 < 0 && num2 > 0, false
    if num1 < 0 {
        num1 = neg(num1)
    }
    if num2 < 0 {
        quotneg = true
        num2 = neg(num2)
    }

    result := 0
    unum1, unum2 := uint(num1), uint(num2)
    for i:=31; i>=0; i = sub(i, 1) {
        j := unum1 >> uint(i)
        if j >= unum2 {
            result += 1 << uint(i)
            unum1 = uint(sub(int(unum1), int(unum2<<uint(i))))
            if unum1 < unum2 {
                break
            }
        }
    }
    num1 = int(unum1)
    if isneg {
        result = neg(result)
    }
    if quotneg {
        num1 = neg(num1)
    }
    return result, num1
}

上面的代码中,没有通过将除数左移再比较来实现,而是通过被除数右移再比较来实现的,原因在于将除数左移可能造成除数溢出,也就是移动后反而小于移动前,造成算法的错误。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
江哥带你玩转C语言 | 09 - C语言进制和位运算
00011 0x001 0x7h4 10.98 0986 .089-109 +178 0b325 0b0010 0xffdc 96f 96.0f 96.oF -.003
极客江南
2021/07/11
1.5K0
反码补码和位运算
三者是计算机存储数据的不同形式,计算机用补码存储数据。而且计算机利用这三者可以用加法实现减法
晚上没宵夜
2020/04/24
6900
世界上有10种人,一种是懂二进制的人,一种是不懂二进制的人。
看到这个问题,我想到了之前的一个场景是要获取近30天的日期列表,我的思路是通过System.currentTimeMillis()获取当前时间戳,然后依次减去对应的毫秒数(24 * 60 * 60 * 1000),后来发现问题:30 * 24 * 60 * 60 * 1000因为超过了int的上限值而变为了一个负数。于是我回复他:
敲得码黛
2021/02/22
1.5K0
世界上有10种人,一种是懂二进制的人,一种是不懂二进制的人。
java用位运算实现加减乘除的过程_java四则运算
我们经常使用的加减乘除,我们所看到的只是表面的效果,那么加减乘除在底层究竟是怎么实现的?今天就让我们一探究竟.今天用位运算实现的加减乘除不使用任何的加减乘除符号.
全栈程序员站长
2022/11/15
8930
java用位运算实现加减乘除的过程_java四则运算
C语言关于进制转换,补码, 整数的位操作
一、进制转换  //关于进制转换,从网上找了几张经典图片,便于后面查询 1、二进制转十进制、八进制转十进制、十六进制转十进制 2、十进制转二进制, 十进制转八进制,十进制转十六进制 3、二进制转八进制
tandaxia
2018/09/27
5.2K0
C语言关于进制转换,补码, 整数的位操作
探索计算机内部的神秘语言:二进制的魅力
在之前的章节中,我们已经详细介绍了计算机硬件的组成部分,包括中央处理器(CPU)、内存、磁盘和总线等。因此,从今天开始,我们将深入探讨计算机内部的工作原理。首先,我们将从二进制这个简单而重要的概念开始讲解,因为计算机底层只能使用二进制来表示和处理信息。
努力的小雨
2023/12/01
5302
图解二进制
更新日志 2022-9-15 子时 于 杭州 目录结构调整 配图补全 封面更改 说明:以下均指8位二进制数形式 在了解原码之前,先熟悉几个名词.。 机器数 数字在计算机中的二进制表现形式。分正负。 图解 真值 有符号数转二进制之后,其原来对应的值位真值,带符号的二进制转为其他进制之后的值称为形式值。 图解 注:红色的数字1是十进制-3转二进制之后的符号位 原码 符号位+真值的绝对值,即是带符号的二进制数 举例: 十进制 二进制原码 +1(正一) 0000 0001 -1
堆栈哲学
2022/11/24
1.3K0
图解二进制
大数加减乘除,一文彻底搞定
大家好,我是bigsai!最近,大数加减频频登上笔试的舞台,小伙伴们在群里也分享自己遇到面试官碰到大数运算的题目,想着这么重要而简单的知识点我还没写过,那得好好和大家一起总结一下。
bigsai
2021/04/12
5590
运筹学与最优化理论基础——高精度加减乘除(C++实现)
在写单纯形算法时,发现了高精度分数存在bug与不足,所以必须对相关函数进行修改。主要有bug的函数是string DIVIDE_INT(string str1,string str2,int flag),之前是为了运算简单起见,对于特殊除数与被除数进行特定的判断来减小计算复杂度,但是发现存在逻辑bug,判断这些条件之后,未直接返回结果使得程序仍然会执行正常的除法操作,因此对这个bug进行修正。同时为了方便之后的单纯型算法的编写,在此又特意添加两个函数int Compare2Zero()和int Compare2Fraction(Fraction fraction),分别来比肩与0和分数fraction的大小。 在写两阶段单纯形算法时,发现了高精度分数中缺少相关取反和取倒数等接口导致代码出现大量重复代码。因此再次对高精度分数类进行修改。主要实现了分数取反和分数取倒数,并将整体代码进行了优化。由于两个函数过于简单,因此不对这两个函数进行讲解。
AI那点小事
2020/04/20
1.3K0
运筹学与最优化理论基础——高精度加减乘除(C++实现)
使用位运算实现int32位 整数的加减乘除
我觉得异或操作和与操作完全就是实现加法的。 异或就是相同位相加最后留下的结果,而与就是相同位相加是否进1的结果。
ShenduCC
2019/08/26
1.4K0
每日一题 (不用加减乘除做加法,找到数组中消失的数字)
在二进制加法中,我们通常使用“逐位相加”的方法来模拟常规加法的过程。当两个数字进行加法运算时,从最低位(通常是右侧)开始相加,然后考虑进位。如果相加的结果产生进位,那么这个进位会被带到下一位的加法中。
用户11039545
2024/03/28
1650
每日一题 (不用加减乘除做加法,找到数组中消失的数字)
Python 笔记:二进制的补码
计算机只能识别0和1,使用的是二进制,而在日常生活中人们使用的是十进制,”正如亚里士多德早就指出的那样,今天十进制的广泛采用,只不过是我们绝大多数人生来具有10个手指头这个解剖学事实的结果。尽管在历史上手指计数(5,10进制)的实践要比二或三进制计数出现的晚。”.为了能方便的与二进制转换,就使用了十六进制(2 4)和八进制1.数值有正负之分,计算机就用一个数的最高位存放符号(0为正,1为负).这就是机器数的原码了。
用户8442333
2021/05/17
1.4K0
计算机组织结构(二) 定点运算
📚 文档目录 合集-数的二进制表示-定点运算-BCD 码-浮点数四则运算-内置存储器-Cache-外存-纠错-RAID-内存管理-总线-指令集: 特征- 指令集:寻址方式和指令格式 1. 移位运算 1.算数移位 符号位不变, 左移相当于乘以 2, 右移相当于除以 2(左侧全补符号位). 2. 逻辑移位 无符号数的移位, 右移时永远在高位填 0. 2. 加法运算 1. 全加器 𝑆_𝑖=𝑋_𝑖⊕𝑌_𝑖⊕𝐶_{𝑖−1} 𝐶_𝑖=𝑋_𝑖𝐶_{𝑖−1}+𝑌_𝑖𝐶_{𝑖−1}+𝑋_𝑖𝑌_𝑖 2. Serial Carr
Rikka
2022/01/11
5960
计算机组织结构(二) 定点运算
(加强版)大数加减乘除,一文彻底搞定
大家好,我是bigsai!(上次发布的忘加原创并且今天的把内容扩充了一下)最近,大数加减频频登上笔试的舞台,小伙伴们在群里也分享自己遇到面试官碰到大数运算的题目,想着这么重要而简单的知识点我还没写过,那得好好和大家一起总结一下。
bigsai
2021/04/26
2.1K0
LeetCode29 Medium 除法与二进制优化
Given two integers dividend and divisor, divide two integers without using multiplication, division and mod operator.
TechFlow-承志
2020/03/05
6730
不用加减乘除做加法
题意 写一个函数,求两个整数之和,要求在函数体内不得使用 +、-、*、/ 四则运算符号。 样例 对于 num1 = 15, num2 = 17,返回 32。 思路 位运算 首先先来看下十进制是如何计算的: 相加各位的值,不进位,结果是 22, (5 + 7 = 12,舍弃进位就是2, 1 + 1 = 2 没有进位就是 22) 计算进位值,得到 10。 然后将上述两步得到的值重复步骤 1 和 2 。直到进位置为 0,返回不进位的值即可。 那么对于二进制也可以用这种方式计算: 相加各位的值,不进位,15 (11
一份执着✘
2018/06/04
9290
补码运算加减乘除原理是什么_计算机组成原理补码乘法运算
大家好,又见面了,我是你们的朋友全栈君。 首先我们来看为什么要使用补码运算法: 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域的加减. (真值的概念在本文最开头). 但是对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单. 计算机辨别”符号位”显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法. 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了. 于是人们开始探索 将符号位参与运算, 并且只保留加法的方法. 首先来看原码: 计算十进制的表达式: 1-1=0
全栈程序员站长
2022/11/04
6190
二进制加,减法,23个位运算技巧[通俗易懂]
二进制最高位为1时表示负数,为0时表示正数。 **原码:**一个正数,转换为二进制位就是这个正数的原码。负数的绝对值转换成二进制位然后在高位补1就是这个负数的原码。 举例说明:       int类型的 3 的原码是 11B(B表示二进制位), 在32位机器上占四个字节,那么高位补零就得:       00000000 00000000 00000000 00000011       int类型的 -3 的绝对值的二进制位就是上面的 11B 展开后高位补零就得:       10000000 00000000 00000000 00000011 **反码:**正数的反码就是原码,负数的反码等于原码除符号位以外所有的位取反。 举例说明:       int类型的 3 的反码是       00000000 00000000 00000000 00000011       和原码一样没什么可说的       int类型的 -3 的反码是       11111111 11111111 11111111 11111100       除开符号位 所有位 取反 **补码:**正数的补码与原码相同,负数的补码为 其原码除符号位外所有位取反(得到反码了),然后最低位加1. 还是举例说明:       int类型的 3 的补码是:       00000000 00000000 00000000 00000011       int类型的 -3 的补码是       11111111 11111111 1111111 11111101       就是其反码加1
全栈程序员站长
2022/09/13
2.2K0
二进制加,减法,23个位运算技巧[通俗易懂]
关于二进制表示和补码计算的来龙去脉,入门看了秒懂
计算机最喜欢的数字就是 0 和 1,在 CPU 的世界中,它只认识这两个数字,即使是强大的操作系统,也都是由 0 和 1 组成的。
IOT物联网小镇
2021/05/13
8720
关于二进制表示和补码计算的来龙去脉,入门看了秒懂
进制介绍与转换
计算机是电子电荷集合的方式在内存中宝保存指令和数据,二进制数用两个数字作基础,其中每一个二进制数成为bit不是0就是1.位自右向左,从0开始顺序增加,左边的位称为最高有效位(Most Significant Bit MSB),右边的称为最低有效位(LSB least significant Bit).一个16位的二进制数 其MSB和LSB如下所示:
小柒吃地瓜
2020/04/23
1.7K0
相关推荐
江哥带你玩转C语言 | 09 - C语言进制和位运算
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验