如何防止我的Ackerman函数溢出堆栈?

有没有办法让我的Ackerman函数不会创建一个堆栈而不是流量是相对较小的数字,即(4,2)。 这是错误

{无法计算表达式,因为当前线程处于堆栈溢出状态。}

private void Button1Click(object sender, EventArgs e) { var t = Ackermann(4,2); label1.Text += string.Format(": {0}", t); label1.Visible = true; } int Ackermann(uint m, uint n) { if (m == 0) return (int) (n+1); if (m > 0 && n == 0) return Ackermann(m - 1, 1); if (m > 0 && n > 0) return Ackermann(m - 1, (uint)Ackermann(m, n - 1)); else { return -1; } } 

避免StackOverflowException的最好方法是不使用堆栈。

让我们摆脱负面情况,因为当我们用uint打电话时它没有意义。 或者,如果我们在考虑其他可能性之前将负面测试作为方法中的第一件事,那么此后的内容也将起作用:

首先,我们需要一艘更大的船:

  public static BigInteger Ackermann(BigInteger m, BigInteger n) { if (m == 0) return n+1; if (n == 0) return Ackermann(m - 1, 1); else return Ackermann(m - 1, Ackermann(m, n - 1)); } 

现在,成功至少在数学上是可行的。 现在, n == 0情况是一个足够简单的尾调用。 让我们手动消除它。 我们将使用goto因为它是暂时的,所以我们不必担心velociraptors或Dijkstra:

  public static BigInteger Ackermann(BigInteger m, BigInteger n) { restart: if (m == 0) return n+1; if (n == 0) { m--; n = 1; goto restart; } else return Ackermann(m - 1, Ackermann(m, n - 1)); } 

这已经需要更长的时间来吹掉堆栈,但它会吹掉它。 但是,看一下这个表格,请注意m从不通过递归调用的返回来设置,而n有时是。

扩展这一点,我们可以将其转换为迭代forms,同时只需要处理跟踪m先前值,以及我们将以递归forms返回的位置,我们以迭代forms分配n 。 一旦我们用完m等待处理,我们返回n的当前值:

  public static BigInteger Ackermann(BigInteger m, BigInteger n) { Stack stack = new Stack(); stack.Push(m); while(stack.Count != 0) { m = stack.Pop(); if(m == 0) n = n + 1; else if(n == 0) { stack.Push(m - 1); n = 1; } else { stack.Push(m - 1); stack.Push(m); --n; } } return n; } 

在这一点上,我们已经回答了OP的问题。 这将需要很长时间才能运行,但它将返回所尝试的值(m = 4,n = 2)。 它永远不会抛出StackOverflowException ,尽管它会在mn某些值之上耗尽内存。

作为进一步的优化,我们可以跳过向堆栈添加一个值,只是在之后立即弹出:

  public static BigInteger Ackermann(BigInteger m, BigInteger n) { Stack stack = new Stack(); stack.Push(m); while(stack.Count != 0) { m = stack.Pop(); skipStack: if(m == 0) n = n + 1; else if(n == 0) { --m; n = 1; goto skipStack; } else { stack.Push(m - 1); --n; goto skipStack; } } return n; } 

这对堆栈没有帮助,也没有对堆有意义,但是考虑到循环的数量,这个东西会对大值进行处理,我们可以剃掉的每一点都是值得的。

在保持优化的同时消除goto仍然是读者的练习:)

顺便说一下,我对测试这个太不耐烦了,所以当m小于3时,我做了一个使用Ackerman函数的已知属性的作弊表:

  public static BigInteger Ackermann(BigInteger m, BigInteger n) { Stack stack = new Stack(); stack.Push(m); while(stack.Count != 0) { m = stack.Pop(); skipStack: if(m == 0) n = n + 1; else if(m == 1) n = n + 2; else if(m == 2) n = n * 2 + 3; else if(n == 0) { --m; n = 1; goto skipStack; } else { stack.Push(m - 1); --n; goto skipStack; } } return n; } 

有了这个版本,我可以得到Ackermann(4, 2) == BigInteger.Pow(2, 65536) - 3在一秒多一点之后(Mono,发布版本,在Core i7上运行)。 鉴于非作弊版本在返回m值的正确结果时是一致的,我认为这是前一版本正确性的合理证据,但我会让它继续运行并看到。

编辑:当然,我并不是真的希望以前版本能够在任何合理的时间范围内返回,但我认为无论如何我都会让它继续运行,看看它的内存使用情况如何。 6个小时后,它的坐在40MiB以下。 我很高兴虽然显然不切实际,如果在真机上有足够的时间,它确实会回归。

编辑:显然,有人认为Stack达到2³¹项目的内部限制也算作一种“堆栈溢出”。 如果我们必须,我们也可以处理:

 public class OverflowlessStack  { internal sealed class SinglyLinkedNode { //Larger the better, but we want to be low enough //to demonstrate the case where we overflow a node //and hence create another. private const int ArraySize = 2048; T [] _array; int _size; public SinglyLinkedNode Next; public SinglyLinkedNode() { _array = new T[ArraySize]; } public bool IsEmpty{ get{return _size == 0;} } public SinglyLinkedNode Push(T item) { if(_size == ArraySize - 1) { SinglyLinkedNode n = new SinglyLinkedNode(); n.Next = this; n.Push(item); return n; } _array [_size++] = item; return this; } public T Pop() { return _array[--_size]; } } private SinglyLinkedNode _head = new SinglyLinkedNode(); public T Pop () { T ret = _head.Pop(); if(_head.IsEmpty && _head.Next != null) _head = _head.Next; return ret; } public void Push (T item) { _head = _head.Push(item); } public bool IsEmpty { get { return _head.Next == null && _head.IsEmpty; } } } public static BigInteger Ackermann(BigInteger m, BigInteger n) { var stack = new OverflowlessStack(); stack.Push(m); while(!stack.IsEmpty) { m = stack.Pop(); skipStack: if(m == 0) n = n + 1; else if(m == 1) n = n + 2; else if(m == 2) n = n * 2 + 3; else if(n == 0) { --m; n = 1; goto skipStack; } else { stack.Push(m - 1); --n; goto skipStack; } } return n; } 

再次,致电Ackermann(4, 2)返回:

在此处输入图像描述

哪个是正确的结果。 使用的堆栈结构永远不会抛出,因此剩余的唯一限制是堆(当然,当有足够大的输入时,您必须使用“Universe life”作为度量单位…)。

由于它的使用方式类似于图灵机的磁带,我们想到的是,任何可计算的function都可以在足够大小的图灵机上计算。

使用memoization。 就像是:

 private static Dictionary a = new Dictionary(); private static int Pack(int m, int n) { return m * 1000 + n; } private static int Ackermann(int m, int n) { int x; if (!a.TryGetValue(Pack(m, n), out x)) { if (m == 0) { x = n + 1; } else if (m > 0 && n == 0) { x = Ackermann(m - 1, 1); } else if (m > 0 && n > 0) { x = Ackermann(m - 1, Ackermann(m, n - 1)); } else { x = -1; } a[Pack(m, n)] = x; } return x; } 

但是,这个例子只显示了这个概念,它仍然没有给出Ackermann(4,2)的正确结果,因为int太小而无法保存结果。 你需要一个65536位的整数,而不是32位。