[英]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
,盡管它最終會超出某些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();
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.