大整数相除防溢出算法

  储存大整数是一件很简单的事情,只需要使用足够长的数组即可。但是将储存好的大整数提取出来用于计算却很麻烦。对于编程来说,常用的数学函数基本上已经内置了,但这些函数都要求使用特定的数据类型,而这些数据类型的数值范围很有限,因此无法应用大整数。本文就来探究一下 大整数相除 的问题。

  假设有这样的一个情景。有两个大整数,已经开发出来大整数之间的 求整数商求余数运算。现在来求它们之间的数学除法运算,并将结果转化为 double 类型。并假设这两个大整数,它们超出了 double 类型的范围,但是它们的商在 double 类型的范围之内。

  这两个大整数超出了范围,但是它们它们的商没有超出范围,为什么会有这样的场景呢?这源自笔者很早之前编写的一款开源 多功能计算器 的项目。为了解决浮点数运算的累积误差问题,该项目使用了一种 分数 数据结构来替代 double 数据结构。分数 数据结构也就是使用两个大整数来分别表示分子、分母,这样就能实现无误差运算。但问题在于,分数 在不断地运算中,无法约分的情况越来越多。这样就导致了其分子、分母的无限增大,但其值其实不大。而当需要将最终结果转化为 double 类型时,如果直接使用分子、分母相除的方式,则会因为分子、分母超出一般整数的范围的原因溢出。



问题回顾

先来回顾一下问题。

已有条件:

  • 两个大整数
  • 支持大整数的和、差运算
  • 支持大整数的求整数商、求余数运算

需要解决的问题:

  • 求这两个大整数之间的数学除法运算,并将结果转化为 double 类型

不能直接进行的操作:

  • 不能直接将大整数转化为 double 类型,这会发生溢出。

下面就来解决这个问题。

大整数相除防溢出快速算法

  由于整数可以拆成商与余数的形式,因此可以实现如下数学推导:

  (这里, m m m 是一个普通整数类型中,能允许出现的最大整数。如对于 Java 中的 long 类型, m m m 可以是 Long.MAX_VALUE。)

在这里插入图片描述

这种算法的核心思想是:

  • 先把大整数拆成一些小整数。由于这种拆分通过一种 求余 运算即可实现,因此是可行的。

  • 然后根据分数的性质,分子、分母同时除以一个数,其结果不变。所以再将分子、分母同时除以一个数就可以让分子、分母都变小。

    这种除法对大整数而言是一种 求整数商 运算,因此是可行的。

    这种除法对小整数而言是一种在浮点数范围内的浮点数除法运算,因此也是可行的。

  这样一来,原本很大的整数 a 1 a_1 a1 a 2 a_2 a2,就变成了较小的 b 1 b_1 b1 b 2 b_2 b2 c 1 c_1 c1 c 2 c_2 c2。程序代码如下:

    public static double rational2doubleQuickly(Rational rational) {
        Figure dividend = rational.getNumerator(); // dividend:被除数
        Figure divisor = rational.getDenominator(); // divisor:除数
        final long maxLong = Long.MAX_VALUE;
        final Figure max = new Figure(maxLong);
        if (FigureOperation.greaterOrEqual(dividend, max) || FigureOperation.greaterOrEqual(divisor, max)) {
            Figure[] a1 = FigureOperation.divideAndRemainder(dividend, max);
            long b1 = a1[0].getInteger().longValueExact();
            double c1 = (double) (a1[1].getInteger().longValueExact());

            Figure[] a2 = FigureOperation.divideAndRemainder(divisor, max);
            long b2 = a2[0].getInteger().longValueExact();
            double c2 = (double) (a2[1].getInteger().longValueExact());

            return (b1 + c1 / maxLong) / (b2 + c2 / maxLong);
        } else {
            // 将分子、分母中较大的那个数转换为类型 double 来运算,因为 double 比 long 的范围大
            if (rational.isProperFraction()) {
                return rational.getNumerator().getInteger().longValueExact()
                        / (double) (rational.getDenominator().getInteger().longValueExact());
            } else {
                return (double) (rational.getNumerator().getInteger().longValueExact())
                        / rational.getDenominator().getInteger().longValueExact();
            }
        }
    }

改进

  上面已经实现了“以大化小”的效果,但数学直觉敏锐的读者可能马上意识到,尽管对大整数 a 1 a_1 a1 a 2 a_2 a2 进行了如下拆分,但是不是可能拆分之后的 b 1 b_1 b1 b 2 b_2 b2 依然很大呢?

在这里插入图片描述

  确实可能如此,下面就用 递归 的思想改进上述方案。

大整数相除防溢出递归算法

  上面已经实现了“以大化小”,只是“化小”的结果依然很大。因此只需要一直“以大化小”,直到“化小”的结果令人满意为止。这种思想很适合使用 递归 或者 循环 来实现,只不过前者通常可读性更强。

  要想实现这一点,需要构造一种封闭的情形,也就是使运算之后的结果与运算之前在结构上相同。为此,需要进行一些数学推导:


  这里,要说明的是(以分子为例):

  • m m m 是一个普通整数类型中,能允许出现的最大整数。如对于 Java 中的 long 类型, m m m 可以是 Long.MAX_VALUE

  • a 1 a_1 a1 c 1 c_1 c1 是大整数类型, d 1 d_1 d1 是普通整数类型,而 b 1 b_1 b1 e 1 e_1 e1 f 1 f_1 f1 是 double 类型。

  • 普通整数类型可以直接转化为 double 类型。

  • 在最开始时, b 1 b_1 b1 为 0。

在这里插入图片描述


  这样一来,经过一轮运算之后, a 1 a_1 a1 b 1 b_1 b1 变成了 c 1 c_1 c1 f 1 f_1 f1,且 a 1 a_1 a1 c 1 c_1 c1 小。然后只要通过 尾递归 的方式将运算结果不断迭代就能将 a 1 a_1 a1 a 2 a_2 a2 不断减少,直至变成一个合适的值。

  那么,递归应该在什么结束呢?很简单,只需要每次在运算之前检查 a 1 a_1 a1 a 2 a_2 a2 值就可以了。只要它们小于等于 m m m,就停止递归。只要它们其中有一个大于 m m m,就执行运算并进行递归。

  程序代码如下:

    public static double dividedBetweenBigIntegers(Figure a1, double b1, Figure a2, double b2) {
        final long maxLong = Long.MAX_VALUE;
        final Figure max = new Figure(maxLong);
        if (FigureOperation.greaterOrEqual(a1, max) || FigureOperation.greaterOrEqual(a2, max)) {
            Figure[] a1Group = FigureOperation.divideAndRemainder(a1, max);
            Figure c1 = a1Group[0];
            double d1 = a1Group[1].getInteger().longValueExact() + b1;

            Figure[] a2Group = FigureOperation.divideAndRemainder(a2, max);
            Figure c2 = a2Group[0];
            double d2 = a2Group[1].getInteger().longValueExact() + b2;

            return dividedBetweenBigIntegers(c1, d1 / maxLong, c2, d2 / maxLong);
        } else {
            return (a1.getInteger().longValueExact() + b1) / (a2.getInteger().longValueExact() + b2);
        }
    }

质疑

  数学直觉敏锐的读者可能很快能意识到一个问题,上面的一轮运算,让 a 1 a_1 a1 变小为 c 1 c_1 c1,但让 b 1 b_1 b1 变大成了 f 1 f_1 f1。随着 a 1 a_1 a1 不断减少,有没有可能让 f 1 f_1 f1 变得超过 m m m 呢?如果发生了这种情况,上面的递归算法就没有意义了。

在这里插入图片描述

  下面就从数学上探究一下 f 1 f_1 f1 的取值范围。

严谨性验证

  由前面的探讨,可以得到如下式子:

在这里插入图片描述

在这里插入图片描述

  现在的问题就是要研究图中的那个求和式。如果直接精确计算它的值,这个问题相当棘手。因为它的各个和因子几乎各不相同,且是通过求余运算得到的。幸好我们只需要确认它不会超过 m m m 就行。

  我们知道这些和因子都是通过求余运算得到了,所以它们一定都小于 m m m。我们可以干脆假设它们等于 m m m,计算这种最坏情况下求和式的值。这样就能得到求和式的值的上界。而且,由于 轮换对称性,我们只需要计算上面那个求和式的值的上界。计算结果如下:

在这里插入图片描述

  这是一个等比级数,数学上已经有它们的和公式了,因此可以直接得出结果。

在这里插入图片描述

  而现在这个式子放缩起来非常方便:

在这里插入图片描述

  因此,上面的那个求和式不管有多少项,它的值也是小于 2 的,而 m m m 往往是远大于 2 的,所以上面的 大整数相除防溢出递归算法 是不会有尾项溢出问题的。

  为什么这么巧这个求和式恒小于 2 呢?现在回过头想想,其实很好理解。因为 大整数相除防溢出递归算法 本质上是不断地将分子、分母同除以一个数 m m m。在这种情况下,分子、分母将各自不断减少。因此,分子、分母的各项正拆分式也不可能会很大。

总结

大整数相除防溢出算法 的核心思想如下:

  1. 先把大整数拆成一些小整数。由于这种拆分通过一种 求余 运算即可实现,因此是可行的。

  2. 然后根据分数的性质,分子、分母同时除以一个数,其结果不变。所以再将分子、分母同时除以一个数就可以让分子、分母都变小。

    这种除法对大整数而言是一种 求整数商 运算,因此是可行的。

    这种除法对小整数而言是一种在浮点数范围内的浮点数除法运算,因此也是可行的。

  3. 使用递归的方法重复【1】、【2】的操作,直至分子、分母均位于 double 类型的范围内。

  4. 将分子、分母转化为 double 类型,然后使用浮点数除数运算得出结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值