【问题标题】:Tail recursion and memoization with C#使用 C# 进行尾递归和记忆
【发布时间】:2012-09-20 19:00:32
【问题描述】:

我正在编写一个函数,该函数根据数据库条目表查找目录的完整路径。每条记录都包含一个键、目录的名称和父目录的键(如果您熟悉,它就是 MSI 中的目录表)。我有一个迭代解决方案,但它开始看起来有点讨厌。我以为我可以写一个优雅的尾递归解决方案,但我不确定了。

我将向您展示我的代码,然后解释我面临的问题。

Dictionary<string, string> m_directoryKeyToFullPathDictionary = new Dictionary<string, string>();
...
private string ExpandDirectoryKey(Database database, string directoryKey)
{
    // check for terminating condition
    string fullPath;
    if (m_directoryKeyToFullPathDictionary.TryGetValue(directoryKey, out fullPath))
    {
        return fullPath;
    }

    // inductive step
    Record record = ExecuteQuery(database, "SELECT DefaultDir, Directory_Parent FROM Directory where Directory.Directory='{0}'", directoryKey);
    // null check
    string directoryName = record.GetString("DefaultDir");
    string parentDirectoryKey = record.GetString("Directory_Parent");

    return Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey), directoryName);
}

当我意识到我有问题时,这就是代码的样子(删除了一些小的验证/按摩)。我想尽可能使用记忆来短路,但这需要我对字典进行函数调用以存储递归 ExpandDirectoryKey 调用的输出。我意识到我在那里也有一个Path.Combine 电话,但我认为可以通过... + Path.DirectorySeparatorChar + ... 来规避。

我想过使用一个辅助方法来记忆目录并返回值,以便我可以在上面的函数末尾这样调用它:

return MemoizeHelper(
    m_directoryKeyToFullPathDictionary,
    Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey)),
    directoryName);

但我觉得那是作弊,不会被优化为尾递归。

有什么想法吗?我应该使用完全不同的策略吗?这根本不需要是一个超级高效的算法,我只是很好奇。我正在使用 .NET 4.0,顺便说一句。

谢谢!

附:如果您想知道我的终止条件,请不要担心。我用根目录预播字典。

【问题讨论】:

  • 无论如何,使用+ 运算符进行字符串连接不会使其成为尾调用——函数中的最后一次调用将是+ 运算符,而不是递归调用。跨度>

标签: c# .net tail-recursion memoization


【解决方案1】:

即使您确实获得了正确的代码布局,您也会遇到的第一个主要问题是 C# 编译器并不能很好地支持尾递归。 Tail recursion in C#

使用那里的信息,您可能可以使其工作。但它充其量是不雅的。

【讨论】:

    【解决方案2】:

    你说你很好奇,我也是,让我们看看你对这种方法的看法。我正在概括您的问题以解释我的观点。假设您有这样的 Record 类型:

    class Record
    {
        public int Id { get; private set; }
        public int ParentId { get; private set; }
        public string Name { get; private set; }
    
        public Record(int id, int parentId, string name)
        {
            Id = id;
            ParentId = parentId;
            Name = name;
        }
    }
    

    还有一个由这个函数表示的“数据库”:

    static Func<int, Record> Database()
    {
        var database = new Dictionary<int, Record>()
         {
             { 1, new Record(1, 0, "a") },
             { 2, new Record(2, 1, "b") },
             { 3, new Record(3, 2, "c") },
             { 4, new Record(4, 3, "d") },
             { 5, new Record(5, 4, "e") },
         };
         return x => database[x];
     }
    

    让我们介绍一种“表示”递归的方法,如下所示:

    public static class Recursor
    {
        public static IEnumerable<T> Do<T>(
            T seed, 
            Func<T, T> next, 
            Func<T, bool> exit)
        {
            return Do(seed, next, exit, x => x);
        }
    
        public static IEnumerable<TR> Do<T, TR>(
            T seed, 
            Func<T, T> next, 
            Func<T, bool> exit, 
            Func<T, TR> resultor)
        {
            var r = seed;
            while (true)
            {
                if (exit(r))
                    yield break;
                yield return resultor(r);
                r = next(r);
            }
        }
    }
    

    正如您所见,这根本不是递归,而是允许我们根据“看起来”递归的部分来表达算法。我们的路径生成函数如下所示:

    static Func<int, string> PathGenerator(Func<int, Record> database)
    {
        var finder = database;
    
        return id => string.Join("/",
            Recursor.Do(id,                       //seed
                        x => finder(x).ParentId,  //how do I find the next item?
                        x => x == 0,              //when do I stop?
                        x => finder(x).Name)      //what do I extract from next item?
                    .Reverse());                  //reversing items
    }
    

    此函数使用Recursor 表示算法,并收集所有产生的部分以组成路径。这个想法是Recursor 不会累积,它将累积的责任委托给调用者。我们将这样使用它:

    var f = PathGenerator(Database());
    Console.WriteLine(f(3));           //output: a/b/c
    

    你也想要memoization,我们来介绍一下:

    public static class Memoizer
    {
        public static Func<T, TR> Do<T, TR>(Func<T, TR> gen)
        {
            var mem = new Dictionary<T, TR>();
            return (target) =>
            {
                if (mem.ContainsKey(target))
                    return mem[target];
                mem.Add(target, gen(target));
                return mem[target];
            };
        }
    }
    

    有了这个,您可以记住您对昂贵资源(数据库)的所有访问,只需更改PathGenerator 中的一行,如下:

    var finder = database;
    

    到这里:

    var finder = Memoizer.Do<int, Record>(x => database(x));
    

    【讨论】:

    • 我不知道这在 CIL 中是否会很好,但是该死的儿子,感谢所有的努力。这是一种非常有趣的看待方式。
    • @Jay 没问题,正如我所说,这是一个有趣的问题 :) 我会看看生成的 IL 并告诉你。
    猜你喜欢
    • 2011-03-28
    • 2012-09-27
    • 2012-11-12
    • 2014-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-04
    • 1970-01-01
    相关资源
    最近更新 更多