【问题标题】:How do I turn a recursive algorithm into a tail-recursive algorithm?如何将递归算法变成尾递归算法?
【发布时间】:2013-05-23 17:53:37
【问题描述】:

作为进入合并排序的第一次尝试,我生成了以下代码,它适用于字符串,因为它们比列表更容易处理。

class Program
{
    static int iterations = 0;
    static void Main(string[] args)
    {            
        string test = "zvutsrqponmlihgfedcba";
        test = MergeSort(test);
        // test is sorted after 41 iterations
    }

    static string MergeSort(string input)
    {
        iterations++;
        if (input.Length < 2)
            return input;
        int pivot = 0;
        foreach (char c in input)
            pivot += c;
        pivot /= input.Length;
        string left = "";
        string right = "";
        foreach (char c in input)            
            if (c <= (char)pivot)
                left += c;
            else
                right += c;            
        return string.Concat(new string[] { MergeSort(left), MergeSort(right) });
    }
}

在 Wikipedia 上阅读可能的优化时,我发现以下提示“为了确保最多使用 O(log N) 空间,首先递归到数组的较小一半,然后使用尾调用递归到另一个。 "但老实说,我不知道如何将其应用于我的案例。 当我们学习递归和阶乘时,我对 IT 课上的尾调用有一些模糊的记忆,但我真的不明白如何将 Wikipedia 的建议应用于我的代码。

任何帮助将不胜感激。

【问题讨论】:

  • @RobertHarvey Dangit,我正要发帖。
  • @RobertHarvey:嗯,这很复杂; x64 抖动在某些情况下会产生尾调用,即使 C# 编译器从不产生尾调用指令,但这不是其中之一。但是这个问题是关于“手动”将程序重写为尾递归的。
  • 哦,好的,谢谢您的回答,我不知道。我想我应该在问之前告诉自己,但维基百科让我感到恐慌。还要感谢@torrential coding 将我的问题编辑为更合适的格式。
  • 这不是归并排序;这是快速排序。

标签: c# mergesort tail-recursion


【解决方案1】:

这个问题有很多问题,首先是您实现了一个非常慢的 QuickSort 版本,但问了一个关于 MergeSort 的问题。 MergeSort 通常不作为尾递归算法实现。

让我代表你问一个更好的问题:

如何将递归算法变成尾递归算法?

让我勾勒出一个更简单的尾递归转换,然后您可以研究如何将其应用于您的排序,如果您认为这样做是个好主意。

假设你有以下递归算法:

static int Count(Tree tree)
{
    if (tree.IsEmpty) 
        return 0;
    return 1 + Count(tree.Left) + Count(tree.Right);
}

让我们使用以下有点奇怪的转换将其分解为更多步骤:

static int Count(Tree tree)
{
    int total = 0;
    Tree current = tree;
    if (current.IsEmpty) 
        return 0;
    total += 1;
    int countLeft = Count(current.Left);
    total += countLeft;
    current = current.Right;
    int countRight = Count(current);
    total += countRight;
    return total;
}

请注意,这与之前的程序完全相同,只是更加冗长。当然你不会用这么冗长的方式编写程序,但它会帮助我们让它尾递归。

尾递归的目的是将递归调用变成goto。我们可以这样做:

static int Count(Tree tree)
{
    int total = 0;
    Tree current = tree;

 Restart:

    if (current.IsEmpty) 
        return total;
    int countLeft = Count(current.Left);
    total += 1;
    total += countLeft;
    current = current.Right;
    goto Restart;
}

看看我们在那里做了什么?我们没有递归,而是将当前引用重置为将被递归的事物,然后回到开始,同时保持累加器的状态。

现在是否清楚如何对快速排序算法做同样的事情?

【讨论】:

  • 非常感谢,即使我选择的语言不支持尾递归,您的回答仍然帮助我更好地理解它。在将答案标记为已接受之前,我将稍等片刻,因为我不想操之过急。
  • 当然,如果这是真实的代码,使用while 会比goto 更好。
  • @Eric Lippert:Microsft C# 编译器能否将示例 #1 中的 Count() 转换为 #3?
  • @svick:我发现while(true) { s; }L: s; goto L; 之间几乎没有区别。 goto 的重点是说明尾调用的真正含义;将goto 隐藏在while 中似乎比启发性更令人困惑。当然,如果我用更好的风格编写这个程序,它会像 int total = 0; for(Tree current = tree; !current.IsEmpty; current = current.Right) total += 1 + Count(current.Left); return total; 这不是好的风格,它的目的是使尾部调用转换清晰。
  • @Jack:不。C# 编译器根本没有尾调用优化。 x64 抖动有时会执行尾调用优化,但这种转换超出了它的范围。
【解决方案2】:

这看起来像是 QuickSort 的次优变体,而不是 MergeSort。您缺少此部分的 C# 等效项:

function merge(left, right)
    var list result
    while length(left) > 0 or length(right) > 0
        if length(left) > 0 and length(right) > 0
            if first(left) <= first(right)
                append first(left) to result
                left = rest(left)
            else
                append first(right) to result
                right = rest(right)
        else if length(left) > 0
            append first(left) to result
            left = rest(left)
        else if length(right) > 0
            append first(right) to result
            right = rest(right)
    end while
    return result

【讨论】:

  • 我正在研究你的答案,通过简单看一下,我想我的理论错了。
  • 好吧,如果我错了,请纠正我,但递归调用 string.concat 不是一回事吗?为什么这种方式更优化?
  • 调用 string.Concat 只是将一个字符串附加到另一个字符串。合并行为不同。假设a =“1357”和b =“468”。 Concat(a,b) 返回 "1357468" 。合并(a,b)返回“1345678”。
  • 如果有什么安慰的话,从长远来看,Quicksort 比 Mergesort 快 =) 如果你真的想要 Mergesort,请仔细研究这个伪代码:en.wikipedia.org/wiki/Merge_sort
  • 嗯,QuickSort平均而言比 MergeSort 快。但是 MergeSort 的好处是 保证 时间为 O(n lg n)。一个简单实现的快速排序通常会更快,但在某些情况下可能会像 O(n squared) 一样慢。
猜你喜欢
  • 2021-08-23
  • 2015-01-21
  • 2012-10-05
  • 2012-09-13
  • 1970-01-01
  • 2013-02-14
  • 2015-04-23
  • 1970-01-01
相关资源
最近更新 更多