Java 数学中的StackOverflower错误。随机递归方法中的随机

Java 数学中的StackOverflower错误。随机递归方法中的随机,java,algorithm,Java,Algorithm,这是我节目的背景 一个函数有50%的几率什么都不做,50%的几率调用自己两次。 该计划完成的可能性有多大 我写了这段代码,显然效果很好。答案可能不是每个人都清楚的是,这个项目有100%的机会完成。但是当我运行这个程序时,在Math.Random()中出现了一个StackOverflower错误(多么方便)。有人能告诉我它是从哪里来的,如果我的代码可能是错的,告诉我吗 static int bestDepth =0; static int numberOfPrograms =0; @Test pu

这是我节目的背景

一个函数有50%的几率什么都不做,50%的几率调用自己两次。 该计划完成的可能性有多大

我写了这段代码,显然效果很好。答案可能不是每个人都清楚的是,这个项目有100%的机会完成。但是当我运行这个程序时,在Math.Random()中出现了一个StackOverflower错误(多么方便)。有人能告诉我它是从哪里来的,如果我的代码可能是错的,告诉我吗

static int bestDepth =0;
static int numberOfPrograms =0;
@Test
public void testProba(){
   for(int i = 0; i <1000; i++){
       long time = System.currentTimeMillis();
       bestDepth = 0;
       numberOfPrograms = 0;
       loop(0);
       LOGGER.info("Best depth:"+ bestDepth +" in "+(System.currentTimeMillis()-time)+"ms");
   }
}

public boolean loop(int depth){
    numberOfPrograms++;
    if(depth> bestDepth){
        bestDepth = depth;
    }
    if(proba()){
        return true;
    }
    else{
        return loop(depth + 1) && loop(depth + 1);
    }
}

public boolean proba(){
    return Math.random()>0.5;
}
。 我怀疑堆栈和其中的函数数量是有限的,但我没有真正看到这里的问题

任何建议或线索显然都是受欢迎的

法比安


编辑:谢谢你的回答,我用java-Xss4m运行了它,效果很好。

递归的缺点是它开始填满堆栈,如果递归太深,最终会导致堆栈溢出。如果要确保测试结束,可以使用以下Stackoverflow线程中给出的答案增加堆栈大小:


无论何时调用函数或创建非静态变量,堆栈都用于为其放置和保留空间

现在,您似乎正在递归调用
循环
函数。这会将参数连同代码段和返回地址一起放在堆栈中。这意味着大量信息被放置在堆栈上

但是,堆栈是有限的。CPU具有内置机制,可以防止数据被推入堆栈时出现问题,并最终覆盖代码本身(随着堆栈的增长)。这称为
一般保护故障
。当发生一般保护故障时,操作系统会通知当前正在运行的任务。因此,启动
堆栈溢出

这似乎发生在
Math.random()


为了解决您的问题,我建议您使用
Java
的-Xss选项增加堆栈大小,正如您所说,
循环
函数递归地调用自身。现在,可以由编译器重写为循环,而不占用任何堆栈空间(这称为尾部调用优化,TCO)。不幸的是,java编译器并没有做到这一点。而且您的
循环
也不是尾部递归的。您的选择如下:

  • 按照其他答案的建议,增加堆栈大小。请注意,这将进一步推迟问题的时间:无论堆栈有多大,其大小仍然是有限的。您只需要更长的递归调用链就可以突破空间限制
  • 根据循环重写函数
  • 使用具有执行TCO的编译器的
  • 您仍然需要将函数重写为尾部递归
  • 或者重写它(只需要细微的更改)。一篇好的论文,解释蹦床并进一步概括它们,叫做“蹦床”
  • 为了说明3.2中的要点,以下是重写函数的外观:

    def loop(depth: Int): Trampoline[Boolean] = {
      numberOfPrograms = numberOfPrograms + 1
      if(depth > bestDepth) {
        bestDepth = depth
      }
      if(proba()) done(true)
      else for {
        r1 <- loop(depth + 1)
        r2 <- loop(depth + 1)
      } yield r1 && r2
    }
    
    def循环(深度:Int):蹦床[布尔]={
    numberOfPrograms=numberOfPrograms+1
    如果(深度>最佳深度){
    最佳深度=深度
    }
    如果(proba())完成(true)
    其他的{
    
    r1增加堆栈大小是一个很好的临时解决方案。但是,正如所证明的,尽管
    loop()
    函数保证最终返回,
    loop()所需的平均堆栈深度
    是无限的。因此,无论堆栈增加多少,程序最终都会耗尽内存并崩溃

    我们无法确切地防止这种情况发生;我们总是需要以某种方式在内存中对堆栈进行编码,我们永远不会拥有无限的内存。然而,有一种方法可以将您使用的内存量减少大约2个数量级。这将使您的程序返回的可能性大大提高,而不是比撞车更可怕

    我们可以注意到,在堆栈的每一层上,运行程序实际上只需要一条信息:告诉我们是否需要调用
    loop()
    返回后是否重试。因此,我们可以使用位堆栈模拟递归。每个模拟堆栈帧只需要一个内存(现在需要64-96倍,具体取决于您是以32位还是64位运行)

    代码看起来是这样的(尽管我现在没有Java编译器,所以无法测试它):

    static int bestDepth=0;
    静态int numLoopCalls=0;
    公共空间{
    //我们的假堆栈。当堆栈上的这一点需要第二次调用loop()时,我们将推送1,如果不需要,则推送0
    BitSet fakeStack=新位集();
    长电流深度=0;
    numLoopCalls=0;
    而(currentDepth>=0)
    {
    numLoopCalls++;
    if(proba()){
    //从当前函数“return”,向上调用堆栈,直到到达需要第二次“call loop()”的点
    fakeStack.clear(当前深度);
    而(!fakeStack.get(currentDepth))
    {
    当前深度--;
    如果(电流深度<0)
    {
    返回;
    }
    }
    //现在,我们已经到了需要再次调用loop()的时候了。
    //将它标记为已调用,然后调用它
    fakeStack.clear(当前深度);
    currentDepth++;
    }
    否则{
    //需要调用loop()两次,所以我们按1并继续while循环
    fakeStack.set(当前深度);
    currentDepth++;
    如果(当前深度>最佳深度)
    {
    最佳深度=当前深度;
    }
    }
    }
    }
    
    这可能会稍微慢一点,但它将使用大约百分之一
    def loop(depth: Int): Trampoline[Boolean] = {
      numberOfPrograms = numberOfPrograms + 1
      if(depth > bestDepth) {
        bestDepth = depth
      }
      if(proba()) done(true)
      else for {
        r1 <- loop(depth + 1)
        r2 <- loop(depth + 1)
      } yield r1 && r2
    }
    
    static int bestDepth = 0;
    static int numLoopCalls = 0;
    
    public void emulateLoop() {
        //Our fake stack.  We'll push a 1 when this point on the stack needs a second call to loop() made yet, a 0 if it doesn't
        BitSet fakeStack = new BitSet();
        long currentDepth = 0;
        numLoopCalls = 0;
    
        while(currentDepth >= 0)
        {
            numLoopCalls++;
    
            if(proba()) {
                //"return" from the current function, going up the callstack until we hit a point that we need to "call loop()"" a second time
                fakeStack.clear(currentDepth);
                while(!fakeStack.get(currentDepth))
                {
                    currentDepth--;
                    if(currentDepth < 0)
                    {
                        return;
                    }
                }
    
                //At this point, we've hit a point where loop() needs to be called a second time.
                //Mark it as called, and call it
                fakeStack.clear(currentDepth);
                currentDepth++;
            }
            else {
                //Need to call loop() twice, so we push a 1 and continue the while-loop
                fakeStack.set(currentDepth);
                currentDepth++;
                if(currentDepth > bestDepth)
                {
                    bestDepth = currentDepth;
                }
            }
        }
    }