算法题:整数除法

题目

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

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

力扣链接: https://leetcode.cn/problems/xoh6Oh/description/

参考

解题及优化思路

减法代替除法

由于题中要求不得使用乘号,除号,求余符号,所以只能使用减法去间接实现除法。
假如被除数a=22,除数b=3,那么结果应该为7。
减法代替除法的计算逻辑是:22-3=19 19-3=16 16-3=13 13-3=10 10-3=7 7-3=4 4-3=1
被除数依次减去除数,直到被除数小于除数,代码如下

public int divide(int a, int b){
        int res = 0;
        while (a >= b){
            a -= b;
            res ++;
        }
        return res;
    }

这里没有考虑正负的问题,可以设置一个标识符,如果被除数和除数同号,标识符取1,否则取-1,代码如下

public int divide(int a, int b){
        int sign = 1;
        if ((a > 0 && b < 0) || (a < 0 && b > 0)){
            sign = -1;
        }
        a = Math.abs(a);
        b = Math.abs(b);

        int res = 0;
        while (a >= b){
            a -= b;
            res ++;
        }
        return sign * res;
    }

上面代码判断正负是将结果为负的两种情况列出来,其实还有一种方法,用异或符号"^",符号两边的二进制位相同结果为false,否则为true,用到这里就是:如果a>0和b>0同时成立(异或结果为false),则取1,否则取-1,所以代码可以优化如下:

public int divide(int a, int b){
        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
        a = Math.abs(a);
        b = Math.abs(b);

        int res = 0;
        while (a >= b){
            a -= b;
            res ++;
        }
        return sign * res;
    }

这里没有考虑边界问题,题目中要求"只能存储 32 位有符号整数",即a和b只能是int型数据,最大值是2^31
-1=2147483647,最小值是-2^31=-2147483648。
如果a是最大值,永远不会越界。
如果a是最小值,且b=-1,则结果为2147483648,大于最大值,发生越界。
所以要先把越界的情况列出来,代码如下

public int divide2(int a, int b){
//        如果溢出,按照题目要求返回最大值
        if (a == Integer.MIN_VALUE && b == -1){
            return Integer.MAX_VALUE;
        }

        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
        a = Math.abs(a);
        b = Math.abs(b);

        int res = 0;
        while (a >= b){
            a -= b;
            res ++;
        }
        return sign * res;
    }

上述代码还有问题:Math.abs(-2147483648)=-2147483648,即最小值取绝对值后还是最小值,这种情况会使结果错误。那么可以考虑:取绝对值的目的是让被除数和除数符号相同,那么可以将被除数和除数全部转为正数,还可以都转为负数,转为正数有问题,那么便可以都转为负数。

public int divide(int a, int b){
        if (a == Integer.MIN_VALUE && b == -1){
            return Integer.MAX_VALUE;
        }

        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
//        a = Math.abs(a);
//        b = Math.abs(b);
        if (a > 0) a = -a;
        if (b > 0) b = -b;

        int res = 0;
//      如果a,b是正数,循环条件为a>=b,如果是负数,循环条件为a<=b  
        while (a <= b){
            a -= b;
            res ++;
        }
        return sign * res;
    }

到这里代码没有问题了,但是时间复杂度为O(n),当a=2147483647, b=1时,n是2147483647,太耗时了,需要优化。

优化:减去除数的倍数

上面代码主要耗时的地方在于,被除数要一次一次地减去除数,直到不能减了为止,那么可以考虑每次减去除数的倍数,比如:
在这里插入图片描述

那么最终结果将所有的k加起来,即4+2+1=7,时间复杂度为O(logn * logn),代码如下

public int divide4(int a, int b){
        if (a == Integer.MIN_VALUE && b == -1){
            return Integer.MAX_VALUE;
        }

        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
        if (a > 0) a = -a;
        if (b > 0) b = -b;

        int res = 0;
        while (a <= b){
            int value = b;//除数
            int k = 1;
//            如果a是正数,这里应该是a>=value+value
//            while (a <= value + value){

//            为了保证value+value不溢出,即保证value+value>=-2^31,需要让value>=-2^30
//            0xc0000000是十进制-2^30的十六进制表示,是最小值-2^31的一半
            while (value >= 0xc0000000 && a <= value + value){
                value += value;
                k += k;
            }
            a -= value;
            res += k;
        }
        return sign * res;
    }

如果是Java,优化到这里已经可以了,但如果是Python,可能会超时,所以可以继续优化。

优化:使用位运算

一个数乘以2相当于对其左移一位,如3*2=3<<1=6,所以上面的计算可以修改为
在这里插入图片描述
将加号改为左移结果也是7,但这样改时间复杂度并没有变化。
可以继续优化,上面的左移每次都是从左移1位开始的,那么是不是也可以从最大位数开始左移。环境只能存储 32 位有符号整数,所以最大位数是31位,那么就左移31位,30位 …具体计算如下

结果为4+2+1=7。
时间复杂度位O(31)~O(1)。
代码如下

public int divide7(int a, int b){
        if (a == Integer.MIN_VALUE && b == -1){
            return Integer.MAX_VALUE;
        }

        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
//        这里用正数比较好
        a = Math.abs(a);
        b = Math.abs(b);
        
        int res = 0;
        for (int i=31; i>=0; i--){
            if (a >= (b << i)){
                a -= (b << i);
                res += (1 << i);
            }
        }
        return sign * res;
    }

写到这里还不行,还需要考虑边界问题。
第一,第13行代码的判断条件种,b<<i 这里,当i比较大时会发生越界,因为b<<i 相当于b*2^31,可以将判断条件这样改:(a >> i) >= b,相当于在>=号的左边除以2^31,这样不会越界。
第二,如果a是最小值-2147483648,上面说过最小值取绝对值后还是最小值,即Math.abs(-2147483648)=-2147483648,此时,如果b是正数,那么(a >> i) >= b将永远不成立,为了使(a>>i)的结果为正数,可以使用无符号右移>>>,这样a>>>i的结果将永远是正数,所以要将判断条件改为(a >>> i) >= b。
第三,如果b是最小值,那么(a >>> i) >= b的结果是true,该条件将永远成立,为了解决这个问题,可以改为减法,即(a >>> i) - b >= 0,这样的话,正数(a>>>i)减去最小值b,结果由于溢出一定是负数,判断结果为负数。代码如下:

public int divide7(int a, int b){
        if (a == Integer.MIN_VALUE && b == -1){
            return Integer.MAX_VALUE;
        }

        int sign = (a > 0) ^ (b > 0) ? -1 : 1;
//        这里用正数比较好
        a = Math.abs(a);
        b = Math.abs(b);

        int res = 0;
        for (int i=31; i>=0; i--){
            if ((a >>> i) - b >= 0){
                a -= (b << i); //当a是最小值时,溢出,但溢出结果恰好是a=|a|-(b<<i)的结果,此时a为正数
                res += (1 << i);
            }
        }
        return sign * res;
    }

其他

欢迎关注同名公众号【长弓瑾瑜】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值