算法:五步教你消除递归
背景
递归对于分析问题比较有优势,但是基于递归的实现效率就不高了,而且因为函数栈大小的限制,递归的层次也有限制。本文给出一种通用的消除递归的步骤,这样您可以在分析阶段采用递归思想,而实现阶段采用非递归算法。
函数的调用过程
函数的调用是基于栈,每次调用都涉及如下操作:
- 调用开始时:将返回地址和局部变量入栈。
- 调用结束时:出栈并将返回到入栈时的返回地址。
使用堆中分配的栈消除递归
递归版本
代码
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 }
备注
搞企业应用的应该用不到这种消除递归的算法,不过学完以后对递归的理解也更清晰了。