[详细思路]如何用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


版权声明:本文为tunmang5421原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。