[详细思路]如何用C++通过位运算实现int整数的加减乘除运算

前言

刷刷题觉得自己行了,打开剑指Offer的一道简单题,一下又给我整不会了:

给定两个整数 ab ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%'

整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31−1]。本题中,如果除法结果溢出,则返回 2^31 − 1

叛逆做法,反正leetcode不会真的检查符号的使用:

int divide(int a, int b) {
    if(a == -2147483648 && b == -1)
        return 2147483647;
    return a/b;
  }

投机做法,确实没在我代码里出现你的符号:

int divide(int a, int b) {
    if(a == -2147483648 && b == -1)
        return 2147483647;
    divides<int> n;
    return n(a,b);
}

提到位运算只能想到一些特殊的运算,比如乘2什么的,完全没有想过怎么用位运算实现int的加减乘除,不如一次性解决:

正文

相加

先来回忆一下娘胎里学的十进制竖式进位加法,我们看73+49:

在这里插入图片描述

  1. 首先把每一位的数字相加,若超过9,则把个位数写在下面,进位的1标到更高一位;
    不考虑进位得到结果12,考虑进位得到结果110;
  2. 把进位数与刚才写的个位结果相加,即得到两数和122,(如果两数相加还是需要进位的话可以再继续列竖式);

当然,更多时候我们直接省略了第二步,直接把高位加起来一起算,或是竖式还有其他的写法。但总体剖开,无非是:

  • 不考虑进位得到结果1;
  • 进位得到结果2;
  • 结果1与结果2相加。

接下来我们切换到二进制,并把计算权交给只会位运算的计算机:

先熟悉一下基本的位运算:按位与&、按位异或^、按位或|、按位取反~、向左\向右移位<<\>>

把十进制里面的运算对应上去,二进制的不考虑进位的位相加,异或运算正好满足:1 ^ 0 = 1, 0 ^ 0 = 0, 1 ^ 1 = 0

那进位的数字怎么得到呢?需要满足1、0或0、0不进位(0),仅1、1进位,与运算也恰好满足:1 & 0 = 0, 0 & 0 = 0, 1 & 1 = 1

还需要进向高位,左移1位即可<< 1

如此以来,所需要的运算也都满足了,那么我们以实例看一下二进制的竖式运算过程:

如101+111:

在这里插入图片描述

  1. 不考虑进位得到结果10,考虑进位得到1010,结果相加:
  2. 不考虑进位得到结果1000,考虑进位得到10,结果相加:
  3. 不考虑进位得到结果1100,考虑进位得到0,结果相加,哦也没必要相加了。

为了方便对比再加个十进制的例子:

在这里插入图片描述

我们有这么几个关键数据:

两个待相加的数;

每次不考虑进位相加得到的结果,记为sumtmp

每次考虑进位相加得到的进位结果,记为carry,英文意为进位;

最后变成无需进位的加法时标志都是carry变为0;

那么算法很清晰了:

  1. 两数按位异或得到sumtmp
  2. 两数按位与并向左移1位得到carry
  3. 再继续把sumtmpcarry相加,直到carry变为0时终止,此时的sumtmp即最终的和;

转化成代码:

// 迭代的写法
int Sum(int a, int b) {
    int sumtmp = a ^ b;
    int carry = (a & b) << 1;
    return carry == 0 ? sumtmp : Sum(sumtmp, carry);
}
// 循环的写法
int Sum(int a, int b) {
    int sumtmp = a ^ b;
    int carry = (a & b) << 1;
    while (carry) {
        int temp = sumtmp;
        sumtmp = temp ^ carry;
        carry = (temp & carry) << 1;
    }
    return sumtmp;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tunmang5421/article/details/123303460

负数的问题,计算机默认的补码已经解决了。就是还没有溢出检测。

那再加上溢出检测。

方法:只有两个符号相同的数相加才会溢出,溢出后相加结果会变为另一符号;

以4位为例,表示范围为 -8 ~ 7,0111 + 0001 = 1000(溢出!);1001 + 1010 = 0110(溢出!)

int Sum(int a, int b) {
    int sumtmp = a ^ b;
    int carry = (a & b) << 1;
    int res = (carry == 0 ? sumtmp : Sum(sumtmp, carry));
    if ((a ^ b) > 0 && (res ^ a) < 0) { // 
        cout << "Error: << a << " + " << b << is out of range!" << endl;
        return 0;
    }
    return res;
}

相减

相减,把一个数取相反数再相加即可,所以要考虑的只有怎么通过位运算取相反数。

找个例子拿0011(3)和1101(-3),可以看到按位取反再加1就是想要的结果,刚好加能用上了,唯一的溢出就是取反-2^31,那么:

int Negate(int a) {
    if (a == 0x80000000) {
        cout << "Error: Negate( " << a << " ) is out of range!" << endl;
        return 0;
    }
    return Sum(~a, 1);
}

int Sub(int a, int b) {
    return Sum(a, Negate(b));
}

乘法

乘法很容易想到暴力解题的思路,a*b就是b个a相加的和。先不考虑符号,两数取绝对值,然后循环累加b次a,得到结果。符号单独判断:由于相乘仅需知道符号位相反或相同->想到异或,两数异或,符号位相反则结果符号位为1(异或结果小于0),否则为0(异或结果大于0),符号相反的话把上述累加结果取反即可,刚好取反的函数也有了。

再考虑一下取绝对值:如果大于0返回自身,小于0则返回相反数,唯一溢出可能是-2^31。

先不考虑乘法的优化和溢出:

int Abs(int a) {
    if (a == 0x80000000) {
        cout << "Error: Abs ( " << a << " ) is out of range!" << endl;
        return 0;
    }
    return a < 0 ? Negate(a) : a;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tunmang5421/article/details/123303460
int Multi(int a, int b) {
    int multiplier = Abs(a), mulyiplicand = Abs(b);

    int res = 0;
    while (mulyiplicand) {
        res = Sum(res, multiplier);
        --mulyiplicand;
    }

    if ((a ^ b) < 0) // 若符号相反
        return Negate(res);

    return res;
}

再考虑一下溢出:

先列出特殊情况:

  • 两数正常,乘积越界;如2^20 * 2^20,又特殊如-2^20 * 2^11

    对正数讲,每次累加,结果都只会大于上次的累加值,一旦发生累加值小于上次,说明发生了溢出;负数则相反,每次累加值应该小于上次的结果;那么考虑同号异号的两种情况,用tmp存储上次累加值,negaflag用作判断的标志位

  • Abs不允许-231,但-231可以与1和0相乘(唯二的非溢出情况):

    上个情况可以解决,但特殊处理一下能保证不会进入Abs和Negate函数再次报错溢出;

  • ……

代码结合一下:

int Multi(int a, int b) {
    if (a == 0 || b == 0) return 0;
    if (a == 0x80000000 || b == 0x80000000) {
        if (b == 1)
            return a;
        if (a == 1)
            return b;
        else {
            cout << a << " * " << b << " is out of range!" << endl;
            return 0;
        }
    }

    int multiplier, mulyiplicand;
    int negaflag; // 标记符号相反,方便对比
    if ((a ^ b) < 0) { // 若符号相反
        multiplier = a < 0 ? a : b; // 负数作为累加值
        mulyiplicand = b > 0 ? b : a; // 正数还是用于循环
        negaflag = 1;
    } else { // 符号相同
        multiplier = Abs(a);
        mulyiplicand = Abs(b);
        negaflag = 0;
    }
 
    int res = 0, tmp = 0;
    while (mulyiplicand) {
        res = Sum(res, multiplier);
        if ((negaflag && tmp < res) || (!negaflag && tmp > res)) {
            cout << a << " * " << b << " is out of range!" << endl;
            return 0;
        }
        --mulyiplicand;
        tmp = res;
    }

    return res;
}

除法

除法又得从竖式看起了,不过这里我们把十进制跳过,直接看二进制:公式里面没有竖式的除符号,就拿根号代替吧。

在这里插入图片描述

在这里插入图片描述

捕捉到这么几个关键步骤:

  • 还是先取绝对值,把除数和被除数对齐(除数左移位),相减,如果除数移位后大于被除数,则当前处理的对齐位后移,否则:
  • 把商对应位置1,将相减得到的数作为被除数,继续除以除数;

对齐,判断除数和被除数的有效位,即最高位的1在哪,他们之间的差就是要除数左移对齐的位数;

清晰了写代码:

int GetBitLength(int a) {
    if (a < 0) {
        cout << "Error: GetBitLength is not for negative int!" << endl;
        exit(1);
    }
    int len = 0;
    while (a) {
        len++;
        a = a >> 1;
    }
    return len;
}

int Divide(int a, int b) {
    int dividend = Abs(a), divisor = Abs(b);

    int bitdiff = Sub(GetBitLength(dividend), GetBitLength(divisor));
    int res = 0; // 商
    while (bitdiff >= 0) { // bitdiff为商的当前操作位
        int tmp = divisor << bitdiff; // 除数左移位
        if (tmp > dividend) { // 移位后若大于被除数则操作位继续后移
            bitdiff = Sub(bitdiff, 1);
            continue;
        }
        res |= (1 << bitdiff); // 注:'|='为按位或

        dividend = Sub(dividend, tmp); // 差值作为新的被除数
        bitdiff = Sub(GetBitLength(dividend), GetBitLength(divisor)); 
    }

    if ((a ^ b) < 0) // 符号相反
        return Negate(res);

    return res;
}

考虑溢出,还是列特殊情况:

  • 除数是0;
  • Abs不允许-231,可以除以1;-231 / (-1) 也会溢出;
int Divide(int a, int b) {
    if (b == 0) {
        cout << "Error: divisor can not be 0!" << endl;
        return 0;
    }
    if (a == 0x80000000 && b != 1) {
        cout << "Error: Divide result is out of range!" << endl;
        return 0;
    }
    
    int dividend = Abs(a), divisor = Abs(b);

    int bitdiff = Sub(GetBitLength(dividend), GetBitLength(divisor));
    int res = 0; // 商
    while (bitdiff >= 0) { // bitdiff为商的当前操作位
        int tmp = divisor << bitdiff; // 除数左移位
        if (tmp > dividend) { // 移位后若大于被除数则操作位继续后移
            bitdiff = Sub(bitdiff, 1);
            continue;
        }
        res |= (1 << bitdiff); // 注:'|='为按位或

        dividend = Sub(dividend, tmp); // 差值作为新的被除数
        bitdiff = Sub(GetBitLength(dividend), GetBitLength(divisor)); 
    }

    if ((a ^ b) < 0) // 符号相反
        return Negate(res);

    return res;
}

取余的话返回最后的差值就好。

希望dalao们多提意见,有错误请评论指出。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tunmang5421/article/details/123303460

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
关于大整数乘除运算,可以使用以下C语言代码进行描述: ```c #include<stdio.h> #include<string.h> #include<stdlib.h> #define maxn 1000 struct bign{ //定义结构体存储大整数 int d[maxn], len; bign() //结构体初始化函数 {memset(d,0,sizeof(d)); len=0;} }; bign change(char str[]) //将字符串转为大整数 { bign res; res.len=strlen(str); for(int i=0;i<res.len;i++) res.d[i]=str[res.len-1-i]-'0'; return res; } int compare(bign a, bign b) //比较两个大整数的大小 { if(a.len>b.len) return 1; else if(a.len<b.len) return -1; else { for(int i=a.len-1;i>=0;i--) if(a.d[i]>b.d[i]) return 1; else if(a.d[i]<b.d[i]) return -1; return 0; } } bign add(bign a, bign b) //大整数法 { bign c; int carry=0; for(int i=0;i<a.len || i<b.len;i++) { int sum=a.d[i]+b.d[i]+carry; c.d[c.len++]=sum%10; carry=sum/10; } if(carry) c.d[c.len++]=carry; return c; } bign sub(bign a,bign b) //大整数法 { bign c; for(int i=0,carry=0;i<a.len;i++) { int tmp=a.d[i]-carry-b.d[i]; if(tmp>=0) carry=0; else { carry=1; tmp+=10; } c.d[c.len++]=tmp; } while(c.len>1 && c.d[c.len-1]==0) c.len--; return c; } bign mul(bign a, int b) //大整数与小整数的乘法 { bign c; int carry=0; for(int i=0;i<a.len;i++) { int tmp=a.d[i]*b+carry; c.d[c.len++]=tmp%10; carry=tmp/10; } while(carry) //必须要判最高的一位有没有进位,否则漏了的话就少了一位 { c.d[c.len++]=carry%10; carry/=10; } return c; } bign mul(bign a, bign b) //大整数乘法 { bign c; c.len=a.len+b.len; for(int i=0;i<a.len;i++) for(int j=0;j<b.len;j++) c.d[i+j]+=a.d[i]*b.d[j]; for(int i=0;i<c.len;i++) { c.d[i+1]+=c.d[i]/10; //进位处理 c.d[i]%=10; } while(c.len>1 && c.d[c.len-1]==0) c.len--; return c; } bign divide(bign a, int b, int &r) //大整数除以小整数 { bign c; c.len=a.len; for(int i=a.len-1;i>=0;i--) { r=r*10+a.d[i]; if(r<b) c.d[i]=0; else { c.d[i]=r/b; r=r%b; } } while(c.len>1 && c.d[c.len-1]==0) c.len--; return c; } char* BignToString(bign a) //大整数转为字符串 { char* res=new char[maxn]; for(int i=0;i<a.len;i++) res[i]=a.d[a.len-1-i]+'0'; res[a.len]='\0'; return res; } int main() { char str1[maxn], str2[maxn]; int a,b; scanf("%s%s%d",str1,str2,&b); bign A=change(str1),B=change(str2),C,D,E; C=add(A,B); D=sub(A,B); E=mul(A,b); int r=0; C=divide(A,b,r); printf("%s\n%s\n%s\n%s\n",BignToString(C),BignToString(D),BignToString(E),BignToString(C)); return 0; } ``` 在程序中,使用结构体bign存储大整数,通过定义法、法、乘法、除法等运算函数,实现了大整数乘除运算。同时,还可以将大整数转为字符串输出,方便进行结果的输出和格式化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WuPeng_uin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值