背景
遞歸對於分析問題比較有優勢,但是基於遞歸的實現效率就不高了,而且因為函數棧大小的限制,遞歸的層次也有限制。本文給出一種通用的消除遞歸的步驟,這樣您可以在分析階段采用遞歸思想,而實現階段采用非遞歸算法。
函數的調用過程
函數的調用是基於棧,每次調用都涉及如下操作:
- 調用開始時:將返回地址和局部變量入棧。
- 調用結束時:出棧並將返回到入棧時的返回地址。
使用堆中分配的棧消除遞歸
遞歸版本
代碼
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 }
備注
搞企業應用的應該用不到這種消除遞歸的算法,不過學完以后對遞歸的理解也更清晰了。