1. 程式人生 > >如何利用迴圈代替遞迴以防止棧溢位(譯)

如何利用迴圈代替遞迴以防止棧溢位(譯)

摘要:我們經常會用到遞迴函式,但是如果遞迴深度太大時,往往導致棧溢位。而遞迴深度往往不太容易把握,所以比較安全一點的做法就是:用迴圈代替遞迴。文章最後的原文裡面講了如何用10步實現這個過程,相當精彩。本文翻譯了這篇文章,並加了自己的一點註釋和理解。

目錄

  1.  簡介
  2. 模擬函式的目的
  3. 遞迴和模擬函式的優缺點
  4. 用棧和迴圈代替遞迴的10個步驟
  5. 替代過程的幾個簡單例子
  6. 更多的例子
  7. 結論
  8. 參考
  9. 協議

1 簡介

  一般我們在進行排序(比如歸併排序)或者樹操作時會用到遞迴函式。但是如果遞迴深度達到一定程度以後,就會出現意想不到的結果比如堆疊溢位。雖然有很多有經驗的開發者都知道了如何用迴圈函式

或者棧加while迴圈來代替遞迴函式,以防止棧溢位,但我還是想分享一下這些方法,這或許會對初學者有很大的幫助。

2 模擬函式的目的

  如果你正在使用遞迴函式,並且沒有控制遞迴呼叫,而棧資源又比較有限,呼叫層次過深的時候就可能導致棧溢位/堆衝突模擬函式的目的就是在堆中開闢區域來模擬棧的行為,這樣你就能控制記憶體分配和流處理,從而避免棧溢位。如果能用迴圈函式來代替效果會更好,這是一個比較需要時間和經驗來處理的事情,出於這些原因,這篇文章為初學者提供了一個簡單的參考,怎樣使用迴圈函式來替代遞迴函式,以防止棧溢位?

3 遞迴函式和模擬函式的優缺點

  遞迴函式:

  優點:演算法比較直觀。可以參考文章後面的例子

  缺點:可能導致棧溢位或者堆衝突

  你可以試試執行下面兩個函式(後面的一個例子),IsEvenNumber(遞迴實現)IsEvenNumber(模擬實現),他們在標頭檔案"MutualRecursion.h"中宣告。你可以將傳入引數設定為10000,像下面這樣:

#include "MutualRecursion.h" 

bool result = IsEvenNumberLoop(10000); // 成功返回

bool result2 = IsEvenNumber(10000);     // 會發生堆疊溢位

 有些人可能會問:如果我增加棧的容量不就可以避免棧溢位嗎?好吧,這只是暫時的解決問題的辦法,如果呼叫層次越來越深,很有可能會再次發生溢位。

   模擬函式:

  優點:能避免棧溢位或者堆衝突錯誤,能對過程和記憶體進行更好的控制

  缺點:演算法不是太直觀,程式碼難以維護

4 用棧和迴圈代替遞迴的10個步驟

第一步

定義一個新的結構體Snapshot,用於儲存遞迴結構中的一些資料和狀態資訊

Snapshot內部需要包含的變數有以下幾種:

  A 一般當遞迴函式呼叫自身時,函式引數會發生變化。所以你需要包含變化的引數,引用除外比如下面的例子中,引數n應該包含在結構體中,而retVal不需要。

void SomeFunc(int n, int &retVal);

  B 階段性變數"stage"(通常是一個用來轉換到另一個處理分支的整形變數),詳見第六條規則

  C 函式呼叫返回以後還需要繼續使用的區域性變數(一般在二分遞迴和巢狀遞迴中很常見)

程式碼:

 1 // Recursive Function "First rule" example
 2 int SomeFunc(int n, int &retIdx)
 3 {
 4    ...
 5    if(n>0)
 6    {
 7       int test = SomeFunc(n-1, retIdx);
 8       test--;
 9       ...
10       return test;
11    }
12    ...
13    return 0;
14 }
15 
16 
17 // Conversion to Iterative Function
18 int SomeFuncLoop(int n, int &retIdx)
19 {
20     // (First rule)
21     struct SnapShotStruct {
22        int n;        // - parameter input
23        int test;     // - local variable that will be used 
24                      //     after returning from the function call
25                      // - retIdx can be ignored since it is a reference.
26        int stage;    // - Since there is process needed to be done 
27                      //     after recursive call. (Sixth rule)
28     };
29     ...
30 }
View Code

 第二

在函式的開頭建立一個區域性變數,這個值扮演了遞迴函式的返回函式角色。它相當於為每次遞迴呼叫儲存一個臨時值,因為C++函式只能有一種返回型別,如果遞迴函式的返回型別是void,你可以忽略這個區域性變數。如果有預設的返回值,就應該用預設值初始化這個區域性變數。

 1 // Recursive Function "Second rule" example
 2 int SomeFunc(int n, int &retIdx)
 3 {
 4    ...
 5    if(n>0)
 6    {
 7       int test = SomeFunc(n-1, retIdx);
 8       test--;
 9       ...
10       return test;
11    }
12    ...
13    return 0;
14 }
15 
16 // Conversion to Iterative Function
17 int SomeFuncLoop(int n, int &retIdx)
18 {
19      // (First rule)
20     struct SnapShotStruct {
21        int n;        // - parameter input
22        int test;     // - local variable that will be used 
23                      //     after returning from the function call
24                      // - retIdx can be ignored since it is a reference.
25        int stage;    // - Since there is process needed to be done 
26                      //     after recursive call. (Sixth rule)
27     };
28 
29     // (Second rule)
30     int retVal = 0;  // initialize with default returning value
31 
32     ...
33     // (Second rule)
34     return retVal;
35 }
View Code

 第三

建立一個棧用於儲存“Snapshot”結構體型別變數

 1 // Recursive Function "Third rule" example
 2 
 3 // Conversion to Iterative Function
 4 int SomeFuncLoop(int n, int &retIdx)
 5 {
 6      // (First rule)
 7     struct SnapShotStruct {
 8        int n;        // - parameter input
 9        int test;     // - local variable that will be used 
10                      //     after returning from the function call
11                      // - retIdx can be ignored since it is a reference.
12        int stage;    // - Since there is process needed to be done 
13                      //     after recursive call. (Sixth rule)
14     };
15 
16     // (Second rule)
17     int retVal = 0;  // initialize with default returning value
18 
19     // (Third rule)
20     stack<SnapShotStruct> snapshotStack;
21     ...
22     // (Second rule)
23     return retVal;
24 }
View Code

 第四

建立一個新的”Snapshot”例項,然後將其中的引數等初始化,並將“Snapshot”例項壓入棧

 1 // Recursive Function "Fourth rule" example
 2 
 3 // Conversion to Iterative Function
 4 int SomeFuncLoop(int n, int &retIdx)
 5 {
 6      // (First rule)
 7     struct SnapShotStruct {
 8        int n;        // - parameter input
 9        int test;     // - local variable that will be used 
10                      //     after returning from the function call
11                      // - retIdx can be ignored since it is a reference.
12        int stage;    // - Since there is process needed to be done 
13                      //     after recursive call. (Sixth rule)
14     };
15 
16     // (Second rule)
17     int retVal = 0;  // initialize with default returning value
18 
19     // (Third rule)
20     stack<SnapShotStruct> snapshotStack;
21 
22     // (Fourth rule)
23     SnapShotStruct currentSnapshot;
24     currentSnapshot.n= n;          // set the value as parameter value
25     currentSnapshot.test=0;        // set the value as default value
26     currentSnapshot.stage=0;       // set the value as initial stage
27 
28     snapshotStack.push(currentSnapshot);
29 
30     ...
31     // (Second rule)
32     return retVal;
33 }
View Code

 第五

寫一個while迴圈,使其不斷執行直到棧為空。while迴圈的每一次迭代過程中,彈出”Snapshot“物件。

 1 // Recursive Function "Fifth rule" example
 2 
 3 // Conversion to Iterative Function
 4 int SomeFuncLoop(int n, int &retIdx)
 5 {
 6      // (First rule)
 7     struct SnapShotStruct {
 8        int n;        // - parameter input
 9        int test;     // - local variable that will be used 
10                      //     after returning from the function call
11                      // - retIdx can be ignored since it is a reference.
12        int stage;    // - Since there is process needed to be done 
13                      //     after recursive call. (Sixth rule)
14     };
15     // (Second rule)
16     int retVal = 0;  // initialize with default returning value
17     // (Third rule)
18     stack<SnapShotStruct> snapshotStack;
19     // (Fourth rule)
20     SnapShotStruct currentSnapshot;
21     currentSnapshot.n= n;          // set the value as parameter value
22     currentSnapshot.test=0;        // set the value as default value
23     currentSnapshot.stage=0;       // set the value as initial stage
24     snapshotStack.push(currentSnapshot);
25     // (Fifth rule)
26     while(!snapshotStack.empty())
27     {
28        currentSnapshot=snapshotStack.top();
29        snapshotStack.pop();
30        ...
31     }
32     // (Second rule)
33     return retVal;
34 }
View Code

 第六

  1. 將當前階段一分為二(針對當前只有單一遞迴呼叫的情形)。第一個階段代表了下一次遞迴呼叫之前的情況,第二階段代表了下一次遞迴呼叫完成並返回之後的情況(返回值已經被儲存,並在此之前被累加)
  2. 如果當前階段有兩次遞迴呼叫,就必須分為3個階段。階段1:第一次呼叫返回之前,階段2:階段1執行的呼叫過程。階段3:第二次呼叫返回之前。
  3. 如果當前階段有三次遞迴呼叫,就必須至少分為4個階段。
  4. 依次類推。
 1 // Recursive Function "Sixth rule" example
 2 int SomeFunc(int n, int &retIdx)
 3 {
 4    ...
 5    if(n>0)
 6    {
 7       int test = SomeFunc(n-1, retIdx);
 8       test--;
 9       ...
10       return test;
11    }
12    ...
13    return 0;
14 }
15 
16 // Conversion to Iterative Function
17 int SomeFuncLoop(int n, int &retIdx)
18 {
19      // (First rule)
20     struct SnapShotStruct {
21        int n;        // - parameter input
22        int test;     // - local variable that will be used 
23                      //     after returning from the function call
24                      // - retIdx can be ignored since it is a reference.
25        int stage;    // - Since there is process needed to be done 
26                      //     after recursive call. (Sixth rule)
27     };
28     // (Second rule)
29     int retVal = 0;  // initialize with default returning value
30     // (Third rule)
31     stack<SnapShotStruct> snapshotStack;
32     // (Fourth rule)
33     SnapShotStruct currentSnapshot;
34     currentSnapshot.n= n;          // set the value as parameter value
35     currentSnapshot.test=0;        // set the value as default value
36     currentSnapshot.stage=0;       // set the value as initial stage
37     snapshotStack.push(currentSnapshot);
38     // (Fifth rule)
39     while(!snapshotStack.empty())
40     {
41        currentSnapshot=snapshotStack.top();
42        snapshotStack.pop();
43        // (Sixth rule)
44        switch( currentSnapshot.stage)
45        {
46        case 0:
47           ...      // before ( SomeFunc(n-1, retIdx); )
48           break; 
49        case 1: 
50           ...      // after ( SomeFunc(n-1, retIdx); )
51           break;
52        }
53     }
54     // (Second rule)
55     return retVal;
56 }
View Code

 第七

根據階段變數stage的值切換到相應的處理流程並處理相關過程。

 1 // Recursive Function "Seventh rule" example
 2 int SomeFunc(int n, int &retIdx)
 3 {
 4    ...
 5    if(n>0)
 6    {
 7       int test = SomeFunc(n-1, retIdx);
 8       test--;
 9       ...
10       return test;
11    }
12    ...
13    return 0;
14 }
15 
16 // Conversion to Iterative Function
17 int SomeFuncLoop(int n, int &retIdx)
18 {
19      // (First rule)
20     struct SnapShotStruct {
21        int n;        // - parameter input
22        int test;     // - local variable that will be used 
23                      //     after returning from the function call
24                      // - retIdx can be ignored since it is a reference.
25        int stage;    // - Since there is process needed to be done 
26                      //     after recursive call. (Sixth rule)
27     };
28 
29     // (Second rule)
30     int retVal = 0;  // initialize with default returning value
31 
32     // (Third rule)
33     stack<SnapShotStruct> snapshotStack;
34 
35     // (Fourth rule)
36     SnapShotStruct currentSnapshot;
37     currentSnapshot.n= n;          // set the value as parameter value
38     currentSnapshot.test=0;        // set the value as default value
39     currentSnapshot.stage=0;       // set the value as initial stage
40 
41     snapshotStack.push(currentSnapshot);
42 
43     // (Fifth rule)
44     while(!snapshotStack.empty())
45     {
46        currentSnapshot=snapshotStack.top();
47        snapshotStack.pop();
48 
49        // (Sixth rule)
50        switch( currentSnapshot.stage)
51        {
52        case 0:
53           // (Seventh rule)
54           if( currentSnapshot.n>0 )
55           {
56              ...
57           }
58           ...
59           break; 
60        case 1: 
61           // (Seventh rule)
62           currentSnapshot.test = retVal;
63           currentSnapshot.test--;
64           ...
65           break;
66        }
67     }
68     // (Second rule)
69     return retVal;
70 }
View Code

 第八

如果遞迴有返回值,將這個值儲存下來放在臨時變數裡面,比如retVal當迴圈結束時,這個臨時變數的值就是整個遞迴處理的結果。

 1 // Recursive Function "Eighth rule" example
 2 int SomeFunc(int n, int &retIdx)
 3 {
 4    ...
 5    if(n>0)
 6    {
 7       int test = SomeFunc(n-1, retIdx);
 8       test--;
 9       ...
10       return test;
11    }
12    ...
13    return 0;
14 }
15 
16 // Conversion to Iterative Function
17 int SomeFuncLoop(int n, int &retIdx)
18 {
19      // (First rule)
20     struct SnapShotStruct {
21        int n;        // - parameter input
22        int test;     // - local variable that will be used 
23                      //     after returning from the function call
24                      // - retIdx can be ignored since it is a reference.
25        int stage;    // - Since there is process needed to be done 
26                      //     after recursive call. (Sixth rule)
27     };
28     // (Second rule)
29     int retVal = 0;  // initialize with default returning value
30     // (Third rule)
31     stack<SnapShotStruct> snapshotStack;
32     // (Fourth rule)
33     SnapShotStruct currentSnapshot;
34     currentSnapshot.n= n;          // set the value as parameter value
35     currentSnapshot.test=0;        // set the value as default value
36     currentSnapshot.stage=0;       // set the value as initial stage
37     snapshotStack.push(currentSnapshot);
38     // (Fifth rule)
39     while(!snapshotStack.empty())
40     {
41        currentSnapshot=snapshotStack.top();
42        snapshotStack.pop();
43        // (Sixth rule)
44        switch( currentSnapshot.stage)
45        {
46        case 0:
47           // (Seventh rule)
48           if( currentSnapshot.n>0 )
49           {
50              ...
51           }
52           ...
53           // (Eighth rule)
54           retVal = 0 ;
55           ...
56           break; 
57        case 1: 
58           // (Seventh rule)
59           currentSnapshot.test = retVal;
60           currentSnapshot.test--;
61           ...
62           // (Eighth rule)
63           retVal = currentSnapshot.test;
64           ...
65           break;
66        }
67     }
68     // (Second rule)
69     return retVal;
70 }
View Code

 第九

如果遞迴函式有“

相關推薦

如何利用迴圈代替防止溢位()

摘要:我們經常會用到遞迴函式,但是如果遞迴深度太大時,往往導致棧溢位。而遞迴深度往往不太容易把握,所以比較安全一點的做法就是:用迴圈代替遞迴。文章最後的原文裡面講了如何用10步實現這個過程,相當精彩。本文翻譯了這篇文章,並加了自己的一點註釋和理解。 目錄  簡介

呼叫中溢位原因

那麼過多的遞迴呼叫為什麼會引起棧溢位呢?事實上,函式呼叫的引數是通過棧空間來傳遞的,在呼叫過程中會佔用執行緒的棧資源。而遞迴呼叫,只有走到最後的結束點後函式才能依次退出,而未到達最後的結束點之前,佔用的棧空間一直沒有釋放,如果遞迴呼叫次數過多,就可能導致佔用的棧資源超過執

關於迴圈的一些思考

有一類問題,可以歸結為如下形式: 已知兩個條件:1,f(n) 和 f(n-1) 的遞推關係式; 2,某個f(x) 的具體值。求解:f(n)。 跟高中的某一類數列問題一模一樣... 這個問題有兩條思路: a,正向推導:根據 f(x) 可以求解 f(x+1)的值,再求f(x+2),...

js實現,尾優化),防止溢位

一、一版的遞迴實現 n!,比如 5!= 5 * 4 * 3 * 2 *1       function fact(n) {             if(n == 1) {

今天為大家整理了十張動圖GIFS,有助於認識迴圈、二分檢索等概念的具體執行情況。程式碼例項Python語言編寫。

一、迴圈 GIF 1:最簡單的 while 迴圈 ​ GIF 2:帶 if/else 的迴圈 二、遞迴 GIF 3:遞迴概念的直接演示 GIF 4:遞迴的程式碼示例 GIF 5:遞迴求斐波那契數列 GIF 6:遞迴求階乘(圖裡縮排有點問題,請忽

二叉樹遍歷理解——及非方法中利用

1.二叉樹介紹 二叉樹是每個節點最多有兩個子樹的樹結構,遍歷方法有深度優先(包括:先序、中序、後序遍歷)和寬度優先(層序遍歷),層序遍歷通過佇列可以實現。這裡主要介紹深度優先遍歷的方法以及其中棧的應用,幫助理解二叉樹的結構、遞迴和非遞迴中棧的應用。程式pyth

逆序

import java.util.Stack; class Solution {     private static void reverse(Stack<Integer> s)

用表儲存代替演算法

我們知道遞迴演算法非常低效,低效的原因在於遞迴的過程會產生冗餘計算。 拿我們熟悉的斐波那契數列為例,計算公式為:F(n) = F(n - 1) + F(n - 2),其中F(0) = F(1) = 1。 例如計算F(5)的執行過程: 在此過程中,F(4) 執行了1次;F(3)執行了2次;F(2)執行

python中,迴圈舉例

python中,迴圈與遞迴舉例,包括階乘、計算和等。 1、計算階乘:5! 1)迴圈方法計算 # 迴圈方法計算階乘:5! def fact1(n): i = 1 result = 1 while i <= n: result = r

SQL Server 利用WITH AS獲取層級關係資料

WITH AS短語,也叫做子查詢部分(subquery factoring),在SQL Server 2005中提供了一種解決方案,這就是公用表表達式(CTE),使用CTE,可以使SQL語句的可維護性,同時,CTE要比表變數的效率高得多。     下面是CTE的語法:

僅使用函式和操作逆序一個

題目 一個棧依次壓入1,2,3, 4, 5,那麼從棧頂到棧底分別為5, 4, 3, 2, 1。將這個棧轉置後,從棧頂到棧底為1,2,3, 4, 5,也就是實現棧中元素的逆序,但是隻能使用遞迴函式來實現,不能使用其他資料結構。 解題參考和一些坑 共兩個遞迴函式來實現逆序: 第一個函

二叉樹遍歷(迴圈

遞迴 1.前序遍歷 void preorder(BinTree *T) { if(T==NULL) return; cout << T->data; preorder(T->left); preorder(T->rig

——全排列和n皇后問題舉例

筆記來自【晴神寶典】 一、遞迴 遞迴 就在於反覆呼叫自身函式,但是每次都把問題範圍縮小,直到範圍可以縮小到可以直接得到邊界資料的結果,然後在返回路上求出對應的解。以上可看出,遞迴很適合用來實現分治思想。 遞迴兩個很重要的組成組成: 1、遞迴邊界(出口); 2、遞迴式

函式對記憶體的使用

遞迴函式由於在運算中,重複的遞迴呼叫自身,函式的區域性變數所佔用的記憶體空間持續增長並不會被釋放,導致區域性變數所佔用的棧記憶體可用空間越來越少,當遞迴呼叫深度達到一定量級,就可能會使得棧記憶體空間不足,導致記憶體分配失敗。所以當設計遞迴函式的時候要注意遞迴的深度所可能達到的上限,避免該

程式設計3:僅用函式和操作逆序一個

<?php header("content-type:text/html;charset=utf-8"); /* * 僅用遞迴函式和棧操作逆序一個棧 P8 */ function getAndRemoveLastElement(SplStack $stack){ if($stack-

如何僅用函式和操作逆序一個——你要先用stack實現,再去改成——需要對理解很深刻才能寫出來

/** * 如何僅用遞迴函式和棧操作逆序一個棧 * 題目: * 一個棧依次壓入1,2,3,4,5,那麼從棧頂到棧底分別為5,4,3,2,1。 * 將這個棧轉置後,從棧頂到棧底為1,2,3,4,5,也就是實現棧中元素的逆序, * 但是隻能用遞迴函式來實現,不能用

用mysql儲存過程代替查詢 用mysql儲存過程代替查詢

用mysql儲存過程代替遞迴查詢 查詢此表某個id=4028ab535e370cd7015e37835f52014b(公司1)下的所有資料 正常情況下,我們採用遞迴演算法查詢,如下 1

javascript分別用for迴圈計算不死神兔

function getSum(n) {   var n1 = 1;  //初始化兩個月的兔子個數   var n2 = 1;   var sum = 1;  //定義一個累加和 ,如果傳遞的是1或者2,預設值為1   for(var i = 3; i <= n; i++) {   sum =

使用函式實現的逆序, 不使用其他資料結構 stack reverse

* Stack.php <?php /** * Created by PhpStorm. * User: Mch * Date: 9/24/18 * Time: 9:30 AM */ namespace ds\stack; class Stack ext

Java 刷題 -- 最小元素/構造佇列/逆序/排序)(左程雲面試指南)

返回棧中最小元素 package abc; import java.util.Stack; /** * 實現一個特殊的棧,在實現基本功能的基礎上,返回棧中最小元素的操作 * pop push getMin時間複雜度都是O(1) * 設計的棧型別可以