《C语言入门100例》(第4例) 给定 a 和 b,问 a 能否被 b 整除 | if 语句 和 条件运算符的应用

本文介绍了如何通过位运算和二分查找算法,避开乘除和mod运算符,解决LeetCode 29题的整数除法问题。着重讨论了如何在有限精度内处理溢出,以及如何利用负数动态范围优化搜索。

【第04题】给定 a 和 b,问 a 能否被 b 整除 | if 语句 和 条件运算符的应用

主要知识点

嗯,别看了,这里确实什么都没有,直接上题

习题

1. Leetcode 29. 两数相除

题目描述

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
限制:
- 被除数和除数均为 32 位有符号整数。
- 除数不为 0。
- 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [ − 2 31 , 2 31 − 1 ] [−2^{31}, 2^{31} − 1] [231,2311]。本题中,如果除法结果溢出,则返回 2 31 − 1 2^{31} − 1 2311

初见

说到除法的替代,第一反应肯定是通过循环将其降级为减法 😄 不出所料,写代码,提交,超时一气呵成~
那么存在限制条件的情况下,当然又将思路回到了位运算上。这里让我们暂时忽略最后一个限制条件,此时我们要做的是利用位运算,每次判断时将除数 × 2 \times2 ×2,提升循环减法的搜索速度:

int divide(int dividend, int divisor) {
    int sign = (dividend>>31 & 0x1) ^ (divisor>>31 & 0x1); // 符号位,同号为 0, 异号为 1
    if (sign) {
    	sign = -1;
    } else {
        return 1;
    }
    long long divd = abs(dividend); // 这里我们用 long long 方便计算
    long long divi = abs(divisor);
    long long oriDiv = divi;
    long long result(0);
    if (divi == divd) {
        return sign * 1;
    }
    while (divi <= divd) {
        int bitMove(0);
        while (divi <= divd) {
            divi <<= 1; // divisor * 2
            bitMove++;
        }
        bitMove--;
        divd = divd - (divi>>1); // 更新被除数,divd - 2^bitMove * divi
        divi = oriDiv; // 重置 divi
        if (bitMove > 0) { // 更新结果
            result += (unsigned int)0x1 << bitMove;
        } else {
            result++;
        }
    }
    result *= sign;
    if (result > 0x7FFFFFFF) { // 保证不越界
        return 0x7FFFFFFF;
    } else {
        return result;
    }
}
思路

回到最后一个限制条件上,这里要求我们整个运算过程中只用 32 位有符号整数,那么可以换一种搜索方式,加快搜索速度。此时可以采用 二分查找divisordividend 均为负数,计算 dividend / divisor):

  1. l = 1, r = intMax;
  2. l <= r ,取 mid = l + ((r - l) >> 1), 否则跳至步骤 4;
  3. 计算 result = mid * divisor,若 result < divisor 则令 r = mid - 1,否则记录 mid 值并令 l = mid + 1,重复步骤 2, 3;
  4. 返回结果

上述描述中的乘法可以参考习题1实现快速乘法。描述中没有涉及边界值的处理,但在代码中需要对可能涉及到越界的情况进行判断与处理。该方法为二分查找的具体应用,模板可以参考资料

代码
int divide(int dividend, int divisor) {
    bool sameSign = (dividend >> 31 & 0x1) ^ (divisor >> 31 & 0x1);
    if (dividend == 0) { // 被除数为 0 时不能依靠快速乘计算结果
        return 0;
    }
    if (dividend == INT_MIN) { // 处理边界值
        if (divisor == 1) {
            return INT_MIN;
        }
        if (divisor == -1) {
            return INT_MAX;
        }
    }
    if (dividend > 0) { // 正数转到负数扩大动态范围,方便统一处理边界值
       dividend = -dividend;
    }
    if (divisor > 0) {
        divisor = -divisor;
    }
    // 二分查找
    int l(1), r(INT_MAX), ans(0);
    while (l <= r) {
        int mid = l + ((r - l) >> 1);
        bool isLess = checkIfLessC(mid, divisor, dividend); // 查找条件
        if (isLess) {
            r = mid - 1; // 若没有 mid +- 1,则不能搜索 [l, r] 范围内的所有数
        } else {
            ans = mid;
            if (mid == INT_MAX) { // 防止 +1 后越界
                return INT_MAX;
            }
            l = mid + 1;
        }
    }

    return sameSign? -ans: ans;
}

bool checkIfLessC(int mid, int divisor, int dividend) {
    // 若 mid * divisor < dividend 返回 true
    int ans(0);
    while (mid != 0) {
        if (mid & 0x1) {
            if (ans < dividend - divisor) { // 防止 ans + divisor 越界
                return true;
            }
            ans += divisor;
        }
        mid >>= 1;
        if (mid != 0) {
            if (divisor < dividend) { // 提前结束循环
                return true;
            }
             // 防止 divisor + divisor 越界,此时若越界则 mid * divisor < dividend
            if (divisor < INT_MIN / 2) {
                return true;
            }
            divisor += divisor;
        }
    }

    return false;
}

总结

  1. 位运算在限制条件下真的很好用
  2. 二分查找算法
    1. 两种基础格式,一种搜索 [ L , R ) [L, R) [L,R),一种搜索 [ L , R ] [L, R] [L,R]
    2. 要注意算法中边界条件的处理
  3. 负数动态范围更大,因此在有符号数的计算中,为处理溢出可以将正数转为负数,进行统一处理

资料
二分法基础 模板 常见写法 - No2ya - 博客园

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值