题意
给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和取余运算。
整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8 ,-2.7335 将被截断至 -2 。
返回被除数 dividend 除以除数 divisor 得到的商。
难度
中等
示例
输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = 3.33333.. ,向零截断后得到 3 。
输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = -2.33333.. ,向零截断后得到 -2 。
分析 1
题目要求不能用除法(/),也不能用乘法(*),还有 “取余(%)”,如果我们偏要用除法会怎么样呢?
class Solution {
public int divide(int dividend, int divisor) {
// 直接使用除法计算结果
long result = (long) dividend / divisor;
// 处理溢出情况
if (result > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (result < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return (int) result;
}
}
来看一下运行结果:
其实有时候我们真解不出来的话,多多少少先把答案写上去,运行通过,但遇到大数就不会成功。
分析 2
题目要求不能用除法,不过我们处理溢出情况的写法,后面还是要用到的,尤其是 (long) dividend 这个强转的处理,其实就考察了基本数据类型的转换问题,包括最后返回的 (int) result,都是很细节的问题,但却很重要。
那既然不能用除法,我们应该怎么去计算两数相除呢?
用减法。
除法基本上是减法的逆运算,它表示将一个数(被除数)分成几个相等的部分(除数)时,可以得到多少个这样的部分(商)。如果用减法来实现除法,本质上是在问:“我可以从被除数中减去多少次除数?”每减去一次,就相当于得到了一个单位的商。
假设我们要计算 dividend / divisor = quotient(被除数 / 除数 = 商),其中 dividend 和 divisor 是已知的,我们要找到 quotient。
①、初始化计数器:设置一个计数器 count,初始值为 0。这个计数器将用来记录我们能从被除数中减去多少次除数。
②、循环减除数:只要被除数 dividend 大于或等于除数 divisor,就从被除数中减去除数,并且计数器 count 加一。
while (dividend >= divisor) {
dividend -= divisor;
count++;
}
③、处理剩余部分:当被除数小于除数时,循环结束。此时,count 的值就是整数除法的结果(商),而此时的 dividend 是除法的余数。
假设我们要计算 10 / 3:
- 初始时,dividend = 10,divisor = 3,count = 0。
- 10 >= 3,执行减法 10 - 3 = 7,count = 1。
- 7 >= 3,再次执行减法 7 - 3 = 4,count = 2。
- 4 >= 3,再次执行减法 4 - 3 = 1,count = 3。
- 此时,1 < 3,循环结束,count = 3 是商,1 是余数。
我们来试一下,直接用 ACM 的模式在 Intellij IDEA 中打印结果:
public class TwoDivide {
public static void main(String[] args) {
// int divide = divide1(10, 3);
// System.out.println(divide);
System.out.println(divide1(10, 3));
System.out.println(divide1(7, -3));
System.out.println(divide1(0, 1));
System.out.println(divide1(1, 1));
System.out.println(divide1(1, 0));
System.out.println(divide1(-2147483648, -1));
}
public static int divide1( int dividend , int divisor){
// 处理特殊情况
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
// 符号处理
boolean negative = (dividend < 0) ^ (divisor < 0);
long ldividend = Math.abs((long)dividend);
long ldivisor = Math.abs((long)divisor);
long result = 0;
while (ldividend >= ldivisor) {
ldividend -= ldivisor;
result++;
}
return negative ? (int)-result : (int)result;
}
很遗憾,前 5 个测试用例都是可以顺利通过的,但最后 1 个测试用例耗时会特别长,因为要计算 2147483648 / 1次,这个数太大了,所以我们要想办法优化一下。
优化减法实现除法的关键在于减少需要执行的减法操作次数,对吧?
我们可以结合减法和倍增思想,每次减去 divisor 的倍数,这样就可以减少减法的次数。
- 初始时,令一个临时除数等于除数,每次尝试从被除数中减去临时除数。
- 如果可以减去,更新被除数(减去临时除数),临时除数倍增(乘以 2),同时记录下减去的次数。
- 如果不可以减去,则临时除数回退到除数,继续尝试减去直到被除数小于除数。
public static int divide2( int dividend , int divisor){
// 处理特殊情况
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
// 符号处理
boolean negative = (dividend < 0) ^ (divisor < 0);
long ldividend = Math.abs((long)dividend);
long ldivisor = Math.abs((long)divisor);
long result = 0;
while (ldividend >=ldivisor){
long tempDivisor = ldivisor , multilpe = 1;
//将除数加倍,并记录加的倍数
while (ldividend >= tempDivisor+tempDivisor){
tempDivisor+= tempDivisor; //加倍除数
multilpe+= multilpe; //累加倍数
}
ldividend-=tempDivisor; //减去加倍后的除数
result+=multilpe;
}
return negative ? (int)-result : (int)result;
}
内层 while 循环,确定能够从被除数中减去的最大的除数的倍数。
这个循环通过不断地加倍除数(乘以 2),寻找最接近且不超过被除数的那个除数的倍数。这个过程实际上是在模拟除法的过程,尝试找到一个合适的倍数,使得除数 × 倍数 ≤ 被除数,且尽可能地大。
while (ldividend >= tempDivisor + tempDivisor) {
tempDivisor += tempDivisor; // 加倍除数
multiple += multiple; // 记录加倍次数
}
- tempDivisor 从原始的除数开始,每次循环加倍(相当于tempDivisor * 2),试图接近但不超过 ldividend。
- multiple 记录了加倍的次数,也就是说,如果 tempDivisor 加倍了,那么 multiple 也加倍,表示除数增加了多少倍。
外层 while 循环,使用上一个循环确定的最大倍数来更新被除数,并累加到最终结果中。
当第一个循环结束后,我们找到了最接近且不超过被除数的那个除数的倍数。然后,我们从被除数中减去这个加倍后的除数,并将对应的倍数累加到最终结果中。如果更新后的被除数仍然大于或等于原始除数,这个过程会重复进行。
ldividend-=tempDivisor;//减去加倍后的除数
result+=multiple;//累加倍数
- ldividend -= tempDivisor;更新被除数,减去了找到的最大可减的加倍后的除数。
- result += multiple;将这一步中减去的除数的倍数累加到 result 中,因为这表示了我们在这一步中“除以了多少个”除数。
来看一下运行效率,还挺高:
分析 3
减法和倍增的另外一种变体就是使用位移,我们来看一下题解:
public int divide3(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE; // 防止溢出
}
boolean negative = (dividend < 0) ^ (divisor < 0);
long ldividend = Math.abs((long)dividend);
long ldivisor = Math.abs((long)divisor);
long result = 0;
while (ldividend >= ldivisor) {
long temp = ldivisor, multiple = 1;
while (ldividend >= (temp << 1)) {
temp <<= 1;
multiple <<= 1;
}
ldividend -= temp;
result += multiple;
}
return negative ? (int)-result : (int)result;
}
在二进制表示中,每向左移动一位,其实就等价于将这个数乘以 2。所以,我们可以通过位移来实现倍增。
while (ldividend >= (temp << 1)) {
temp <<= 1;
multiple <<= 1;
}
假设 temp 的值为 3,其二进制表示为 0011(这里为了简化,只展示了四位二进制)。当执行 temp << 1 操作时,temp 的二进制表示会向左移动一位,变为 0110,此时 temp 的值就变成了 6。可以看到,3 * 2 = 6,这正是位左移一位的效果。
分析 4
我们还可以使用二分法来实现除法,这个方法是最优的,也是最常用的。
二分法的思路是:我们可以将除数不断地向左移位,直到它大于被除数。因为这样可以最大限度地减小被除数,使得被除数减去除数的次数最少。
我们来看一下题解:
class Solution {
public int divide(int dividend, int divisor) {
// 处理特殊情况:Integer.MIN_VALUE除以-1会溢出
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
// 使用异或运算确定结果的正负性,不同号为负
boolean negative = (dividend < 0) ^ (divisor < 0);
// 将被除数和除数都转换为正数,使用long类型避免溢出
long ldividend = Math.abs((long)dividend);
long ldivisor = Math.abs((long)divisor);
// 初始化结果变量
long result = 0;
// 从最高位开始检查,对32位整数而言,这是31位
for (int i = 31; i >= 0; i--) {
// 检查ldividend右移i位后是否仍然大于等于ldivisor
if ((ldividend >> i) >= ldivisor) {
// 如果是,说明在2^i的位置上有一个ldivisor,将2^i累加到结果中
result += 1L << i;
// 同时,从ldividend中减去ldivisor左移i位的值,即减去了ldivisor的2^i倍
ldividend -= ldivisor << i;
}
}
// 根据之前确定的正负性,返回正确的结果
return negative ? (int)-result : (int)result;
}
}
public int divide4(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE; // 防止溢出
}
boolean negative = (dividend < 0) ^ (divisor < 0);
long ldividend = Math.abs((long)dividend);
long ldivisor = Math.abs((long)divisor);
long result = 0;
//从最高位开始检查,对32位整数而言,这是31位
for (int i = 31 ;i >=0 ; i--){
//检查ldividend右移i位后是否仍大于等于ldivisor
if (ldividend>>i >= ldivisor){
//如果是,说明在i^2的位置上有一个ldivisor,将i^2累加到结果中
result+=1 << i ;
ldividend-= ldivisor << i;
}
}
// 根据之前确定的正负性,返回正确的结果
return negative ? (int)-result : (int)result;
}
这个题解虽然没有直接采用传统的二分搜索算法框架,但它实质上利用了二分思想的核心——每次操作都在减少搜索范围,逐步逼近目标值。
在这个问题中,目标是找到一个数x,使得x * divisor ≈ dividend。通过位操作,每次将dividend右移,实际上是在缩小搜索范围。
这个过程可以看作是对商的二进制表示的逐位确认:
- 高位到低位逼近:从dividend的最高位开始,逐步向下检查每一位,看在当前位上divisor是否能"放入"。这里,“放入”的意思是,看乘以2^i(通过左移i位实现)后的divisor是否不超过当前的dividend。这相当于在二分搜索的每一步中,决定这一位上的商是0还是1。
- 动态调整搜索范围:通过不断地调整ldividend(即减去已经确定能够放入的divisor的倍数),实际上是在缩小搜索范围,因为每次操作后的ldividend代表了尚未被除尽的余数。
总结
在数学中,加法是加减乘除的基础,减法是加法的逆运算,乘法是加法的倍增,除法是乘法的逆运算。
两数相乘,可以用加法来实现,两数相除,可以用减法来实现,这是最基本的思路。
比如说 10/2,我们可以用 10-2-2-2-2-2=0 来实现,这样就得到了商 5。
比如说 10/3,我们可以用 10-3-3-3-1=0 来实现,这样就得到了商 3 余 1。