递归到Tail递归

时间:2017-06-09 13:59:17

标签: c# traversal

我正在编写一个遍历以找到道路中最长的路径。这段代码的神奇部分是segment.Next指的是LINQ,它具有应用于它的特定逻辑,就像不重新访问已访问过的节点一样。因此,不要指出travsel中的缺陷超出范围。

我想要做的是减少堆栈上的调用次数,因为有时路径可能是5000长。我知道我必须使这个递归调用尾递归。

public static IEnumerable<Segment> FindLongestPath(Segment segment)
{
    var rv = new List<Segment> {segment};
    var longestPathLength = 0;
    var longestNextPaths = Enumerable.Empty<Segment>();

    foreach (var n in segment.Next)
    {
        var paths = FindLongestPath(n);
        var length = paths.Sum(p => p.LengthMeters);
        if (length > longestPathLength)
        {
            longestPathLength = length;
            longestNextPaths = paths;
        }
    }

    rv.AddRange(longestNextPaths);

    return rv;
}

如何使这个递归调用成为尾递归?我知道我可能不得不在旅行时保持IEnumerable<Segment>,但我只是不想绕过它。

2 个答案:

答案 0 :(得分:4)

spender的答案是在没有递归的情况下解决此问题的实用方法:使用显式堆栈或队列作为帮助程序。

原始问题和消费者,在评论中,分别想知道如何在尾递归样式和延续传递样式中执行此算法。 (CPS是一种编程风格,其中每个调用都是尾调用。)

为了让您了解此算法的CPS版本的外观,让我(1)大大简化问题,(2)用ML编写解决方案,而不是C#。简化的问题是:

  • 函数children接受一个节点并生成一堆子节点。
  • 函数cost给出了遍历单个节点的成本。
  • 给出的问题是找出最大成本路径的成本。

首先,ML中的直接非CPS解决方案:

let rec maximum_path_cost node =
  let rec aux nodes max =
    match nodes with
    | [] -> max
    | head :: tail -> 
       let c = maximum_path_cost head in
       let new_max = if c > max then c else max in
       aux tail new_max
  in
  (cost node) + (aux (children node) 0)

简单地说:我们模拟一个带有递归辅助函数的循环,它累积到目前为止看到的最大值。循环条件是“列表是空的吗?”如果是,则结果是迄今为止看到的最大值;如果没有,那么我们计算当前项目(列表的头部)的成本,将其与最大值进行比较,然后在尾部运行循环。

请注意,aux是尾递归的,但是maximum_path_cost不是。

在延续传递样式中,maximum_path_cost采用延续 - 在这种情况下,是一个带有int的函数 - 并且需要使用其结果调用该函数,而不是返回。我们将使aux做同样的事情。

为简单起见,我们不会将成本和子项转换为CPS。

let rec maximum_path_cost node continuation =
  let rec aux nodes max aux_continuation =
    match nodes with
    | [] -> aux_continuation max
    | head :: tail ->
       let mpcc c = 
         let new_max = if c > max then c else max in
         aux tail new_max aux_continuation
       in
       maximum_path_cost head mpcc
  in
  let ac result =
    continuation ((cost node) + result) 
  in
    aux (children node) 0 ac

我知道很难将你的大脑包裹起来,但如果你仔细阅读它,它应该是有意义的。我们要做的第一件事是与孩子们一起调用aux,当前最大值为零;什么是第一次调用aux的延续?将其结果添加到头部的开销中,并将其传递给maximum_path_cost的延续。我们什么时候这样做?当我们运行整个子节点列表并且没有剩下的时候。

将其转换为C#并使C#保证尾递归仍然是一个练习。 :)

答案 1 :(得分:3)

使用尾递归执行此操作将会非常棘手,因为您需要将continuation作为委托来处理,以便进行后递归处理。对于那些不熟悉功能风格的人来说,代码看起来非常讨厌。

这似乎是你的主要动机不是打击电话堆栈。你可以减少你的大脑烧伤&#34;通过采用非递归方法。使用显式Queue<T> / Stack<T>(取决于您是否要首先遍历深度或广度),而不是从非尾递归方法调用获得的隐式堆栈意味着您的堆栈仅受限可用的记忆。

这应该让你开始走这条路:

public static IEnumerable<Segment> FindLongestPath(Segment segment)
{
    var queue = new Queue<Segment>(); //or a Stack<Segment> with Push and Pop
    queue.Enqueue(segment);

    while(queue.Any())
    {
        var currentSegment = queue.Dequeue();
        foreach(var seg in currentSegment.Next)
        {
            queue.Enqueue(seg);
        }
        //process currentSegment
    }
}