zoukankan      html  css  js  c++  java
  • 算法:五步教你消除递归

    背景

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

    函数的调用过程

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

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

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

    递归版本

    代码

     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 }

    备注

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

  • 相关阅读:
    PNG文件格式具体解释
    opencv2对读书笔记——使用均值漂移算法查找物体
    Jackson的Json转换
    Java实现 蓝桥杯VIP 算法训练 装箱问题
    Java实现 蓝桥杯VIP 算法训练 装箱问题
    Java实现 蓝桥杯VIP 算法训练 单词接龙
    Java实现 蓝桥杯VIP 算法训练 单词接龙
    Java实现 蓝桥杯VIP 算法训练 方格取数
    Java实现 蓝桥杯VIP 算法训练 方格取数
    Java实现 蓝桥杯VIP 算法训练 单词接龙
  • 原文地址:https://www.cnblogs.com/happyframework/p/3485960.html
Copyright © 2011-2022 走看看