C#,入门教程(41)——递归算法与递归算法的非递归实现,完美解决堆栈溢出异常问题(Stack Overflow Exception)

本文(作者:多可文档管理系统)简要介绍了递归算法及其应用场景,包括但不限于阶乘计算汉诺塔问题斐波那契数列树的深度优先遍历(DFS,Depth First Search)等。鉴于递归算法经常出现堆栈溢出异常问题(System.StackOverflowexception),本文还详细介绍了树深度优先遍历算法的非递归实现,并给出了完整的C#源程序框架。

1 递归算法 Recursive Algorithm

递归算法简单地说是函数调用自己的一种编程方法。

原理:代码将中间结果保存于独立的堆栈空间,并在后续的计算过程中按后进先出的顺序依次弹出、使用中间结果,最终完成了算法。

1.1 什么是递归算法?

递归算法(Recursion Algorithm)指一种通过重复将问题分解为同类的子问题而解决问题的计算方法。递归算法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身实现递归计算。通用计算理论证明:递归可完全取代循环,因此在很多函数编程语言中习惯用递归来实现循环。

递归算法的优势:逻辑简单,代码简单。

递归算法的致命缺陷递归算法通过堆栈保存中间计算结果,因而层级很多的递归会导致“堆栈溢出错误,抛出异常System.StackOverflowexception”。

虽然有不少常规的解决异常的方法,但终极方法还是用非递归代替递归。

1.2 递归算法的应用场景

递归算法常常用于:(1)基础的科学计算(实验性程序),理解与学习递归算法;(2)树(Tree)的遍历;(3)有向图或图(Graph)的遍历;(4)很多可拆分为子问题的求解与运算,比如汉诺塔问题(Hanoi Tower)。

1.3 递归算法的C#实例

下面给出一些使用递归实现的科学计算实例。

1.3.1 阶乘之原始算法的C#源代码

阶乘是 基斯顿·卡曼(Christian Kramp,1760~1826)于 1808 年发明的运算符号,是数学术语。

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且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 汉诺塔(河内塔)问题

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。

传说与规则:大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

计算汉诺塔移动步数的程序:

/// <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年的著作《计算之书》中包涵了许多希腊、埃及、阿拉伯、印度、甚至是中国数学相关内容。

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)在现代物理、准晶体结构、化学等领域,斐波那契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波那契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

        /// <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