C#、入門チュートリアル (41) - 再帰アルゴリズムと再帰アルゴリズムの非再帰実装、スタック オーバーフロー例外 (スタック オーバーフロー例外) の完全な解決策

この記事 (著者: Duoke Document Management System) では、階乗計算ハノイの塔の問題フィボナッチ数列、およびツリーの深さ優先探索(DFS、深さ優先検索) 待機などを含むがこれらに限定されない、再帰アルゴリズムとその適用シナリオについて簡単に紹介します。再帰アルゴリズムにはスタック オーバーフロー例外(System.StackOverflowexception)が発生することが多いという事実を考慮して、このペーパーでは、ツリーの深さ優先トラバーサル アルゴリズムの非再帰実装についても詳しく紹介し、完全な C# ソース プログラム フレームワークを提供します。

1 再帰アルゴリズム

再帰的アルゴリズムは、関数が自分自身を呼び出す単純なプログラミング方法です。

原理: コードは中間結果を独立したスタック空間に保存し、後続の計算プロセスで後入れ先出しの順にポップアップし、中間結果を使用して、最終的にアルゴリズムを完成させます。

1.1 再帰アルゴリズムとは?

再帰的アルゴリズムとは、問題を同じ種類の部分問題に繰り返し分解することによって問題を解決する計算方法を指します。再帰アルゴリズムは、多くのコンピューター サイエンスの問題を解決するために使用できるため、コンピューター サイエンスにおいて非常に重要な概念です。ほとんどのプログラミング言語は関数の自己呼び出しをサポートしており、関数は自分自身を呼び出すことで再帰計算を実行できます。一般的なコンピューティング理論では、再帰がループを完全に置き換えることができることが証明されているため、多くの関数型プログラミング言語では、再帰を使用してループを実装するのが通例です。

再帰アルゴリズムの利点: シンプルなロジックとシンプルなコード。

再帰アルゴリズムの致命的な欠陥:再帰アルゴリズムは途中の計算結果をスタックに保存するため、多くのレベルの再帰は「スタック オーバーフロー エラー、例外 System.StackOverflowexception をスローする」を引き起こします。

例外を解決する従来の方法は数多くありますが、究極の方法は、再帰を非再帰に置き換えることです。

1.2 再帰アルゴリズムの適用シナリオ

再帰アルゴリズムは、(1) 基本的な科学計算 (実験的プログラム)、再帰アルゴリズムの理解と学習、(2) ツリー (Tree) トラバーサル、(3) 有向グラフまたはグラフ (Graph) トラバーサル、( 4) 多くのソリューションに使用されます。ハノイタワー問題などのサブ問題に分割できる操作。

1.3 再帰アルゴリズムの C# の例

再帰を使用して実装された科学計算のいくつかの例を以下に示します。

1.3.1 factorial のオリジナル アルゴリズムの C# ソース コード

階乗1808年にクリスチャン・クランプ(1760~1826)によって発明された演算記号で、数学用語です。

正の整数の階乗は、その数以下のすべての正の整数の積であり、0 の階乗は 1 です。自然数 n の階乗は n! と書きます。1808 年にクリスチャン・カーマンがこの表記法を導入しました。

つまり、n! = 1×2×3×...×(n-1)×n です。

階乗は再帰的に定義することもできます: 0!=1, n!=(n-1)!×n.

/// <summary>
/// 阶乘的递归算法 x!
/// </summary>
/// <param name="x"></param>
/// <returns></returns>
public static long Factor(int x)
{
    // 终止条件
    if (x == 1) return 1;
    // 递归调用
    return x * Factor(x - 1);
}

このようなアルゴリズムは、教科書や問題集にしか登場せず、実用化には問題があります。

核となる問題は、 long は限られたサイズの数しか格納できないこと、そして 20 を超える階乗は計算できないことです! ! ! !

以下は、再帰計算の各ステップの結果を追跡するコードです。


#if DEBUG
        static StringBuilder sb = new StringBuilder();
#endif

        /// <summary>
        /// 阶乘的递归算法 x!
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        public static long Factor(int x)
        {
#if ORIGINAL
            // 书上代码
            // 终止条件
            if (x == 1) return 1;
            // 递归调用
            return x * Factor(x - 1);
#elif DEBUG
            // 测试代码
            long r = (x <= 1) ? 1 : x * Factor(x - 1);
            sb.AppendLine("x = " + x + ", f(x) = " + r + "<br>");
            return r;
#else
            // 发行代码
            return (x <= 1) ? 1 : x * Factor(x - 1);
#endif
        }

#if DEBUG
        public void Factor_Driver()
        {
            Factor(36);
            webBrowser1.DocumentText = sb.ToString();
        }
#endif

これが計算36です!各ステップの計算結果(中間結果)は計算20で見ることができます!その後、間違ったデータが表示されました。

longが表現できる値を超えています!

x = 1, f(x) = 1
x = 2, f(x) = 2
x = 3, f(x) = 6
x = 4, f(x) = 24
x = 5, f(x) = 120
x = 6, f(x) = 720
x = 7, f(x) = 5040
x = 8, f(x) = 40320
x = 9, f(x) = 362880
x = 10, f(x) = 3628800
x = 11, f(x) = 39916800
x = 12, f(x) = 479001600
x = 13, f(x) = 6227020800
x = 14, f(x) = 87178291200
x = 15, f(x) = 1307674368000
x = 16, f(x) = 20922789888000
x = 17, f(x) = 355687428096000
x = 18, f(x) = 6402373705728000
x = 19, f(x) = 121645100408832000
x = 20, f(x) = 2432902008176640000
x = 21, f(x) = -4249290049419214848 // error begin...
x = 22, f(x) = -1250660718674968576
x = 23, f(x) = 8128291617894825984
x = 24, f(x) = -7835185981329244160
x = 25, f(x) = 7034535277573963776
x = 26, f(x) = -1569523520172457984
x = 27, f(x) = -5483646897237262336
x = 28, f(x) = -5968160532966932480
x = 29, f(x) = -7055958792655077376
x = 30, f(x) = -8764578968847253504
x = 31, f(x) = 4999213071378415616
x = 32, f(x) = -6045878379276664832
x = 33, f(x) = 3400198294675128320
x = 34, f(x) = 4926277576697053184
x = 35, f(x) = 6399018521010896896
x = 36, f(x) = 9003737871877668864

より大きな値の階乗については、「大数クラス」を使用する必要があります。私のブログを検索して、C# ソース コードを取得できます。

1.3.2 ハノイの塔(ハノイの塔)問題

ハノイの塔とも呼ばれるハノイの、古代インドの伝説に端を発する知育玩具です。

伝説とルール:ブラフマーが世界を創造したとき、彼は 3 つのダイヤモンドの柱を作り、1 つの柱に 64 個の金の円盤を下から上にサイズの順に積み重ねました。ブラフマーはバラモンに、別の柱のディスクを下からサイズの順に並べ替えるように命じました。また、ディスクは小さなディスクで拡大することはできず、3 つの柱の間で一度に移動できるディスクは 1 つだけであると規定されています。

ハノイの塔の移動ステップ数を計算するプログラム:

/// <summary>
/// 汉诺塔计算移动次数
/// </summary>
/// <param name="disk">圆盘数量</param>
/// <returns></returns>
public static long Hanoi(int disk)
{
    return (disk == 1) ? 1 : Hanoi(disk - 1) * 2 + 1;
}

実験手順:

private void btnTest_Click(object sender, EventArgs e)
{
    for (int i = 1; i <= 32; i++)
    {
        sb.AppendLine("disk=" + i + " " + Hanoi(i) + " moves<br>");
    }
    webBrowser1.DocumentText = sb.ToString();
}

計算結果:

disk=1 1 moves
disk=2 3 moves
disk=3 7 moves
disk=4 15 moves
disk=5 31 moves
disk=6 63 moves
disk=7 127 moves
disk=8 255 moves
disk=9 511 moves
disk=10 1023 moves
disk=11 2047 moves
disk=12 4095 moves
disk=13 8191 moves
disk=14 16383 moves
disk=15 32767 moves
disk=16 65535 moves
disk=17 131071 moves
disk=18 262143 moves
disk=19 524287 moves
disk=20 1048575 moves
disk=21 2097151 moves
disk=22 4194303 moves
disk=23 8388607 moves
disk=24 16777215 moves
disk=25 33554431 moves
disk=26 67108863 moves
disk=27 134217727 moves
disk=28 268435455 moves
disk=29 536870911 moves
disk=30 1073741823 moves
disk=31 2147483647 moves
disk=32 4294967295 moves

もちろん、各ステップの動きも表示したいので、ソースプログラムを次のように変更します。

移動の各ステップの再帰アルゴリズム ソース プログラムを記録します。

#if DEBUG
        static StringBuilder sb = new StringBuilder();
#endif
        static int step = 1;

        /// <summary>
        /// 用字母显示 A B C 柱子上圆盘的移动
        /// </summary>
        /// <param name="disk"></param>
        /// <param name="up"></param>
        /// <param name="down"></param>
        public static void Hanoi_Move(int disk, char up, char down)
        {
            sb.AppendLine("<font color=blue>step:" + String.Format("{0:D4}", step++) + " </font>:<font color=darkgreen> move disk <font color='red'> " + disk + " </font> from " + up + " to " + down + " </font><br>");
        }

        /// <summary>
        /// 汉诺塔的圆盘移动算法
        /// </summary>
        /// <param name="n">圆盘数量</param>
        /// <param name="a">A柱名称</param>
        /// <param name="b">B柱名称</param>
        /// <param name="c">C柱名称</param>
        public static void Hanoi(int n, char a = 'A', char b = 'B', char c = 'C')
        {
            if (n == 1)
            {
                Hanoi_Move(1, a, c);
            }
            else
            {
                // 将A柱中的N-1层通过C柱,转移到B
                Hanoi(n - 1, a, c, b);
                // 将A柱中的最底层转移到C柱
                Hanoi_Move(n, a, c);
                // 将B柱中原本的N-1层,通过A柱转移到C柱上
                Hanoi(n - 1, b, a, c);
            }
        }

移行:

private void btnTest_Click(object sender, EventArgs e)
{
    Hanoi(6, 'A', 'B', 'C');
    webBrowser1.DocumentText = sb.ToString();
}

移動の各ステップの結果 (グラフィック表示に使用できる Hanoi_Move 関数を変更します):

step:0001 : move disk 1 from A to B 
step:0002 : move disk 2 from A to C 
step:0003 : move disk 1 from B to C 
step:0004 : move disk 3 from A to B 
step:0005 : move disk 1 from C to A 
step:0006 : move disk 2 from C to B 
step:0007 : move disk 1 from A to B 
step:0008 : move disk 4 from A to C 
step:0009 : move disk 1 from B to C 
step:0010 : move disk 2 from B to A 
step:0011 : move disk 1 from C to A 
step:0012 : move disk 3 from B to C 
step:0013 : move disk 1 from A to B 
step:0014 : move disk 2 from A to C 
step:0015 : move disk 1 from B to C 
step:0016 : move disk 5 from A to B 
step:0017 : move disk 1 from C to A 
step:0018 : move disk 2 from C to B 
step:0019 : move disk 1 from A to B 
step:0020 : move disk 3 from C to A 
step:0021 : move disk 1 from B to C 
step:0022 : move disk 2 from B to A 
step:0023 : move disk 1 from C to A 
step:0024 : move disk 4 from C to B 
step:0025 : move disk 1 from A to B 
step:0026 : move disk 2 from A to C 
step:0027 : move disk 1 from B to C 
step:0028 : move disk 3 from A to B 
step:0029 : move disk 1 from C to A 
step:0030 : move disk 2 from C to B 
step:0031 : move disk 1 from A to B 
step:0032 : move disk 6 from A to C 
step:0033 : move disk 1 from B to C 
step:0034 : move disk 2 from B to A 
step:0035 : move disk 1 from C to A 
step:0036 : move disk 3 from B to C 
step:0037 : move disk 1 from A to B 
step:0038 : move disk 2 from A to C 
step:0039 : move disk 1 from B to C 
step:0040 : move disk 4 from B to A 
step:0041 : move disk 1 from C to A 
step:0042 : move disk 2 from C to B 
step:0043 : move disk 1 from A to B 
step:0044 : move disk 3 from C to A 
step:0045 : move disk 1 from B to C 
step:0046 : move disk 2 from B to A 
step:0047 : move disk 1 from C to A 
step:0048 : move disk 5 from B to C 
step:0049 : move disk 1 from A to B 
step:0050 : move disk 2 from A to C 
step:0051 : move disk 1 from B to C 
step:0052 : move disk 3 from A to B 
step:0053 : move disk 1 from C to A 
step:0054 : move disk 2 from C to B 
step:0055 : move disk 1 from A to B 
step:0056 : move disk 4 from A to C 
step:0057 : move disk 1 from B to C 
step:0058 : move disk 2 from B to A 
step:0059 : move disk 1 from C to A 
step:0060 : move disk 3 from B to C 
step:0061 : move disk 1 from A to B 
step:0062 : move disk 2 from A to C 
step:0063 : move disk 1 from B to C 

64階以上のハノイの塔問題も過剰なデータにつながります!

1.3.3 フィボナッチ数列の C# ソースプログラム

フィボナッチ (Leonardo Pisano, Fibonacci, Leonardo Bigollo, 1175-1250) は、中世のイタリアの数学者で、西側で最初にフィボナッチ数を研究し、現代の数と乗数を記述したヨーロッパで導入された表記法です。1202 年に書かれた彼の著書「The Book of Calculation」には、ギリシャ語、エジプト語、アラビア語、インド語、さらには中国の数学が数多く含まれています。

黄金分割数列としても知られるフィボナッチ数列は、数学者レオナルド・フィボナッチがウサギの飼育を例に紹介したことから、「ウサギ数列」とも呼ばれています。 、3、5、8、13、21、34、... 数学では、フィボナッチ数列は次のように再帰的に定義されます: F(0)= 0、F(1)=1、F(n)=F(n - 1)+F(n - 2) (n ≥ 2, n ∈ N*) 現代の物理学、準結晶構造、化学およびその他の分野では、Fi ボナッチ数列は直接適用されます. このため、アメリカ数学会は、は、1963 年から「Fibonacci Sequence Quarterly」という名前の数学雑誌を発行しており、この分野の研究成果を公開するために使用されています。

        /// <summary>
        /// 斐波那契数列
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        public static long Fibonacci(int x)
        {
#if ORIGINAL
            // 终止条件
            if (x == 1 || x == 2) return 1;
            // 递归调用
            return Fibonacci(x - 1) + Fibonacci(x - 2);
#else
            return (x == 1 || x == 2) ? 1 : (Fibonacci(x - 1) + Fibonacci(x - 2));
#endif
        }

1.3.4 树的深度优先遍历(DFS,Depth First Search)

树是最常用的数据结构之一,图可以理解为特殊的树。树的核心算法就是树的遍历,其中尤其以深度优先遍历居多。本文的思路稍微修改同样适用于广度优先遍历(BFS,Breadth First Search)。

深度优先遍历(DFS,Depth First Search)顾名思义就是尽可能先访问下一层(更深)节点。

广度优先遍历(BFS,Breadth First Search)则尽可能先访问本层(同深度)节点。

(1)通用的树(Tree)的数据结构(树节点的定义)的C#源程序

public class TreeNode
{
    /// <summary>
    /// 编码
    /// </summary>
    public int Id { get; set; } = 0;
    /// <summary>
    /// 内容
    /// </summary>
    public string Content { get; set; } = "";
    /// <summary>
    /// 父节点(非必须)
    /// </summary>
    public TreeNode Parent { get; set; } = null;
    /// <summary>
    /// 子节点(2个就是二叉树,8个就是八叉树)
    /// </summary>
    public List<TreeNode> Childrens { get; set; } = new List<TreeNode>();
    /// <summary>
    /// 默认构造函数
    /// </summary>
    public TreeNode() { }
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="id"></param>
    /// <param name="c"></param>
    public TreeNode(int id, string c) { Id = id; Content = c; }
}

(2)树(Tree)深度优先遍历DFS的C#源程序

/// <summary>
/// 深度优先遍历的代码框架
/// </summary>
/// <param name="nodes">所有根节点</param>
/// <param name="root">当前根节点</param>
public static void DFS(List<TreeNode> root_nodes, TreeNode root)
{
    foreach (TreeNode node in ((root == null) ? root_nodes : root.Childrens))
    {
        DFS(root_nodes, node);
    }

    {
        // do something for root
    }
}

(3)测试代码

/// <summary>
/// 测试代码
/// </summary>
public static void DFS_Driver()
{
    // 1 常见所有节点(含根节点)create nodes
    List<TreeNode> root_nodes = new List<TreeNode>();

    // 2 深度优先遍历 recursive calgorithm
    DFS(root_nodes, null);
}

深度优先遍历算法、广度优先遍历算法同样适用于 图(Graph)及其他数据结构,均可参照编写。

优先遍历算法、广度优先遍历是按遍历结果的顺序性需求选择的。

2 递归算法导致的堆栈溢出异常(Stack Overflow Exception)

堆栈溢出异常经常出现于有递归的C#应用程序。

2.1 出现堆栈溢出异常的原因

来自于 Microsoft 的解释与说明:

堆栈溢出的产生是由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级。

System.StackOverflowException

当执行堆栈溢出时,抛出此异常,这通常意味着递归出错。该代码有太多的嵌套方法调用。事情就是这样:这个异常是无法捕获的-至少从.NET 2.0起就没有-这意味着当抛出该异常时,您几乎没有其他选择。默认情况下,您的过程将被终止。

StackOverflowException 对于执行堆栈溢出错误,通常会引发非常深或未绑定的递归。 因此,请确保代码没有无限循环或无限递归。

您应该做的而不是捕获此异常的方法是改写代码,以防止它再次发生。

2.2 通过修改堆栈空间的方法不能完美地解决

互联网上大家经常能搜到的解决方案是,所谓的修改堆栈空间的方法,即:

在调用递归的地方新增一下一行代码:

var T = new Thread(delegate() { DFS(root_nodes, null); }, 1073741824); T.Start();

其中:DFS 即递归调用;1073741824 为堆栈空间(1024x1024x1024,1GB)。

实际上,如果出现堆栈溢出,这点空间远远不够!无法避免错误的再次出现。

因而,需要找到完美解决之道——递归算法的非递归实现。

3 递归算法的非递归实现

鉴于递归算法的堆栈溢出缺陷,需要将同样的算法改写为非递归实现。

这在C#语言中相当地简单。

3.1 非递归算法的核心思想

递归算法的汇编语言实现,就是依赖于“堆栈Stack”。

因而,非递归实现的思路就是自己管理堆栈空间。

3.2 递归算法的非递归实现C#实例

递归算法的非递归实现,需要对上述的代码做一些简单的修改。

3.2.1 适用于非递归实现的树数据结构定义C#源程序

public class TreeNode
{
    /// <summary>
    /// 编码
    /// </summary>
    public int Id { get; set; } = 0;
    /// <summary>
    /// 内容
    /// </summary>
    public string Content { get; set; } = "";
    /// <summary>
    /// 访问标识
    /// </summary>
    public bool Visited { get; set; } = false;
    /// <summary>
    /// 父节点(非必须)
    /// </summary>
    public TreeNode Parent { get; set; } = null;
    /// <summary>
    /// 子节点(2个就是二叉树,8个就是八叉树)
    /// </summary>
    public List<TreeNode> Childrens { get; set; } = new List<TreeNode>();
    /// <summary>
    /// 默认构造函数
    /// </summary>
    public TreeNode() { }
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="id"></param>
    /// <param name="c"></param>
    public TreeNode(int id, string c) { Id = id; Content = c; }
}

3.2.2 树深度优先遍历的非递归实现C#代码

深度优先遍历的非递归实现基于堆栈(Stack<T>)数据结构。

以TreeNode为数据集类型,替代了T。

/// <summary>
/// 递归算法的非递归实现
/// </summary>
/// <param name="root_nodes"></param>
private static void DFS_Unrecursive(List<TreeNode> root_nodes)
{
    Stack<TreeNode> stack = new Stack<TreeNode>();
    foreach (TreeNode bx in root_nodes)
    {
        bx.Visited = false;
        stack.Push(bx);
    }

    while (stack.Count > 0)
    {
        TreeNode root = stack.Peek();
        if (root.Childrens.Count > 0 && root.Visited == false)
        {
            foreach (TreeNode bx in root.Childrens)
            {
                bx.Visited = false;
                stack.Push(bx);
            }
            root.Visited = true;
        }
        else
        {
            root = stack.Pop();
            
            // do something for root node!

            root.Visited = false;
        }
    }
}

POWER BY TRUFFER.CN

商业化软件中极少使用简单的递归。

おすすめ

転載: blog.csdn.net/beijinghorn/article/details/128888513