【问题标题】:How can I prevent my Ackerman function from overflowing the stack?如何防止我的 Ackerman 函数溢出堆栈?
【发布时间】:2012-08-24 13:53:55
【问题描述】:

有没有办法让我的 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;
            }
        }

【问题讨论】:

标签: c# recursion stack-overflow


【解决方案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 有时是。

扩展这个,我们可以把它变成一个迭代形式,同时只需要处理跟踪m的先前值,并且我们将以递归形式返回,我们在迭代形式中分配给n。一旦我们用完等待处理的ms,我们就返回n的当前值:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
        Stack<BigInteger> stack = new Stack<BigInteger>();
        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<BigInteger> stack = new Stack<BigInteger>();
        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<BigInteger> stack = new Stack<BigInteger>();
        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;
    }

使用这个版本,我可以在一秒多后得到trueAckermann(4, 2) == BigInteger.Pow(2, 65536) - 3 的结果(单声道,发布版本,在Core i7 上运行)。鉴于非作弊版本在返回 m 此类值的正确结果方面是一致的,我将此作为前一版本正确性的合理证据,但我将让它运行并查看。

编辑:当然,我并不真的期望以前的版本会在任何合理的时间范围内返回,但我想我还是让它继续运行,看看它的内存使用情况如何。 6 小时后,它的大小正好低于 40MiB。我很高兴虽然显然不切实际,但如果在真机上有足够的时间,它确实会返回。

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

public class OverflowlessStack <T>
{
    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<BigInteger>();
    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) 返回:

这是正确的结果。使用的堆栈结构永远不会抛出,因此唯一剩下的限制是堆(当然还有时间,如果输入足够大,您将不得不使用“宇宙寿命”作为衡量单位......)。

由于它的使用方式类似于图灵机的磁带,我们想起了任何可计算函数都可以在足够大小的图灵机上计算的论点。

【讨论】:

  • 但是您仍然使用堆栈,它不是内置堆栈。问题是关于避免溢出堆栈,而不是避免StackOverflowException
  • 您将其用作堆栈。不管你怎么称呼它或如何实现它,你仍然在用另一个堆栈替换一个堆栈,只是为了避免内置堆栈。
  • 你在自相矛盾。您的解决方案不能解决溢出堆栈的问题,它只允许稍高的输入值。正如您在答案中所说的那样,它仍然会溢出,然后在尝试使您的解决方案听起来比实际更好时发生矛盾。
  • 你没有抓住重点。您只是用另一种堆栈替换一种堆栈,但它仍然会溢出。如果目标只是为了避免StackOverflowException,您可以将代码替换为throw new ApplicationException();,问题就解决了。
  • 加 1 表示“我们不必担心迅猛龙或 Dijkstra”
【解决方案2】:

使用记忆。比如:

private static Dictionary<int, int> a = new Dictionary<int, int>();

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 位。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-02-03
  • 2016-10-02
  • 2010-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多