[剑指 Offer II] 整数-001. 整数除法
又开始刷题啦,先来点简单的吧
1. 题目
给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%' 。
注意:
- 整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
- 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31−1]。本题中,如果除法结果溢出,则返回 2^31 − 1
示例1:
输入:a = 15, b = 2
输出:7
解释:15/2 = truncate(7.5) = 7
示例2:
输入:a = 7, b = -3
输出:-2
解释:7/-3 = truncate(-2.33333..) = -2
示例3:
输入:a = 0, b = 1
输出:0
示例4:
输入:a = 1, b = 1
输出:1
提示:
-2^31 <= a, b <= 2^31 - 1
b != 0
2. 题解
题目要求我们模拟除法运算,条件是不能使用除号“/”、乘号“*”、取余号“%”
,并且规定了环境只能存储32位有符号整数,即取值范围是[−2^31, 2^31−1]
,并规定如果结果溢出则返回2^31 − 1
2.1 减法代替除法
由于不能使用除号,我们很容易可以想到可以用减法来代替
举例:
15/2 = 7……1
可以看成15减去7个2余数为1
这样子我们就可以写出核心的部分了,上代码
var divide = function(dividend, divisor) {
// 当被除数大于除数时,持续减去除数,用res累计结果
let res = 0
while (dividend >= divisor) {
dividend -= divisor;
res++;
}
return res;
}
这里我们会遇到几个问题:
- 只适用于正数除法
- 没有处理边界问题,会导致结果溢出:例如当被除数为-231**,除数为**-1**时,结果为**231,超出了最大范围2^31 - 1
- 效率很低,我们一个一个减去除数,如果被除数很大,需要循环很多遍
2.2 解决符号问题,以及处理边界
除法运算的正负取决于被除数和除数的正负,当其中仅有一个是负数是结果才会是负数,其余情况结果为正数,因此我们可以考虑将两个数都转为正数或者负数,最后再得出运算结果后再确定符号,那么转为正数还是负数呢?
转为负数比较好
原因还是因为取值范围的问题,当一个数是-2^31次方时,转为正数会溢出,而转为负数则不会有这种情况,上代码
var divide = function(dividend, divisor) {
let INT_MAX = 2 ** 31 - 1;
let INT_MIN = - ( 2 ** 31 );
// 处理边界问题
if (dividend === INT_MIN && divisor === -1) return INT_MAX;
// 用于确定符号,为1是是负,其他为正
let sign = 2;
if (dividend > 0) {
dividend = -dividend;
sign--;
}
if (divisor > 0) {
divisor = -divisor;
sign--;
}
// 当被除数大于除数时,持续减去除数,用res累计结果
let res = 0
while (dividend >= divisor) {
dividend -= divisor;
res++;
}
return sign === 1 ? -res : res;
}
2.3 提高效率
比较困难的地方在这里,之前我们是一个一个除数得减去,结果每次加1,那我们可以考虑一次减去多个除数,结果每次加上多个除数的个数
具体思路是:
- 让除数不停得翻倍,直到翻倍结果刚好小于被除数,例如:被除数是15,除数是2,
2 * 2 = 4
,4 * 4 = 8
,8 * 8 = 16
, 16大于15了,说明8才是刚好小于被除数的那个数。 - 8是由4个2组成的,我们把4记录下来,再把被除数减去上面的结果,也就是
15 - 8 = 7
,结果res加上4 - 将7作为被除数,重复步骤1和步骤2:
2 * 2 = 4
,4 * 4 = 8
, 8大于7了,所以取4,4是由2个2组成,结果res加上2,7 - 4 = 3
, - 把3作为被除数,重复步骤1和步骤2:
2 * 2 = 4
, 4大于3,所以取1,结果res加上1,结束循环,最终结果为7
注意:需要注意的是我们将除数翻倍作为判断条件,除数翻倍的结果也是有可能越界的,因此应作具体判断,我们看代码吧
var divide = function(dividend, divisor) {
let INT_MAX = 2 ** 31 - 1;
let INT_MIN = - ( 2 ** 31 );
if (dividend === INT_MIN && divisor === -1) return INT_MAX;
let res = 0;
let sign = 2;
if (dividend > 0) {
dividend = -dividend;
sign--;
}
if (divisor > 0) {
divisor = -divisor;
sign--;
}
while (dividend <= divisor) {
let value = divisor;
// k来记录除数在一轮中的个数,初始值为1
let k = 1
// 为了防止越界,应让value小于-(2^31)/2,也就是-2^30
while (value > -(2 ** 30) && dividend <= (value + value)) {
// 除数翻倍
value = value + value;
// 每一轮k也翻倍
k = k + k;
}
// 被除数减去 翻倍结果刚好小于被除数 的那个数
dividend -= value;
// 将k加入结果
res += k;
}
return sign === 1 ? -res : res;
};
这道题目的讲解就到这里啦,其实还有可以优化的地方,可以考虑位运算,等我学会了日后再补上(maybe),有兴趣的同学也可以先自己研究