【问题标题】:How can I write this method so that it is eligible for tail recursion optimization?如何编写此方法以使其符合尾递归优化的条件?
【发布时间】:2011-08-22 21:00:04
【问题描述】:

有人知道一种算法来对尾递归进行简单的递归吗? 更具体地说,您将如何将该算法应用于以下代码?

namespace Testing
{
    class Program
    {
        static void Main(string[] args)
        {
           Console.WriteLine(match("?**", "aaa"));
           Console.WriteLine(match("*#*?", "aa1$a1a1"));
           Console.WriteLine(match("*#*", "aa11"));
           Console.WriteLine(match("??*", "0110"));
           Console.WriteLine(match("", "abc"));
           Console.WriteLine(match("???", ""));
           Console.ReadLine();

        }


        public static bool match(string p, string s)
        {
            if (p.Length == 0)
                return true;
            if (p.Length > s.Length)
                return false;

            bool firstLetterMatches = false;
            char nextCharInStr = s[0];
            switch (p[0])
            {
                case '*':
                    firstLetterMatches = 'a'<= nextCharInStr && nextCharInStr <= 'z';
                    break;

                case '#':
                    firstLetterMatches = '0'<= nextCharInStr && nextCharInStr <= '9';
                    break;

                case '?':
                    firstLetterMatches = ('a'<= nextCharInStr && nextCharInStr <= 'z') ||
                                         ('0'<= nextCharInStr && nextCharInStr <= '9');
                    break;

                default:
                    return false;
            }

            return match(p,s.Substring(1)) ||  
                (firstLetterMatches && match(p.Substring(1),s.Substring(1)));
        }
    }


}

谢谢!

【问题讨论】:

  • 由于 MS C# 编译器不发出尾调用,并且即使它发出 CLI 也不是强制 为了尊重它们,如果您想避免使用堆栈,您将寻找一种更手动的方法。我敢提goto吗?
  • @Marc - 我认为手动堆栈可能是更好的选择。他们要避免的另一件事是重复复制字符串。
  • @ChaosPandion 哦,当然你会使用你递增的charIndex 变量。当数据量不可预测时,手动堆栈很有用 - 不确定您是否需要。
  • 我不明白为什么你们认为这个人想要一个 C# 特定的答案(关于性能)只是因为他用 C# 写了这个问题——我假设他只是在使用以C#为例,询问将函数重组为尾递归的一般问题。
  • @mquander 用 C# 中的示例标记了 C#?我认为我们可以假设 C#...

标签: c# tail-recursion


【解决方案1】:

我假设您之所以这么问,是因为您实际上遇到了一个现实世界的问题,即炸毁堆栈。看起来您在这里对较小的子字符串进行递归操作。这可能极度低效且危险。字符串很容易长到递归算法破坏堆栈,并且由于字符串是不可变的但不是持久的,因此每次调用 substring 时都会创建一个新字符串;这将创建最少 O(n^2) 字节的字符串,这些字符串必须被复制。

(另外,看起来你正在做某种最长匹配子序列模式;正如 mquander 指出的那样,某种记忆策略很可能有助于降低时间复杂度;它通常对这种问题。)

要解决字符串分配问题,您可以传递一个字符串,而不是一个字符串,一个字符串和将被视为字符串开头的索引。现在你只是增加一个整数,而不是分配和复制一个字符串。

为了解决递归问题,您可以使用多种技术。我写了一系列文章,介绍了将简单的树递归算法转换为消耗堆而不是调用堆栈空间的算法的各种方法。其中一些是用 JScript 编写的,但这些想法很容易转换为 C#。

最后,在 C# 5 中,我们将引入一个“await”关键字,它会导致编译器对程序进行持续传递样式转换。这个关键字的目的是使异步编程更容易,但它的副作用是它也使无堆栈编程更容易。如果您有兴趣,请下载我们发布的社区技术预览版,您可以自动将您的程序转换为不消耗堆栈的程序。

好的,那么,关于将递归算法变成消耗堆而不是堆栈的算法的文章,从这里开始:

http://blogs.msdn.com/b/ericlippert/archive/2005/07/27/recursion-part-one-recursive-data-structures-and-functions.aspx

我所有关于延续传递风格的文章都在这里:(从底部开始)

http://blogs.msdn.com/b/ericlippert/archive/tags/continuation+passing+style/

关于异步的都在这里:(再次,从底部开始)

http://blogs.msdn.com/b/ericlippert/archive/tags/async/

【讨论】:

  • 哇,我没有想到 awaitasync 可以在异步上下文之外使用来执行此操作。很酷。
  • @mquander:是的。 “await”只是“call/cc”,戴着一顶有趣的帽子。详情请见stackoverflow.com/questions/4070237/…
  • 迭代器块是做同样事情的另一种方式——而且通常很容易实现。我想您可以将它们视为弱协程......这是 CPS 的一种形式。
  • @LBushkin:确实。我们尝试了很长时间来提出一个统一的抽象,使迭代器块和异步块“相同”,但不能让它看起来不错。
【解决方案2】:

有点。您可以通过将任何递归算法转换为continuation-passing style 来使任何递归算法成为尾递归。效果只是获取调用堆栈并显式传递它。但这不会给您带来您可能想到的好处,即能够在递归调用后丢弃先前的状态以节省空间。您只是将状态放在其他地方。

真正的问题可能是:您能否通过使用尾递归的方式将任何递归算法更改为只需要恒定空间?答案当然是也许。通常,使用树递归的递归函数(递归调用分支为多个更深层次的递归调用)可能很难以这种方式转换。你的算法符合这个描述。

(我最初建议记住 match 或使用 DP 来解决这个问题,这会加快速度,但我想这实际上不会节省你的空间。哦,好吧。)

【讨论】:

  • 嗯,.NET 实际上不会优化它,但它会“符合”TCO。这就是我将他的问题解释为要问的问题,而不是关于他如何让它在 C# 中有效运行。
【解决方案3】:

简单的方法是使用while(true) 循环。示例:

public static bool match(string p, string s)
{
  while (true)
  {
    // normal code
    ...
    // tail call handling
    // instead of return match(x, y)
    var t1 = x; // need to use temps for evaluation of x
    var t2 = y; // same here
    p = t1;
    s = t2;
    continue; 
  }
}

此过程通常称为 TCE(或尾调用消除)。

更新:

最后我错过了||。您无法转换它,因为没有匹配调用位于尾部位置。 || 要求在返回之前评估结果。或许可以重写该方法来避免这种情况。

【讨论】:

  • 我很想看看你将如何实际实现这一点,因为他的算法是树递归的——如果第一个字母匹配但匹配稍后失败,它需要使用初始字符串再次递归。将它全部放在一个循环中似乎不会让您保持足够的状态来获得正确的结果。
  • @mquander:您不能将此应用于非尾递归。可以处理return match 的实际尾调用。所有其他的递归,都需要用到栈。
  • @mquander:谢谢你让我再看一遍。实际上,尾部位置没有方法调用,因此无法应用此转换。它仅适用于正确的尾递归调用。
【解决方案4】:

C# 当前不支持尾递归 看到这个相关的question就此事

还可以查看msdn article 的类似技术,称为蹦床

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-23
    • 2015-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-03
    相关资源
    最近更新 更多