[英]How to avoid stack overflow of a recursive function?
例如,如果我們通過跟隨函數遍歷一個相當大的樹,我們可能會得到堆棧溢出。
void inorder(node* n)
{
if(n == null) return;
inorder(n->l);
n->print();
inorder(n->r);
}
如何在函數中添加條件或某些東西以防止這種溢出發生?
考慮迭代而不是遞歸,如果這真的是一個問題。
http://en.wikipedia.org/wiki/Tree_traversal
請參閱psedo代碼以進行迭代迭代迭代迭代預測迭代序列
基本上在while循環中使用自己的列表作為堆棧數據結構,可以有效地替換函數遞歸。
關於遞歸的事情是你永遠不能保證它永遠不會溢出堆棧, 除非你可以在(最小)內存大小和(最大)輸入大小上設置一些界限。 但是,如果你有一個無限循環,你可以做的是保證它會溢出堆棧......
我看到你的“if()回歸;” 終止條件,所以你應該避免無限循環,只要你的樹的每個分支都以null結尾。 因此,一種可能性是格式錯誤的輸入,其中樹的某些分支永遠不會達到空。 (例如,如果樹數據結構中有循環,則會發生這種情況。)
我看到的唯一另一種可能性是你的樹數據結構對於可用的堆棧內存量來說太大了。 (注意這是虛擬內存和交換空間可以使用,因此它不一定是RAM不足的問題。)如果是這種情況,您可能需要提出一些不遞歸的其他算法方法。 雖然你的函數占用的內存很小,但除非你省略了一些額外的處理,否則你的樹真的必須非常深刻才能成為一個問題。 (注意它的最大深度是一個問題,而不是節點的總數。)
如果你有一個非常大的樹,並且你遇到了使用遞歸遍歷來覆蓋堆棧的問題,那么問題很可能是你沒有一個平衡的樹。 第一個建議是嘗試平衡的二叉樹,例如紅黑樹或AVL樹,或每個節點有超過2個子節點的樹,例如B +樹。 C ++庫提供了std::map<>
和std::set<>
,它們通常使用平衡樹實現。
但是,避免遞歸有序遍歷的一種簡單方法是將樹修改為線程化。 也就是說,使用葉子節點的右指針指示下一個節點。 這種樹的遍歷看起來像這樣:
n = find_first(n);
while (! is_null(n)) {
n->print();
if (n->is_leaf()) n = n->r;
else n = find_first(n->r);
}
除了通過顯式管理堆棧替換遞歸(使用std::vector<Node*>
)之外,沒有可移植的解決方案。 非便攜式,您可以使用靜態變量跟蹤深度; 如果您知道最大堆棧大小以及每次遞歸所需的堆棧數,那么您可以檢查深度是否超過該值。
由於堆棧是動態分配的,因此很多系統(如Linux和Solaris)都無法預先了解最大堆棧深度。 但是,至少在Linux和Solaris下,一旦將內存分配給堆棧,它將保持分配並繼續受到堆棧的影響。 因此,您可以在程序開始時相當深入地進行遞歸(可能崩潰,但在完成任何操作之前),然后在以后檢查此值:
static char const* lowerBound = nullptr;
// At start-up...
void
preallocateStack( int currentCount ) {
{
char dummyToTakeSpace[1000];
-- currentCount;
if ( currentCount <= 0 ) {
lowerBound = dummyToTakeSpace;
} else {
preallocateStack( currentCount - 1 );
}
}
void
checkStack()
{
char dummyForAddress;
if ( &dummyForAddress < lowerBound ) {
throw std::bad_alloc(); // Or something more specific.
}
}
您會注意到在該代碼中存在一些未定義/未指定行為的情況,但我已經成功使用過幾次(在Sparc上的Solaris下,但是PC上的Linux在這方面完全相同)看待)。 事實上,它幾乎適用於任何系統: - 堆棧增長,以及 - 堆棧上分配局部變量。 因此,它也可以在Windows上運行,但如果它無法分配初始堆棧,則必須重新鏈接,而不是僅在盒子上的活動較少(或更改ulimits
)時運行程序(因為Windows上的堆棧大小在鏈接時固定)。
關於使用顯式堆棧的一條評論:某些系統(包括Linux,默認情況下)過度使用,這意味着在擴展std::vector<>
時,您無法可靠地獲得內存不足錯誤; 系統將告訴std::vector<>
存儲器在那里,然后在嘗試訪問它時給程序一個段違例。
您可以添加靜態變量以跟蹤調用函數的時間。 如果它接近您認為會使系統崩潰的內容,請執行一些例程以通知用戶該錯誤。
可以通過將另一個int變量與遞歸函數相關聯來進行更改的小原型。您可以將變量作為參數傳遞給函數,默認情況下在根處以零值開始,並在您沿着樹向下遞減時。 ..
缺點:這個解決方案的代價是為每個節點分配一個int變量的開銷。
void inorder(node* n,int counter)
{
if(counter<limit) //limit specified as per system limit
{
if(n == null) return;
inorder(n->l,counter-1);
n->print();
inorder(n->r,counter-1);
}
else
n->print();
}
考慮進一步研究:雖然如果只考慮遞歸,問題可能不是遍歷。 並且可以通過更好的樹創建和更新來避免。 如果沒有考慮過,請檢查平衡樹的概念。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.