簡體   English   中英

如何防止我的Ackerman函數溢出堆棧?

[英]How can I prevent my Ackerman function from overflowing the stack?

有沒有辦法讓我的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有時是。

擴展這一點,我們可以將其轉換為迭代形式,同時只需處理跟蹤m先前值,以及我們將以遞歸形式返回的位置,我們以迭代形式分配n 一旦我們用完m等待處理,我們返回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;
    }

有了這個版本,我可以在Ackermann(4, 2) == BigInteger.Pow(2, 65536) - 3之后得到一個true的結果一秒鍾之后(Mono,發布版本,在Core i7上運行)。 鑒於非作弊版本在返回m這些值的正確結果時是一致的,我認為這是前一版本正確性的合理證據,但我會讓它繼續運行並看到。

編輯:當然,我並不是真的希望以前版本能夠在任何合理的時間范圍內返回,但我認為無論如何我都會讓它繼續運行,看看它的內存使用情況如何。 6個小時后,它的坐姿遠低於40MiB。 我很高興雖然顯然不切實際,但如果在真機上有足夠的時間,它確實會回歸。

編輯:顯然,有人認為Stack<T>達到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)返回:

在此輸入圖像描述

哪個是正確的結果。 使用的堆棧結構永遠不會拋出,因此剩余的唯一限制是堆(當然,當有足夠大的輸入時,您必須使用“Universe life”作為度量單位...)。

由於它的使用方式類似於圖靈機的磁帶,我們想到的是,任何可計算的功能都可以在足夠大小的圖靈機上計算。

使用memoization。 就像是:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM