在LINQ中转换递归方法组选择迭代方法

时间:2017-03-11 16:28:50

标签: c# linq recursion iteration

我有一个看起来像这样的课程:

public class SourceObject
{
    public string Id { get; set; }
    public List<SourceObject> Children { get; set; }

    public SourceObject()
    {
        Children = new List<SourceObject>();
    }
}

如您所见,它有一个属性,其中包含此同一类的其他实例的列表。我在这个类中处理的数据意味着直到运行时才会知道子项的数量以及整个&#34;深度&#34;结果对象图的结果也是未知的。

我需要创建一个&#34;映射&#34;从SourceObject的对象图形到DestinationObject的类似形状的图形(类似于AutoMapper可能如何从一个对象映射到另一个对象)。

我有一个方法可以从我的源图表映射到目标图表,但是,此方法使用递归:

// Recursive way of mapping each Source object to Destination
public static DestinationObject MapSourceToDestination(SourceObject source)
{
    var result = new DestinationObject();
    result.Id = source.Id;
    result.Children = source.Children.Select(MapSourceToDestination).ToList();
    return result;
}

当源对象图的大小不太大或太深时,这很好,但是,当源对象图非常大时,此方法将抛出StackOverflow异常。

我已设法创建此函数的替代版本,该函数删除递归并使用类似于this answer中描述的技术将其替换为队列/堆栈)但是,我已注意到队列/堆栈也会变得非常大,我不确定我的实现是否最有效。

是否可以将递归函数转换为纯粹在源对象图上使用迭代的函数(即删除递归,理想情况下,使用队列/堆栈)?

4 个答案:

答案 0 :(得分:2)

我仍然相信具有树的最大深度大小的堆栈是最佳的通用解决方案。

但有趣的是,数据结构和具体过程包含了实现转换所需的所有必要信息,而没有基于Children.Count的显式堆栈。让我们看看我们需要什么:

(1)是否有更多来源儿童要处理:source.Children.Count != target.Children.Count)

(2)下一个要处理的源子项是:source.Children[target.Children.Count]

(3)当前处理子指数是什么:target.Children.Count - 1

请注意,上述规则适用于处理过程中的任何级别。

以下是实施:

public static DestinationObject MapSourceToDestination(SourceObject source)
{
    // Map everything except childen
    Func<SourceObject, DestinationObject> primaryMap = s => new DestinationObject
    {
        Id = s.Id,
        // ...
        Children = new List<DestinationObject>(s.Children.Count) // Empty list with specified capacity
    };

    var target = primaryMap(source);

    var currentSource = source;
    var currentTarget = target;
    int depth = 0;
    while (true)
    {
        if (currentTarget.Children.Count != currentSource.Children.Count)
        {
            // Process next child
            var sourceChild = currentSource.Children[currentTarget.Children.Count];
            var targetChild = primaryMap(sourceChild);
            currentTarget.Children.Add(targetChild);
            if (sourceChild.Children.Count > 0)
            {
                // Move one level down
                currentSource = sourceChild;
                currentTarget = targetChild;
                depth++;
            }
        }
        else
        {
            // Move one level up
            if (depth == 0) break;
            depth--;
            currentSource = source;
            currentTarget = target;
            for (int i = 0; i < depth; i++)
            {
                int index = currentTarget.Children.Count - 1;
                currentSource = currentSource.Children[index];
                currentTarget = currentTarget.Children[index];
            }
        }
    }

    return target;
}

唯一棘手(且部分效率低下)的部分是向上移动步骤(这就是通用解决方案需要堆栈的原因)。如果对象具有Parent属性,则可能只是:

currentSource = currentSource.Parent;
currentTarget = currentTarget.Parent;

由于缺少这些属性,为了找到当前源项和目标项的父项,我们从根项开始,向下移动当前处理索引(见(3)),直到达到所需的深度。

答案 1 :(得分:1)

我不认为纯粹使用迭代的函数本身就更好了,但我实现了它有几个扩展

public static SourceObject GetAtList(this SourceObject s, List<int> cycleRef)
{
    var ret = s;
    for (int i = 0; i < cycleRef.Count; i++)
    {
        ret = ret.Children[cycleRef[i]];
    }
    return ret;
}
public static void SetAtList(this DestinationObject d, List<int> cycleRef, SourceObject s)
{
    var ret = d;
    for (int i = 0; i < cycleRef.Count - 1; i++)
    {
        ret = ret.Children[cycleRef[i]];
    }
    ret.Children.Add ( new DestinationObject() { Id = s.Id } );
}

和迭代器列表

public static DestinationObject MapSourceToDestinationIter(SourceObject source)
{
    var result = new DestinationObject();
    result.Id = source.Id;
    if (source.Children.Count == 0)
    {
        return result;
    }
    List<int> cycleTot = new List<int>();
    List<int> cycleRef = new List<int>();
    cycleRef.Add(0);
    cycleTot.Add(source.Children.Count-1);
    do
    {
        var curr = source.GetAtList(cycleRef);
        result.SetAtList(cycleRef, curr);
        if (curr.Children.Count == 0)
        {
            cycleRef[cycleRef.Count - 1]++;
            while (cycleRef[cycleRef.Count - 1]> cycleTot[cycleTot.Count-1])
            {
                cycleRef.RemoveAt(cycleRef.Count - 1);
                cycleTot.RemoveAt(cycleTot.Count - 1);
                if (cycleRef.Count == 0)
                {
                    break;
                }
                cycleRef[cycleRef.Count - 1]++;
            } 
        } else
        {
            cycleRef.Add(0);
            cycleTot.Add(curr.Children.Count - 1);
        }
    } while (cycleTot.Count>0);
    return result;
}

我不一定建议继续这样做,但它可能比Linq替代方案更快......

无论如何,明确使用Stack(如Ivan Stoev的answer)将是最佳解决方案。

答案 2 :(得分:0)

  

是否可以将递归函数转换为纯粹使用迭代的函数   在源对象图上(即删除递归,理想情况下,使用队列/堆栈)?

使用堆栈和队列可以实现用堆栈替换LINQ.Select的递归调用。 我使用Tuple来记住父节点的id。

运行时间 - o(n)。 空间复杂度 - o(级别中的节点数)。

如果我们只使用队列,我们​​可以改变空间复杂度 - o(min(h * d,n))。 h表示高度,b表示节点中的最大子节点数。 请考虑以下代码:

public DestinationObject MapSourceToDestination(SourceObject root)
{
    Stack<Tuple<DestinationObject,int>> stack = new Stack<Tuple<DestinationObject,int>>();

    DestinationObject currentChild = new DestinationObject();
    currentChild.Id = root.Id;
    stack.Push(new Tuple<DestinationObject,int>(currentChild,root.Id));

    while(stack.Count > 0)
    {
        Tuple<DestinationObject,int> currentTuple = stack.Pop();

        current = currentTuple[0];

        children = current.Children;

        foreach (SourceObject sourceChild in root.Children)
        {
            currentChild = new DestinationObject();
            currentChild.Id = currentTuple[1];
            Children.Add(currentChild);
            stack.Push(new Tuple<DestinationObject,int>(currentChild,sourceChild.Id));
        }
    }
}

答案 3 :(得分:0)

我敢说... 您的要求存在冲突,因此您的问题出在需求/设计中,而不是代码中?您在问题中提到的两点:

  1. 您说SourceObject的孩子数量在运行时之前是未知的。 在这种情况下,堆栈溢出的可能性是不可避免的。这就是当数据大小未知时发生的情况,并且在运行时它会比计算机上的可用空间大。

  2. 此外,无论您喜欢什么,堆栈或队列是这种处理的正确数据结构,如果您想避免递归。 您必须进行递归,或者必须将SourceObject存储在某些数据结构中,以便在继续处理时跟踪要访问的文件。

  3. 我会使用Stack / Queue方法进行图形探索或图遍历的递归,并留意如果图形足够大,那么我的Stack / Queue将占用所有系统内存,并导致溢出。

    要避免这种情况,请同时增加机器内存(即按比例放大)或增加为您工作的机器数量,同时并行化算法(即向外扩展)。