题目描述

image.png

题目链接

https://leetcode.cn/problems/divide-two-integers/

思路

前言
由于题目规定了「只能存储【29】两数相除 - 图2位整数」,本题解的正文部分和代码中都不会使用任何【29】两数相除 - 图3位整数。诚然,使用【29】两数相除 - 图4位整数可以极大地方便我们的编码,但这是违反题目规则的。题目规定:如果除法结果溢出,那么我们需要返回【29】两数相除 - 图5作为答案。因此在编码之前,我们首先对于溢出或容易出错的边界情况进行讨论:

  • 当被除数为【29】两数相除 - 图6位有符号整数的最小值【29】两数相除 - 图7时:
    • 如果除数为【29】两数相除 - 图8,那么我们可以直接返回答案【29】两数相除 - 图9
    • 如果除数为【29】两数相除 - 图10,那么答案为【29】两数相除 - 图11,产生了溢出。此时我们需返回【29】两数相除 - 图12


  • 当除数为【29】两数相除 - 图13位有符号整数的最小值【29】两数相除 - 图14时:
    • 如果被除数同样为【29】两数相除 - 图15,那么我们可以直接返回答案【29】两数相除 - 图16
    • 对于其余的情况,我们返回答案【29】两数相除 - 图17


  • 当被除数为【29】两数相除 - 图18时,我们可以直接返回答案【29】两数相除 - 图19

对于一般的情况,根据除数和被除数的符号,我们需要考虑【29】两数相除 - 图20种不同的可能性。因此,为了方便编码,我们可以将被除数或者除数取相反数,使得它们符号相同。

如果我们将被除数和除数都变为正数,那么可能会导致溢出。例如当被除数为【29】两数相除 - 图21时,它的相反数【29】两数相除 - 图22就产生了溢出。因此,我们将被除数和除数都变为负数,这样就不会溢出,在编码时只需要考虑【29】两数相除 - 图23种情况就可以了。如果我们将被除数和除数的其中(恰好)一个变为了正数,那么在返回答案之前,我们需要对答案也取相反数。

二分查找思路
根据「前言」部分的讨论,我们记被除数为【29】两数相除 - 图24,除数为【29】两数相除 - 图25,并且【29】两数相除 - 图26【29】两数相除 - 图27都是负数。我们需要找出【29】两数相除 - 图28的结果【29】两数相除 - 图29【29】两数相除 - 图30一定是正数或【29】两数相除 - 图31。根据除法以及余数的定义,我们可以将其改成乘法的等价形式,即:

【29】两数相除 - 图32

因为【29】两数相除 - 图33是正数或【29】两数相除 - 图34【29】两数相除 - 图35【29】两数相除 - 图36都是负数,所以【29】两数相除 - 图37(正负得负),又因为对【29】两数相除 - 图38截取其小数部分得到的值肯定是小于或等于【29】两数相除 - 图39的,所以【29】两数相除 - 图40,例如【29】两数相除 - 图41

我们可以使用二分查找的方法得到【29】两数相除 - 图42,即找出最大的【29】两数相除 - 图43使得【29】两数相除 - 图44成立。但由于我们不能使用乘法运算符,因此我们可以使用「快速乘」算法得到【29】两数相除 - 图45的值。快速乘算法与「快速幂」类似,前者通过加法实现乘法,后者通过乘法实现幂运算。快速幂算法可以参考 50. Pow(x, n) 的题解,快速乘算法只需要在快速幂算法的基础上,将乘法运算改成加法运算即可。

细节
由于我们只能使用【29】两数相除 - 图46位整数,因此二分查找中会有很多细节。

首先,二分查找的下界为【29】两数相除 - 图47,上界为【29】两数相除 - 图48。唯一可能出现的答案为【29】两数相除 - 图49的情况已经被我们在「前言」部分进行了特殊处理,因此答案的最大值为【29】两数相除 - 图50。如果二分查找失败,那么答案一定为【29】两数相除 - 图51

在实现「快速乘」时,我们需要使用加法运算,然而较大的【29】两数相除 - 图52也会导致加法运算溢出。例如我们要判断【29】两数相除 - 图53是否小于【29】两数相除 - 图54时(其中【29】两数相除 - 图55均为负数),【29】两数相除 - 图56可能会产生溢出,因此我们必须将判断改为【29】两数相除 - 图57是否成立。由于任意两个负数的差一定在【29】两数相除 - 图58范围内,这样就不会产生溢出。

代码实现

  1. public int divide(int dividend, int divisor) {
  2. // 考虑被除数为最小值的情况
  3. if (dividend == Integer.MIN_VALUE) {
  4. if (divisor == 1) {
  5. return Integer.MIN_VALUE;
  6. }
  7. if (divisor == -1) {
  8. return Integer.MAX_VALUE;
  9. }
  10. }
  11. // 考虑除数为最小值的情况
  12. if (divisor == Integer.MIN_VALUE) {
  13. return dividend == Integer.MIN_VALUE ? 1 : 0;
  14. }
  15. // 考虑被除数为 0 的情况
  16. if (dividend == 0) {
  17. return 0;
  18. }
  19. // 将所有的正数取相反数,这样就只需要考虑一种情况
  20. boolean rev = false;
  21. if (dividend > 0) {
  22. dividend = -dividend;
  23. rev = true;
  24. }
  25. if (divisor > 0) {
  26. divisor = -divisor;
  27. rev = !rev;
  28. }
  29. // 二分查找
  30. int left = 1, right = Integer.MAX_VALUE, ans = 0;
  31. while (left <= right) {
  32. // 注意溢出,并且不能使用除法
  33. int mid = left + ((right - left) >> 1);
  34. if (quickAdd(divisor, mid, dividend)) {
  35. ans = mid;
  36. if (mid == Integer.MAX_VALUE) {
  37. break;
  38. }
  39. left = mid + 1;
  40. } else {
  41. right = mid - 1;
  42. }
  43. }
  44. return rev ? -ans : ans;
  45. }
  46. // x 和 y 是负数,z 是正数,需要判断 z * y >= x 是否成立,即 z 个 y 相加
  47. public boolean quickAdd(int y, int z, int x) {
  48. int result = 0;
  49. while (z != 0) {
  50. if ((z & 1) == 1) {
  51. // 需要保证 result + y >= x
  52. if (result + y < x) {
  53. return false;
  54. }
  55. result += y;
  56. }
  57. if (z != 1) {
  58. // 需要保证 y + y >= x
  59. if (y + y < x) {
  60. return false;
  61. }
  62. y += y;
  63. }
  64. z >>= 1;
  65. }
  66. return true;
  67. }

复杂度分析

  • 时间复杂度:【29】两数相除 - 图59,其中【29】两数相除 - 图60表示【29】两数相除 - 图61位整数的范围。二分查找的次数为【29】两数相除 - 图62,其中的每一步我们都需要【29】两数相除 - 图63使用「快速乘」算法判断【29】两数相除 - 图64是否成立。
  • 空间复杂度:【29】两数相除 - 图65