算法:五步教你消除遞歸


背景

遞歸對於分析問題比較有優勢,但是基於遞歸的實現效率就不高了,而且因為函數棧大小的限制,遞歸的層次也有限制。本文給出一種通用的消除遞歸的步驟,這樣您可以在分析階段采用遞歸思想,而實現階段采用非遞歸算法。

函數的調用過程

函數的調用是基於棧,每次調用都涉及如下操作:

  • 調用開始時:將返回地址和局部變量入棧。
  • 調用結束時:出棧並將返回到入棧時的返回地址。

使用堆中分配的棧消除遞歸

遞歸版本

代碼

 1         public static int Triangle(int n)
 2         {
 3             // 地址:2
 4             if (n == 1)
 5             {
 6                 // 地址:4
 7                 return n;
 8             }
 9 
10             /*   
11              *   地址:4  地址:3
12              *     /      /
13              *    /      /
14              *   /      /            */
15             return n + Triangle(n - 1);
16         }

非遞歸版本

代碼

 1         private class StackFrame
 2         {
 3             public int N;
 4             public int ReturnAddress;
 5         }
 6 
 7         public static int Triangle2(int n)
 8         {
 9             var stack = new Stack<StackFrame>();
10             var currentReturnValue = 0;
11             var currentAddress = 1;
12 
13             while (true)
14             {
15                 switch (currentAddress)
16                 {
17                     case 1:
18                         {
19                             stack.Push(new StackFrame
20                             {
21                                 N = n,
22                                 ReturnAddress = 5
23                             });
24                             currentAddress = 2;
25                         }
26                         break;
27                     case 2:
28                         {
29                             var frame = stack.Peek();
30                             if (frame.N == 1)
31                             {
32                                 currentReturnValue = 1;
33                                 currentAddress = 4;
34                             }
35                             else
36                             {
37                                 stack.Push(new StackFrame
38                                 {
39                                     N = frame.N - 1,
40                                     ReturnAddress = 3
41                                 });
42                                 currentAddress = 2;
43                             }
44                         }
45                         break;
46                     case 3:
47                         {
48                             var frame = stack.Peek();
49                             currentReturnValue = frame.N + currentReturnValue;
50                             currentAddress = 4;
51                         }
52                         break;
53                     case 4:
54                         {
55                             currentAddress = stack.Pop().ReturnAddress;
56                         }
57                         break;
58                     case 5:
59                         {
60                             return currentReturnValue;
61                         }
62                 }
63             }

消除過程

第一步:識別遞歸版本中的代碼地址

  • 第一個代表:原始方法調用。
  • 倒數第一個代表:原始方法調用結束。
  • 第二個代表:方法調用入口。
  • 倒數第二個代表:方法調用出口。
  • 遞歸版本中的每個遞歸調用定義一個代碼地址。

假如遞歸調用了 n 次,則代碼地址為:n + 4。

 1         public static int Triangle(int n)
 2         {
 3             // 地址:2
 4             if (n == 1)
 5             {
 6                 // 地址:4
 7                 return n;
 8             }
 9 
10             /*   
11              *   地址:4  地址:3
12              *     /      /
13              *    /      /
14              *   /      /            */
15             return n + Triangle(n - 1);
16         }

第二步:定義棧幀

棧幀代表了代碼執行的上下文,將遞歸版本代碼體中用到的局部值類型變量定義為棧幀的成員變量,為啥引用類型不用我就不多說了,另外還需要定義一個返回地址成員變量。

1         private class StackFrame
2         {
3             public int N;
4             public int ReturnAddress;
5         }

第三步:while 循環

在 while 循環之前聲明一個 stack、一個 currentReturnValue 和 currentAddress。

 1         public static int Triangle2(int n)
 2         {
 3             var stack = new Stack<StackFrame>();
 4             var currentReturnValue = 0;
 5             var currentAddress = 1;
 6 
 7             while (true)
 8             {
 9             }
10         }

第四步:switch 語句。

 1         public static int Triangle2(int n)
 2         {
 3             var stack = new Stack<StackFrame>();
 4             var currentReturnValue = 0;
 5             var currentAddress = 1;
 6 
 7             while (true)
 8             {
 9                 switch (currentAddress)
10                 {
11                     case 1:
12                         {
13                         }
14                         break;
15                     case 2:
16                         {
17                         }
18                         break;
19                     case 3:
20                         {
21                         }
22                         break;
23                     case 4:
24                         {
25                         }
26                         break;
27                     case 5:
28                         {
29                         }
30                 }
31             }
32         }

第五步:填充 case 代碼體。

將遞歸版本的代碼做如下變換:

  • 函數調用使用:stack.push(new StackFrame{...}); 和 currentAddress = 2;
  • 引用的局部變量變為,比如:n,變為:stack.Peek().n
  • return 語句變為:currentReturnValue = 1; 和 currentAddress = 4; 
  • 倒數第一個 case 代碼體為:return currentReturnValue;

最終的效果就是上面的示例。

漢諾塔練習

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 
  7 namespace DataStuctureStudy.Recursives
  8 {
  9     class HanoiTest
 10     {
 11         public static void Hanoi(int n, string source, string middle, string target)
 12         {
 13             if (n == 1)
 14             {
 15                 Console.WriteLine(String.Format("{0}->{1}", source, target));
 16             }
 17             else
 18             {
 19                 Hanoi(n - 1, source, target, middle);
 20                 Console.WriteLine(String.Format("{0}->{1}", source, target));
 21                 Hanoi(n - 1, middle, source, target);
 22             }
 23         }
 24 
 25         public static void Hanoi2(int n, string source, string middle, string target)
 26         {
 27             var stack = new Stack<StackFrame>();
 28             var currentAddress = 1;
 29 
 30             while (true)
 31             {
 32                 switch (currentAddress)
 33                 {
 34                     case 1:
 35                         {
 36                             stack.Push(new StackFrame
 37                             {
 38                                 N = n,
 39                                 Source = source,
 40                                 Middle = middle,
 41                                 Target = target,
 42                                 ReturnAddress = 5
 43                             });
 44                             currentAddress = 2;
 45                         }
 46                         break;
 47                     case 2:
 48                         {
 49                             var frame = stack.Peek();
 50                             if (frame.N == 1)
 51                             {
 52                                 Console.WriteLine(String.Format("{0}->{1}", frame.Source, frame.Target));
 53                                 currentAddress = 4;
 54                             }
 55                             else
 56                             {
 57                                 stack.Push(new StackFrame
 58                                 {
 59                                     N = frame.N - 1,
 60                                     Source = frame.Source,
 61                                     Middle = frame.Target,
 62                                     Target = frame.Middle,
 63                                     ReturnAddress = 3
 64                                 });
 65                                 currentAddress = 2;
 66                             }
 67                         }
 68                         break;
 69                     case 3:
 70                         {
 71                             var frame = stack.Peek();
 72                             Console.WriteLine(String.Format("{0}->{1}", frame.Source, frame.Target));
 73                             stack.Push(new StackFrame
 74                             {
 75                                 N = frame.N - 1,
 76                                 Source = frame.Middle,
 77                                 Middle = frame.Source,
 78                                 Target = frame.Target,
 79                                 ReturnAddress = 4
 80                             });
 81                             currentAddress = 2;
 82                         }
 83                         break;
 84                     case 4:
 85                         currentAddress = stack.Pop().ReturnAddress;
 86                         break;
 87                     case 5:
 88                         return;
 89                 }
 90             }
 91         }
 92 
 93         private class StackFrame
 94         {
 95             public int N;
 96             public string Source;
 97             public string Middle;
 98             public string Target;
 99             public int ReturnAddress;
100         }
101     }
102 }

二叉樹遍歷練習

這個練習是我之前采用的方式看,思想和上面的非常相似。

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 
  7 namespace DataStuctureStudy.Recursives
  8 {
  9     class TreeTest
 10     {
 11         public static void Test()
 12         {
 13             RecursiveTraverse(Node.BuildTree());
 14             StackTraverse(Node.BuildTree());
 15         }
 16 
 17         private class Node
 18         {
 19             public Node Left { get; set; }
 20 
 21             public Node Right { get; set; }
 22 
 23             public int Value { get; set; }
 24 
 25             public static Node BuildTree()
 26             {
 27                 return new Node
 28                 {
 29                     Value = 1,
 30                     Left = new Node
 31                     {
 32                         Value = 2,
 33                         Left = new Node
 34                         {
 35                             Value = 3
 36                         },
 37                         Right = new Node
 38                         {
 39                             Value = 4
 40                         }
 41                     },
 42                     Right = new Node
 43                     {
 44                         Value = 5,
 45                         Left = new Node
 46                         {
 47                             Value = 6
 48                         },
 49                         Right = new Node
 50                         {
 51                             Value = 7
 52                         }
 53                     }
 54                 };
 55             }
 56         }
 57 
 58         private static void RecursiveTraverse(Node node)
 59         {
 60             if (node == null)
 61             {
 62                 return;
 63             }
 64 
 65             RecursiveTraverse(node.Left);
 66             Console.WriteLine(node.Value);
 67             RecursiveTraverse(node.Right);
 68         }
 69 
 70         private enum CodeAddress
 71         {
 72             Start,
 73             AfterFirstRecursiveCall,
 74             AfterSecondRecursiveCall
 75         }
 76 
 77         private class StackFrame
 78         {
 79             public Node Node { get; set; }
 80 
 81             public CodeAddress CodeAddress { get; set; }
 82         }
 83 
 84         private static void StackTraverse(Node node)
 85         {
 86             var stack = new Stack<StackFrame>();
 87             stack.Push(new StackFrame
 88             {
 89                 Node = node,
 90                 CodeAddress = CodeAddress.Start
 91             });
 92 
 93             while (stack.Count > 0)
 94             {
 95                 var current = stack.Peek();
 96 
 97                 switch (current.CodeAddress)
 98                 {
 99                     case CodeAddress.Start:
100                         if (current.Node == null)
101                         {
102                             stack.Pop();
103                         }
104                         else
105                         {
106                             current.CodeAddress = CodeAddress.AfterFirstRecursiveCall;
107                             stack.Push(new StackFrame
108                             {
109                                 Node = current.Node.Left,
110                                 CodeAddress = CodeAddress.Start
111                             });
112                         }
113                         break;
114                     case CodeAddress.AfterFirstRecursiveCall:
115                         Console.WriteLine(current.Node.Value);
116 
117                         current.CodeAddress = CodeAddress.AfterSecondRecursiveCall;
118                         stack.Push(new StackFrame
119                         {
120                             Node = current.Node.Right,
121                             CodeAddress = CodeAddress.Start
122                         });
123                         break;
124                     case CodeAddress.AfterSecondRecursiveCall:
125                         stack.Pop();
126                         break;
127                 }
128             }
129         }
130     }
131 }

備注

搞企業應用的應該用不到這種消除遞歸的算法,不過學完以后對遞歸的理解也更清晰了。

 


免責聲明!

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



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