动态Java整数/长整数溢出检查与性能比较

4

这是一个相对理论性的问题,虽然语言是Java,但是任何通用解决方案都可以胜任。

假设我想编写一个简单的阶乘函数:

long factorial(int n)
{
    //handle special cases like negatives, etc.

    long p = 1;
    for(int i = 1; i <= n; i++)
    {
        p = p * n;
    }
    return p;
}

但现在,我还想检查阶乘是否溢出(而不是简单地硬编码MAX_FACTORIAL_PARAMETER之类的内容)。一般来说,在乘法过程中检查溢出就像检查结果是否与原始输入相同一样简单。但在这种情况下,由于溢出可能发生在任何时候,每个循环中执行更多的除法和比较将非常昂贵。

因此,问题有两个方面-有没有办法解决阶乘问题的溢出,而不是在每个步骤检查乘法溢出或硬编码最大允许参数?

那么一般来说,我应该如何处理涉及多个迭代/递归阶段的问题,这些阶段可能会在每个阶段默默失败,而不会通过引入昂贵的检查来危及性能?


3
您已经排除了所有可能性。通过更深入的数学分析,可以预测溢出何时发生,但这将产生一个 MAX_ 类型的常量,而您已禁止使用它。如果没有这个,唯一的选择就是检查每个可能会溢出的选项,但您认为这太昂贵了,因此也被禁止了。我想您可以使用大整数算术。您忘记禁止它了。 - President James K. Polk
6个回答

1

这取决于你具体的问题。也许你可以进行曲线绘制或其他数学分析。

在你给出的例子中,最好的方法是在每个循环中进行检查。这并不费时,因为它甚至不会修改你的复杂度类(因为O(n) = O(n+n) = O(2n) = O(n))。在大多数情况下,进行简单的检查也是最好的选择,因为它可以使你的代码保持干净和可维护性。


1

虽然 Java 无法帮助你解决这个问题,但肯定有一些编程语言可以帮助你解决溢出问题。例如,C# 提供了checked keyword。在底层,这个特性可能使用overflow flag的硬件支持。


1
在这种特定情况下,最简单的方法是硬编码最大的'n'值。如果速度对你很重要,你可以将所有可能的值存储在数组中(并不多),而不进行任何计算。 ;)
如果你想改进这个方法,使用long结果(不会好太多,但是一个简单的改变),或者使用BigInteger,它没有溢出问题。 ;)

0
有没有办法解决阶乘问题的溢出,而不是在每个步骤检查乘法溢出或硬编码最大允许参数?
没有。
一般来说,如何处理涉及多个迭代/递归阶段的问题,这些阶段可能会在每个阶段悄悄失败,而不会通过引入昂贵的检查来影响性能?
我认为你不能在Java中做到这一点。
一些机器指令集如果前一个整数算术操作溢出,则设置“溢出”位,但大多数编程语言都没有提供利用它的方法。 C#是一个例外,Ada也是(如果我没记错的话)。

Stephen,看我的回答。C#是一种主流语言,极大地简化了处理溢出情况的操作。 - Dilum Ranatunga

0
自从Java 8以来,Java就有了Math.multiplyExact()方法。这个方法会在内部检查溢出情况。由于该方法是一个'intrinsic' ( 在此处查看列表),如果可能的话,Java实现将被专用的低级机器指令所替代,可能只需检查CPU溢出标志,因此检查速度非常快。
顺便说一句,注意到在JDK 1.8.0_40上,这似乎比JDK 1.0.8_05快得多。

-1
在Java中,溢出应该会给你一个负数结果,因此你可以将其用作检查:
long factorial(int n)
{
    //handle special cases like negatives, etc.

    long p = 1;
    for(int i = 1; i <= n; i++)
    {
        p = p * n;
    }

    if (p < 0)
    {
        throw new ArithmeticException("factorial: Overflow");
    }

    return p;
}

或者,对于具有正溢出的语言,由于n!>(n-1)!,您可以尝试:

long factorial(int n)
{
    //handle special cases like negatives, etc.

    if (n == 1 || n == 2) { return n; }

    // Calculate (n-1)!
    long p = 2;
    for(int i = 3; i < n; i++)  // Note changes to loop start and end.
    {
        p = p * n;
    }

    long previous = p;  // (n-1)!

    p = p * n;  // Calculate n!

    if (p < previous)
    {
        throw new ArithmeticException("factorial: Overflow");
    }
    return p;
}

这些方法每个阶乘只需要一次检查。


如果你继续进行下去,溢出后可能会得到 p > 0,因此在过程结束时检查 p < 0 并不是一个真正的解决方案。 - Calimo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接