在不使用BigInteger的情况下处理Java中的溢出 - java

假设我有一种方法可以从n个项目计算r个项目的组合:

    public static long combi(int n, int r) {

        if ( r == n) return 1;
        long numr = 1;
        for(int i=n; i > (n-r); i--) {
            numr *=i;

        }
        return numr/fact(r);

    }


public static long fact(int n) {

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

    }

如您所见,它涉及阶乘,很容易导致结果溢出。例如,如果我有foctorial方法的fact(200),我将得到零。问题是为什么我得到零?

其次,在上述情况下我该如何处理溢出?如果结果太大,该方法应返回尽可能大的数字以适合,而不是返回错误的答案。

一种方法(但这可能是错误的)是,如果结果超过某个较大的数字,例如1,400,000,000,则返回结果模的余数
1,400,000,001。您能否解释一下这意味着什么,以及如何在Java中做到这一点?

请注意,我不保证上述方法对于计算阶乘和组合是正确的。如果您发现错误并纠正错误,则可获得额外的奖励。

请注意,我只能使用int或long,如果不可避免,也可以使用double。不允许使用其他数据类型。

我不确定谁将此问题标记为作业。这不是功课。我希望这是家庭作业,我回到了大学的年轻学生。但是我已经有十多年的程序员年龄了。我只想练习用Java开发高度优化的解决方案。在我们大学时代,互联网甚至不存在。今天的学生很幸运,他们甚至可以像SO这样的网站上张贴作业。

参考方案

为了回答您的第一个问题(为什么会归零),由模块化算术计算出的fact()值等于全部64位归零!将您的事实代码更改为此:

public static long fact(int n) {
    long rs = 1;
    if( n <2) return 1;
    for (int i=2; i<=n; i++) {
        rs *=i;
        System.out.println(rs);
    }
    return rs;
}

看看输出!他们非常有趣。

现在进入第二个问题。

似乎您想为适合的longn值给出精确的整数(er,r)答案,如果不正确,则抛出异常。这是一个公平的练习。

为了正确地做到这一点,您根本不应该使用阶乘。诀窍是认识到C(n,r)可以通过添加项来递增计算。这可以通过带备忘录的递归来完成,也可以通过Stefan Kendall提到的乘法公式来完成。

当您将结果累积到将用于答案的long变量中时,请在每次添加后检查该值是否为负。当发生时,引发异常。如果它保持肯定,则可以安全地返回累积结果作为答案。

要了解为什么这样做有效,请考虑Pascal三角形

1
1  1
1  2   1
1  3   3   1
1  4   6   4   1
1  5  10  10   5  1
1  6  15  20  15  6  1

生成如下:

C(0,0) = 1 (base case)
C(1,0) = 1 (base case)
C(1,1) = 1 (base case) 
C(2,0) = 1 (base case) 
C(2,1) = C(1,0) + C(1,1) = 2
C(2,2) = 1 (base case)
C(3,0) = 1 (base case)
C(3,1) = C(2,0) + C(2,1) = 3
C(3,2) = C(2,1) + C(2,2) = 3
...

使用备注计算C(n,r)的值时,将递归调用的结果存储在适当的结构中,例如数组或哈希图,以将它们存储在其中。每个值是两个较小数字的总和。数字从小开始,总是正数。每当您计算新值(我们称其为子项)时,您都会添加较小的正数。从您的计算机组织类回想起,每当您添加两个模数正数时,当且仅当总和为负数时,才会发生溢出。在整个过程中只需要一个溢出就可以使您知道要查找的C(n,r)太大。

可以将这一论点转换为一个很好的归纳证明,但这可能是针对另一个任务,也许是另一个StackExchange网站。

附录

这是可以运行的完整应用程序。 (我还没有弄清楚如何使Java在键盘和ideone上运行)。

/**
 * A demo showing how to do combinations using recursion and memoization, while detecting
 * results that cannot fit in 64 bits.
 */
public class CombinationExample {

    /**
     * Returns the number of combinatios of r things out of n total.
     */
    public static long combi(int n, int r) {
        long[][] cache = new long[n + 1][n + 1];
        if (n < 0 || r > n) {
            throw new IllegalArgumentException("Nonsense args");
        }
        return c(n, r, cache);
    }

    /**
     * Recursive helper for combi.
     */
    private static long c(int n, int r, long[][] cache) {
        if (r == 0 || r == n) {
            return cache[n][r] = 1;
        } else if (cache[n][r] != 0) {
            return cache[n][r];
        } else {
            cache[n][r] = c(n-1, r-1, cache) + c(n-1, r, cache);
            if (cache[n][r] < 0) {
                throw new RuntimeException("Woops too big");
            }
            return cache[n][r];
        }
    }

    /**
     * Prints out a few example invocations.
     */
    public static void main(String[] args) {
        String[] data = ("0,0,3,1,4,4,5,2,10,0,10,10,10,4,9,7,70,8,295,100," +
                "34,88,-2,7,9,-1,90,0,90,1,90,2,90,3,90,8,90,24").split(",");
        for (int i = 0; i < data.length; i += 2) {
            int n = Integer.valueOf(data[i]);
            int r = Integer.valueOf(data[i + 1]);
            System.out.printf("C(%d,%d) = ", n, r);
            try {
                System.out.println(combi(n, r));
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

希望它是有用的。这只是一个快速的技巧,因此您可能需要对其进行清理...。还要注意,一个好的解决方案将使用适当的单元测试,尽管此代码确实给出了不错的输出。

JAVA:字节码和二进制有什么区别? - java

java字节代码(已编译的语言,也称为目标代码)与机器代码(当前计算机的本机代码)之间有什么区别?我读过一些书,他们将字节码称为二进制指令,但我不知道为什么。 参考方案 字节码是独立于平台的,在Windows中运行的编译器编译的字节码仍将在linux / unix / mac中运行。机器代码是特定于平台的,如果在Windows x86中编译,则它将仅在Win…

java:继承 - java

有哪些替代继承的方法? java大神给出的解决方案 有效的Java:偏重于继承而不是继承。 (这实际上也来自“四人帮”)。他提出的理由是,如果扩展类未明确设计为继承,则继承会引起很多不正常的副作用。例如,对super.someMethod()的任何调用都可以引导您通过未知代码的意外路径。取而代之的是,持有对本来应该扩展的类的引用,然后委托给它。这是与Eric…

Java:BigInteger,如何通过OutputStream编写它 - java

我想将BigInteger写入文件。做这个的最好方式是什么。当然,我想从输入流中读取(使用程序,而不是人工)。我必须使用ObjectOutputStream还是有更好的方法?目的是使用尽可能少的字节。谢谢马丁 参考方案 Java序列化(ObjectOutputStream / ObjectInputStream)是将对象序列化为八位字节序列的一种通用方法。但…

Java-如何将此字符串转换为日期? - java

我从服务器收到此消息,我不明白T和Z的含义,2012-08-24T09:59:59Z将此字符串转换为Date对象的正确SimpleDateFormat模式是什么? java大神给出的解决方案 这是ISO 8601标准。您可以使用SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM…

Java:从类中查找项目名称 - java

仅通过类的实例,如何使用Java反射或类似方法查找项目名称?如果不是,项目名称(我真正想要的是)可以找到程序包名称吗? 参考方案 项目只是IDE使用的简单组织工具,因此项目名称不是类或JVM中包含的信息。要获取软件包,请使用Class#getPackage()。然后,可以调用Package#getName()将包作为您在代码的包声明中看到的String来获取…