アルゴリズム:5ステップで再帰を解消します

52761 ワード

背景
再帰は解析問題に有利であるが,再帰に基づく実現効率は高くなく,関数スタックサイズの制限により再帰の階層にも制限がある.本稿では、分析フェーズで再帰的な考え方を採用し、実装フェーズで非再帰的なアルゴリズムを採用することができるように、再帰的な除去の一般的な手順を示します.
関数の呼び出しプロセス
関数の呼び出しはスタックに基づいており、呼び出すたびに次の操作が行われます.
  • 呼び出し開始時:戻りアドレスとローカル変数がスタックに格納されます.
  • 呼び出し終了時:スタックを出て、スタックに入ったときの戻りアドレスに戻ります.

  • スタックに割り当てられたスタックを使用して再帰を除去する
    再帰バージョン
    コード#コード#
     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             }

    消去プロセス
    ステップ1:再帰バージョンのコードアドレスを識別する
  • の最初の代表:元のメソッド呼び出し.
  • 逆数の最初の代表:元のメソッド呼び出しが終了しました.
  • の2番目の代表:メソッド呼び出しエントリ.
  • の最後から2番目の代表:メソッド呼び出し出口.
  • 再帰バージョンの各再帰呼び出しは、コードアドレスを定義する.

  • 再帰的に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         }

    ステップ2:スタックフレームの定義
    スタックフレームはコード実行のコンテキストを表し、再帰バージョンのコードボディで使用されるローカル値タイプ変数をスタックフレームのメンバー変数として定義します.なぜ参照タイプは私が言わなくてもいいのか、また戻りアドレスメンバー変数を定義する必要があります.
    1         private class StackFrame
    2         {
    3             public int N;
    4             public int ReturnAddress;
    5         }

    ステップ3: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         }

    ステップ4: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         }

    ステップ5: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 }

    コメント
    企業の応用をするには、このような再帰を解消するアルゴリズムは使えないはずだが、勉強が終わってから再帰に対する理解も明らかになった.