递归和尾递归的实现逻辑

问题描述

我已经编写了用于对数组元素求和的代码

递归

 static int sum(int[] array)
        {

            if (array.Length == 1)
                return array[0];

            else
            {
                int[] newArr = new int[array.Length - 1];
                for (int i = 1; i < array.Length; i++)
                {
                    newArr[i - 1] = array[i];
                }

                return array[0] + sum(newArr);
            }
        }

和与

尾递归

 static int sumTR(int[] array,int sum)
        {
            //base case
            if (array.Length == 1)
                return array[0] + sum;
            else
            {
                  //tail recursive case
                int[] newArr = new int[array.Length - 1];

                for (int i = 1; i < array.Length; i++)
                    newArr[i - 1] = array[i];

               
                return sumTR(newArr,array[0] + sum);
            }
        }

据我所知,在尾递归中,基本方法不应该等待递归方法完成执行,也不应该依赖于它的输出。这种实现是实现这一目标的正确方法吗?

解决方法

据我所知,在尾递归中,基方法不应该等待递归方法完成执行,也不应该依赖其输出

这不太正确。尾递归主要使编译器能够应用 tail call optimization(如果支持),即将递归重写为常规循环。这具有减少堆栈中内存使用量的优点。它与“不等待”无关。

在第一个示例中,它必须为列表中的每个项目保留一个堆栈帧,如果您有一个很长的列表,则可能会耗尽堆栈内存并导致堆栈溢出。

在尾递归的情况下,当它到达尾调用时不再需要当前堆栈帧,因此每次调用都可以重新使用相同的堆栈帧,这应该导致代码排序等同于常规循环。

这个实现是实现这一目标的正确方法吗?

我觉得还不错。但这并不一定意味着会应用优化,它似乎取决于编译器版本,并且可能有其他要求。请参阅 Why doesn't .NET/C# optimize for tail-call recursion? 一般来说,我建议您依靠语言规范而不是编译器优化来确保程序的正确功能。

请注意,递归通常不是 C# 中的理想方法。对于简单的求和,使用常规循环更容易、更快、更易读。对于更复杂的情况,例如遍历树,递归可能是合适的,但在这种情况下,尾调用优化将无济于事。

,

您可以使用 Span 防止复制数组。然后您可以 slice 递归到数组的末尾。

int sum(Span<int> span,int subtotal)
{
    return span.Length > 0
        ? sum(span.Slice(1),subtotal + span[0])
        : subtotal;
}

Span 是不久前添加的,我相信在 .NET Core 中,它带来了相当多的性能改进。它允许将更多代码从 C++ 核心移至 C#。 Here 是我读过的一篇关于该主题的文章。

,

递归是一个调用自身的函数。尾递归是一个调用自身的函数,调用自身是它执行的最后一个操作。这很重要,因为支持尾递归的编译器可以在递归调用之前消除堆栈帧,甚至可以将其转换为循环。无论哪种情况,都可以防止由于重复调用而导致堆栈帧的累积,从而消除了堆栈溢出的可能性。

也就是说,您的递归 sum() 函数可以工作,但效率低下:它为每一步都创建新数组。有一种更简单的递归计算总和的方法:

static int sum(int[] array)
{
    return sum(array,array.Length - 1);
}

static int sum(int[] array,int index)
{
    return array[index] + (index == 0 ? 0 :
        sum(array,index - 1));
}

第一个sum()函数调用带有合适参数的辅助函数,辅助函数只需要调整提供的索引即可。为简洁起见,这里使用三元表达式完成此操作:它使函数本质上保持单行,并且清楚地说明递归调用不是返回前的最后一个操作(加法是)。

要将这个计算转换为尾递归计算,我们需要为中间结果添加一个参数:

static int sum(int[] array)
{
    return sum(array,array.Length - 1,0);
}

static int sum(int[] array,int index,int res)
{
    return index < 0 ? res :
        sum(array,index - 1,res + array[index]);
}

这里的加法被移到递归调用之前,而且调用显然是最后一个操作,使得函数尾递归。