前言
刷刷题觉得自己行了,打开剑指Offer的一道简单题,一下又给我整不会了:
给定两个整数 a
和 b
,求它们的除法的商 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:
- 首先把每一位的数字相加,若超过9,则把个位数写在下面,进位的1标到更高一位;
不考虑进位得到结果12,考虑进位得到结果110; - 把进位数与刚才写的个位结果相加,即得到两数和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:
- 不考虑进位得到结果10,考虑进位得到1010,结果相加:
- 不考虑进位得到结果1000,考虑进位得到10,结果相加:
- 不考虑进位得到结果1100,考虑进位得到0,结果相加,哦也没必要相加了。
为了方便对比再加个十进制的例子:
我们有这么几个关键数据:
两个待相加的数;
每次不考虑进位相加得到的结果,记为sumtmp
;
每次考虑进位相加得到的进位结果,记为carry
,英文意为进位;
最后变成无需进位的加法时标志都是carry
变为0;
那么算法很清晰了:
- 两数按位异或得到
sumtmp
; - 两数按位与并向左移1位得到
carry
; - 再继续把
sumtmp
和carry
相加,直到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