遞歸轉循環的通用方法


轉載請注明出處:http://blog.csdn.net/tobewhatyouwanttobe/article/details/51180977

1.遞歸

定義:程序調用自身的編程技巧稱為遞歸。

棧與遞歸的關系:遞歸是借助於系統棧來實現的。每次遞歸調用,系統都要為該次調用分配一系列的棧空間用於存放此次調用的相關信息:返回地址,局部變量等。當調用完成時,就從棧空間內釋放這些單元,在該函數沒有完成前,分配的這些單元將一直保存着不被釋放。


2.遞歸轉化為循環

記得有人說過“所有遞歸都能轉化為循環”,某晚睡不着思考了一下這個問題,通過幾天的努力,查閱了一些資料,這里結合三個例子給出解決思路,歡迎討論。

既然系統是根據棧來實現遞歸的,我們也可以考慮模擬棧的行為來將遞歸轉化為循環,

比如二叉樹的中序遍歷代碼:


我們可以建個棧,棧保存每次遞歸的狀態(結構體record),包括所有局部變量的值。
現在有兩個問題:
1.怎樣模擬遞歸的調用,當前進入哪個遞歸環境。
2.怎樣保證棧中狀態出入棧的順序和遞歸的順序一致。


問題1:我們用一個record cur來記錄當前的遞歸環境,相當於每次遞歸的傳參,發生遞歸調用時改變cur相應的值。
問題2:我們將這個遞歸函數划分為(遞歸調用+1)種情況,如上圖划分為3種情況,state0:進入pos->le遞歸;state1:輸出當前點,進入pos->ri遞歸;state2:返回。當進入一個遞歸體時,遇到遞歸dg1,處理該state應該做的事,然后構造該遞歸狀態和state,入棧,再更新cur進入下一層。

因為cur進入下一層后的遞歸都要比dg1先執行(即cur進入下一層后的遞歸全部返回后才執行dg1),棧是先進后出,所以我們將該遞歸狀態入棧能夠保證第二點。如果一個調用結束了,就需要返回上一層,即本例的state2,作用相當於確定當前進入哪個遞歸環境,當前肯定是進入棧頂環境,直接將棧頂的記錄彈出,來更新cur即可。說的有點多,結合完整代碼看更好懂一些。


3.二叉樹遍歷代碼:


   
   
  
  
          
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <stack>
  4. #include <algorithm>
  5. using namespace std;
  6. struct node
  7. {
  8. int val;
  9. node *le,*ri;
  10. };
  11. struct record
  12. {
  13. node* a;
  14. int state;
  15. record(node* a, int state):a(a),state(state) {}
  16. };
  17. void non_recursive_inorder(node* root) //循環中序遍歷
  18. {
  19. stack<record> s;
  20. node* cur=root; //初始化狀態
  21. int state= 0;
  22. while( 1)
  23. {
  24. if(!cur) //如果遇到null結點,返回上一層 對應遞歸中的if(pos==NULL) return ;
  25. {
  26. if(s.empty()) break; //如果沒有上一層,退出循環
  27. cur=s.top().a;
  28. state=s.top().state; //返回上層狀態
  29. s.pop();
  30. }
  31. else if(state == 0) //state0,執行第一個遞歸inorder(cur->le);
  32. {
  33. s.push(record(cur, 1)); //保存本層狀態
  34. cur=cur->le; //更新到下層狀態
  35. state= 0;
  36. }
  37. else if(state == 1) //state1,執行print和inorder(cur->ri)
  38. {
  39. printf( "%d ",cur->val);
  40. s.push(record(cur, 2)); //保存本層狀態
  41. cur=cur->ri; //進入下層狀態
  42. state= 0;
  43. }
  44. else if(state == 2) //state2,函數結束,返回上層狀態
  45. {
  46. if(s.empty()) break; //初始結點的退出狀態,遍歷結束
  47. cur=s.top().a; //返回上層狀態
  48. state=s.top().state;
  49. s.pop();
  50. }
  51. }
  52. putchar( '\n');
  53. }
  54. void build(node **pos,int val) //建二叉樹
  55. {
  56. if(*pos== NULL)
  57. {
  58. *pos=(node *) malloc( sizeof(node));
  59. (*pos)->val=val;
  60. (*pos)->le=(*pos)->ri= NULL;
  61. return ;
  62. }
  63. if(val<(*pos)->val) build(&((*pos)->le),val);
  64. else if(val>(*pos)->val) build(&((*pos)->ri),val);
  65. }
  66. void Del(node *pos) //刪除二叉樹
  67. {
  68. if(pos== NULL) return;
  69. Del(pos->le);
  70. Del(pos->ri);
  71. free(pos);
  72. }
  73. void visit_mid(node *pos) //遞歸中序遍歷
  74. {
  75. if(pos== NULL) return ;
  76. visit_mid(pos->le);
  77. printf( "%d ",pos->val);
  78. visit_mid(pos->ri);
  79. }
  80. int main()
  81. {
  82. int i,n,x;
  83. while(~ scanf( "%d",&n))
  84. {
  85. node *root= NULL;
  86. for(i= 1; i<=n; i++)
  87. {
  88. scanf( "%d",&x);
  89. build(&root,x);
  90. }
  91. puts( "遞歸中序遍歷:");
  92. visit_mid(root);
  93. puts( "");
  94. puts( "循環中序遍歷:");
  95. non_recursive_inorder(root);
  96. Del(root);
  97. }
  98. return 0;
  99. }
  100. /*
  101. 9
  102. 5 2 1 4 3 8 6 7 9
  103. */

可以畫出遞歸的調用和返回的圖,手動模擬代碼運行,看每一步cur和棧里面的狀態時怎樣的,能夠幫助更好的理解這種思想。

若要將中序改成先序和后序,也只用改變代碼中的printf的位置即可,知道每個state要做什么事情就行。


4.快速排序:

快速排序是分治、遞歸經典應用,一般的寫法都是遞歸,因為其代碼簡單易懂,我們不妨也用上述思路來轉化為循環版本,因為遞歸的形式和二叉樹的遍歷基本一致,也只需划分為3種情況即可。

代碼:


   
   
  
  
          
  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstring>
  4. #include <stack>
  5. #define maxn 1005
  6. using namespace std;
  7. int a[maxn];
  8. struct record
  9. {
  10. int le,ri,state;
  11. record( int le= 0, int ri= 0, int state= 0):le(le),ri(ri),state(state){}
  12. };
  13. int Partition(int le,int ri) //划分
  14. {
  15. int tmp=a[le],pos=le;
  16. while(le<ri)
  17. {
  18. while(le<ri&&a[ri]>=tmp) ri--;
  19. a[pos]=a[ri];
  20. pos=ri;
  21. while(le<ri&&a[le]<=tmp) le++;
  22. a[pos]=a[le];
  23. pos=le;
  24. }
  25. a[pos]=tmp;
  26. return pos;
  27. }
  28. void Qsort(int le,int ri) //遞歸版本
  29. {
  30. if(le<ri)
  31. {
  32. int p=Partition(le,ri);
  33. Qsort(le,p -1);
  34. Qsort(p+ 1,ri);
  35. }
  36. }
  37. void QsortLoop(int n) //循環版本
  38. {
  39. int i;
  40. record cur(1,n,0),now;
  41. stack<record>s;
  42. while( 1)
  43. {
  44. //getchar();
  45. //printf("%d %d %d\n",cur.le,cur.ri,cur.state);
  46. if(cur.le<cur.ri)
  47. {
  48. if(cur.state== 0) //划分 向下遞歸 保存本層下次遞歸狀態
  49. {
  50. int p=Partition(cur.le,cur.ri);
  51. now.le=p+ 1; now.ri=cur.ri; now.state= 1;
  52. s.push(now);
  53. cur.ri=p -1;
  54. }
  55. else if(cur.state== 1) //向下遞歸 保存本層下次遞歸狀態
  56. {
  57. now=cur; now.state= 2;
  58. s.push(now);
  59. cur.state= 0;
  60. }
  61. else
  62. {
  63. if(s.empty()) break ; //棧內沒有節點退出
  64. cur=s.top();
  65. s.pop();
  66. }
  67. }
  68. else //遞歸返回
  69. {
  70. if(s.empty()) break ; //棧內沒有節點退出 不加這句可以試一試2 1 2
  71. cur=s.top();
  72. s.pop();
  73. }
  74. }
  75. }
  76. int main()
  77. {
  78. int i,n;
  79. while(~ scanf( "%d",&n))
  80. {
  81. for(i= 1;i<=n;i++) scanf( "%d",&a[i]);
  82. //Qsort(1,n);
  83. QsortLoop(n);
  84. for(i= 1;i<=n;i++)
  85. {
  86. printf( "%d ",a[i]);
  87. }
  88. puts( "");
  89. }
  90. return 0;
  91. }
  92. /*
  93. 5
  94. 1 2 3 4 5
  95. 7
  96. 5 10 8 6 3 20 2
  97. 6
  98. 64 5 3 45 6 78
  99. */

5.輸出1~n的全排列。

首先來看一下循環的版本,初始化vis為0,調用dfs(n)即可得到結果:


   
   
  
  
          
  1. int res[maxn]; //答案數組
  2. bool vis[maxn]; //標記數組
  3. void dfs(int pos) //當前進行到pos位置
  4. {
  5. if(pos==n+ 1) //為n+1則得到了一組答案 輸出
  6. {
  7. for( int i= 1; i<=n; i++)
  8. {
  9. printf( "%d ",res[i]);
  10. }
  11. puts( "");
  12. }
  13. for( int i= 1; i<=n; i++) //枚舉當前位置可以為幾
  14. {
  15. if(!vis[i]) //沒有使用過 當前位置可以為i
  16. {
  17. vis[i]= 1; //標記並設置當前位置
  18. res[pos]=i;
  19. printf( "vis[%d]=1 pos:%d=%d\n",i,pos,i);
  20. dfs(pos+ 1);
  21. printf( "vis[%d]=0\n",i);
  22. vis[i]= 0; //回溯
  23. }
  24. }
  25. }

這個遞歸就稍稍復雜一點了,因為一個遞歸體會產生幾次調用時不知道的,為了讓遞歸調用和返回清晰可見,我在遞歸調用前和遞歸調用返回后都加了一些輸出,也給循環版本增加了一點難度。

產生了兩個新問題:

(1)調用次數位置,不好划分狀態。

解決:可以根據當前循環變量i的值設置狀態state,因為根據i可以知道要進入的是哪個dfs,其實向下遞歸的狀態可以看做一個狀態的,可以統一化處理。

(2)狀態返回之后的事情怎么處理?

狀態的返回該方法的處理是取棧頂狀態,這樣就失去了返回的那個過程,返回之后的事情就不好處理了。我采用的方法是將返回之后的事情當做本層下一次遞歸調用之前的事情,每個狀態需要增加一個變量pre記錄本層上一次是調用的state是多少。

代碼:


   
   
  
  
          
  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstring>
  4. #include <stack>
  5. #define maxn 15
  6. typedef long long ll;
  7. using namespace std;
  8. #define FLAG 1
  9. //如果不想看見遞歸時的輸出 將1改為0
  10. int n;
  11. int res[maxn]; //答案數組
  12. bool vis[maxn]; //標記數組
  13. struct record
  14. {
  15. int pos,state,pre;
  16. record( int pos= 0, int state= 0, int pre= 0):pos(pos),state(state),pre(pre){}
  17. void show()
  18. {
  19. printf( "pos:%d state:%d pre:%d ",pos,state,pre);
  20. }
  21. };
  22. void show(stack<record> my)
  23. {
  24. while(!my.empty())
  25. {
  26. my.top().show();
  27. my.pop();
  28. }
  29. puts( "");
  30. }
  31. void debug(record cur,stack<record> s) //調試函數
  32. {
  33. int i;
  34. getchar();
  35. printf( "cur: ");
  36. cur.show(); puts( "");
  37. show(s);
  38. for(i= 1;i<=n;i++)
  39. {
  40. printf( "i:%d vis:%d ",i,vis[i]);
  41. }
  42. puts( "");
  43. for(i= 1;i<=n;i++)
  44. {
  45. printf( "i:%d res:%d ",i,res[i]);
  46. }
  47. puts( "");
  48. }
  49. void solve() //循環版本
  50. {
  51. int i;
  52. memset(vis, 0, sizeof(vis));
  53. stack<record> s;
  54. record cur(1,1,0),now;
  55. while( 1)
  56. {
  57. //debug(cur,s);
  58. if(cur.pos>n) // 當前位置大於n找到一組答案 輸出並返回上一層
  59. {
  60. for(i= 1;i<=n;i++)
  61. {
  62. printf( "%d ",res[i]);
  63. }
  64. puts( "");
  65. if(s.empty()) break; // 棧內沒有節點退出
  66. cur=s.top();
  67. s.pop();
  68. }
  69. else
  70. {
  71. if(cur.state<=n) //state1~n的情況
  72. {
  73. for(i=cur.state+ 1;i<=n;i++) // 找這一層下次進哪個dfs
  74. {
  75. if(!vis[i]) break;
  76. }
  77. if(i<=n) // 本層還要遞歸 向下遞歸 保存本層下一次遞歸
  78. {
  79. now=cur; now.state=i; now.pre=cur.state;
  80. vis[cur.pre]= 0; // 將本層的上次遞歸的vis清0
  81. #if FLAG
  82. if(cur.pre) printf( "vis[%d]=0\n",cur.pre);
  83. printf( "vis[%d]=1 pos:%d=%d\n",cur.state,cur.pos,cur.state);
  84. #endif
  85. vis[cur.state]= 1; // 標記並記錄答案
  86. res[cur.pos]=cur.state;
  87. s.push(now); // 本層下一次遞歸入棧
  88. cur.pos++; // cur更新為下一層狀態
  89. cur.pre= 0;
  90. for(i= 1;i<=n;i++) // 找下一層從哪個dfs開始
  91. {
  92. if(!vis[i]) break;
  93. }
  94. cur.state=i;
  95. }
  96. else // 該遞歸是本層最后一次遞歸 向下遞歸完后本層結束 返回上層
  97. {
  98. now=cur; now.state=n+ 1; now.pre=cur.state;
  99. vis[cur.pre]= 0; // 將本層的上次遞歸的vis清0
  100. #if FLAG
  101. if(cur.pre) printf( "vis[%d]=0\n",cur.pre);
  102. printf( "vis[%d]=1 pos:%d=%d\n",cur.state,cur.pos,cur.state);
  103. #endif // FLAG
  104. vis[cur.state]= 1;
  105. res[cur.pos]=cur.state;
  106. s.push(now);
  107. cur.pos++;
  108. cur.pre= 0;
  109. for(i= 1;i<=n;i++) // 找下一層從哪個dfs開始
  110. {
  111. if(!vis[i]) break;
  112. }
  113. cur.state=i;
  114. }
  115. }
  116. else
  117. {
  118. # if FLAG
  119. printf( "vis[%d]=0\n",cur.pre);
  120. #endif // FLAG
  121. if(s.empty()) break; // 棧內沒有節點退出
  122. vis[cur.pre]= 0;
  123. cur=s.top();
  124. s.pop();
  125. }
  126. }
  127. }
  128. }
  129. int main()
  130. {
  131. while(~ scanf( "%d",&n))
  132. {
  133. solve(); //循環版本
  134. }
  135. return 0;
  136. }
  137. /*
  138. 3
  139. */

ps:代碼增加了對應遞歸版本的中間輸出,如果不希望看到,這可以把FLAG置為0.

輸入3,運行可以得到如下結果,和遞歸版本的結果一模一樣,遞歸的調用和返回時做的事情也清晰可見。


   
   
  
  
          
  1. vis[1]=1 pos:1=1
  2. vis[2]=1 pos:2=2
  3. vis[3]=1 pos:3=3
  4. 1 2 3
  5. vis[3]=0
  6. vis[2]=0
  7. vis[3]=1 pos:2=3
  8. vis[2]=1 pos:3=2
  9. 1 3 2
  10. vis[2]=0
  11. vis[3]=0
  12. vis[1]=0
  13. vis[2]=1 pos:1=2
  14. vis[1]=1 pos:2=1
  15. vis[3]=1 pos:3=3
  16. 2 1 3
  17. vis[3]=0
  18. vis[1]=0
  19. vis[3]=1 pos:2=3
  20. vis[1]=1 pos:3=1
  21. 2 3 1
  22. vis[1]=0
  23. vis[3]=0
  24. vis[2]=0
  25. vis[3]=1 pos:1=3
  26. vis[1]=1 pos:2=1
  27. vis[2]=1 pos:3=2
  28. 3 1 2
  29. vis[2]=0
  30. vis[1]=0
  31. vis[2]=1 pos:2=2
  32. vis[1]=1 pos:3=1
  33. 3 2 1
  34. vis[1]=0
  35. vis[2]=0
  36. vis[3]=0

思路來源於: http://blog.csdn.net/biran007/article/details/4156351



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM