整数除法

378 阅读4分钟

「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

前言

笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。

系列文章收录《算法》专栏中。

问题描述

给定两个整数 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

确定学习目标

对《整数除法》的算法过程进行剖析。

剖析

同样如果使用我们平时用的十进制出除法会产生借位除,我们优先使用已经实现的《不用加减乘除做加法》《不用加减乘除做减法》来实现,我们该怎么做呢?

其实整数除法在简单的时候我们不计算可以很容易猜的出来,比如101/5,我们脑子里马上就能算出结果为20。为什么呢,因为我们除法可以转换为乘法,设x和y相除等于z,我们是不是可以转换成x=z*y。其实本质上我们就是在给除数扩大倍数,那么我们是不是可以使用二分法来实现除法呢?当然,下面我们再考虑下,实现过程中会遇到的问题。

  1. 结果正负数:两数相除的时候我们还要考虑a和b的正负数,如果把正负数在算法的过程中去处理会比较复杂,通用的方式是先转换成统一符号,那么结果就需要转换了也比较简单放在最后在处理,如果我们统一转换成正数那么当a或b为Integer.MIN_VALUE的时候就会出现溢出,所以我们需要统一转换成负数。
  2. 求商过程中累加的溢出:在求结果的时候本质上就是累加,但是累加会出现负数,所以我们改用累减。
  3. 结果溢出的处理:其实溢出的唯一情况就是Integer.MIN_VALUE除以-1,我们在最后的时候对商累减结果判断下如果为Integer.MIN_VALUE并且是同号的情况下就直接进行加1变成Integer.MAX_VALUE符合题目的要求。
  4. 进行翻倍的溢出:进行翻倍之前需要进行判断,翻倍之前的数必须大于等于Integer.MIN_VALUE >> 1 避免溢出。

具体过程看下面的代码。

代码

    /**
     * 1. a和b异或位运算得出每位抛去进位的结果。
     * 2. a和b与位运算再左移1位,得出进位结果
     * 3. 设进位结果为b,a为每位抛去进位的结果,如果b!=0就重复步骤1和2。
     * <p>
     * 溢出的话就是只表示允许表示的位的结果(比如int 直接从右往左取32位)
     *
     * @param a
     * @param b
     * @return
     */
    public static int add(int a, int b) {
        int sum = a;
        while (b != 0) {
            sum = a ^ b;
            int carry = (a & b) << 1;
            a = sum;
            b = carry;
        }

        return sum;
    }

    /**
     * 减法和加法的区别在于减数需要取补码(取反加1)再进行相加
     *
     * @param a
     * @param b
     * @return
     */
    public static int substract(int a, int b) {
        b = add(~b, 1);
        return add(a, b);
    }

    /**
     * 其实整数除法在简单的时候我们不计算可以很容易猜的出来,比如101/5,我们脑子里马上就能算出结果为20。为什么呢,因为我们除法可以转换为乘法,设x和y相除等于z,我们是不是可以转换成x=z*y。
     * 其实本质上我们就是在给除数扩大倍数,那么我们是不是可以使用二分法来实现除法呢?当然,下面我们再考虑下,实现过程中会遇到的问题。
     * <p>
     * 1. 结果正负数:两数相除的时候我们还要考虑a和b的正负数,如果把正负数在算法的过程中去处理会比较复杂,通用的方式是先转换成统一符号,那么结果就需要转换了也比较简单放在最后在处理,如果我们统一转换成正数那么当a或b为Integer.MIN_VALUE的时候就会出现溢出,所以我们需要统一转换成负数。
     * 2. 求商过程中累加的溢出:在求结果的时候本质上就是累加,但是累加会出现负数,所以我们改用累减。
     * 3. 结果溢出的处理:其实溢出的唯一情况就是Integer.MIN_VALUE除以-1,我们在最后的时候对商累减结果判断下如果为Integer.MIN_VALUE并且是同号的情况下就直接进行加1变成Integer.MAX_VALUE符合题目的要求。
     *
     * @param a
     * @param b
     * @return
     */
    public static int divide(int a, int b) {
        if (b == 0) {
            throw new RuntimeException("b不能为0");
        }
        //用于标记a、b正负号的情况
        int flag = 0;

        //统一转换成负数
        if (a > 0) {
            a = add(~a, 1);
            flag = add(flag, 1);
        }

       //统一转换成负数
        if (b > 0) {
            b = add(~b, 1);
            flag = add(flag, 1);
        }

        int ret = calc(a, b);

        // 溢出的唯一情况就是Integer.MIN_VALUE除以-1,我们在最后的时候对商累减结果判断下如果为Integer.MIN_VALUE并且是同号的情况下就直接进行加1变成Integer.MAX_VALUE符合题目的要求。
        if (flag != 1 && ret == Integer.MIN_VALUE) {
            ret = add(ret, 1);
        }

        //因为ret本来就是负数,所以当a,b异号的情况下直接返回,否则取反
        return flag == 1 ? ret : add(~ret, 1);
    }

    /**
     * a和b目前都是负数
     * a要小于b才继续求商否则就是0
     * 翻倍结果预先和a比较直到小于a,同时进行翻倍之前需要进行判断,翻倍之前的数必须大于等于`Integer.MIN_VALUE >> 1` 避免溢出。
     * 翻倍终止后a变成a减去终止后最终翻倍的结果,然后重头让b进行翻倍继续使用二分发逼近a直到b不能继续翻倍,说明只剩余数
     *
     * @param a
     * @param b
     * @return ret返回是负的 这样不会溢出
     */
    private static int calc(int a, int b) {
        int ret = 0;
        while (a <= b) {
            int cnt = 1;
            int val = b;
            while (val >= Integer.MIN_VALUE >> 1 && a <= val << 1) {
                cnt = add(cnt, cnt);
                val = add(val, val);
            }
            ret = substract(ret, cnt);
            a = substract(a, val);
        }
        return ret;
    }