算法:五步教你消除递归

背景

递归对于分析问题比较有优势,但是基于递归的实现效率就不高了,而且因为函数栈大小的限制,递归的层次也有限制。本文给出一种通用的消除递归的步骤,这样您可以在分析阶段采用递归思想,而实现阶段采用非递归算法。

函数的调用过程

函数的调用是基于栈,每次调用都涉及如下操作:

  • 调用开始时:将返回地址和局部变量入栈。
  • 调用结束时:出栈并将返回到入栈时的返回地址。

使用堆中分配的栈消除递归

递归版本

代码

 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 }

备注

搞企业应用的应该用不到这种消除递归的算法,不过学完以后对递归的理解也更清晰了。

 

posted on 2013-12-22 14:05  幸福框架  阅读(9573)  评论(6编辑  收藏  举报

导航

我要啦免费统计