【问题标题】:Linq Recursive SumLinq 递归和
【发布时间】:2018-07-05 08:19:27
【问题描述】:

我有以下数据结构:

        List<Item> Items = new List<Item>
        {
            new Item{ Id = 1, Name = "Machine" },
            new Item{ Id = 3, Id_Parent = 1,  Name = "Machine1"},
            new Item{ Id = 5, Id_Parent = 3,  Name = "Machine1-A", Number = 2, Price = 10 },
            new Item{ Id = 9, Id_Parent = 3,  Name = "Machine1-B", Number = 4, Price = 11 },
            new Item{ Id = 100,  Name = "Item" } ,
            new Item{ Id = 112,  Id_Parent = 100, Name = "Item1", Number = 5, Price = 55 }
        };

我想构建一个查询,获取其父项中所有子项价格的总和(项目与 Id_Parent 相关)。 例如,对于 Item Id = 100,我有 55,因为这是它的子项的值。

对于项目 ID = 3,我有 21 个,因为项目 ID = 5 和 ID = 9 总和。 到目前为止还不错。

我正在努力得到的是项目 Id = 1 我也应该有总和 = 21,因为 Id = 3 是 Id = 1 的子项,并且总和为 21。

这是我的代码:

        var result = from i in items
                                   join item in item on i.Id_Parent equals item.Id
                                   select new
                                   {
                                       Name = prod.Nome,
                                       Sum =
                                         (from it in items
                                          where it.Id_Parent == item.Id
                                          group it by new
                                          {
                                              it.Id_Parent
                                          }
                                          into g
                                          select new
                                          {
                                              Sum = g.Sum(x => x.Price)
                                          }
                                         ).First()
                                   };

帮助表示赞赏。

【问题讨论】:

  • 我会在父 id 上创建一个查找并执行递归方法来总结所有孩子的值。 Linq 在递归方面不是最好的。
  • 您是否可以为您的项目建立一个看起来不像是直接从数据库中读取的内存表示?如果Item 有一个IEnumerable&lt;Item&gt; Children 属性和一个Item Parent 属性,那就太好了。如果你有这些,那么在数据结构上写fold 的问题就会变得容易得多。
  • You may want to consider not using underscores。它们在技术上没有任何问题。

标签: c# linq recursion


【解决方案1】:

创建一个递归函数来查找父级的所有子级:

public static IEnumerable<Item> ItemDescendents(IEnumerable<Item> src, int parent_id) {
    foreach (var item in src.Where(i => i.Id_Parent == parent_id)) {
        yield return item;
        foreach (var itemd in ItemDescendents(src, item.Id))
            yield return itemd;
    }
}

现在您可以获得任何父母的价格:

var price1 = ItemDescendants(Items, 1).Sum(i => i.Price);

请注意,如果您知道某项的子项的 id 值始终大于其父项,则不需要递归:

var descendents = Items.OrderBy(i => i.Id).Aggregate(new List<Item>(), (ans, i) => {
    if (i.Id_Parent == 1 || ans.Select(a => a.Id).Contains(i.Id_Parent))
        ans.Add(i);
    return ans;
});

对于那些喜欢避免递归的人,您可以使用显式堆栈来代替:

public static IEnumerable<Item> ItemDescendentsFlat(IEnumerable<Item> src, int parent_id) {
    void PushRange<T>(Stack<T> s, IEnumerable<T> Ts) {
        foreach (var aT in Ts)
            s.Push(aT);
    }

    var itemStack = new Stack<Item>(src.Where(i => i.Id_Parent == parent_id));

    while (itemStack.Count > 0) {
        var item = itemStack.Pop();
        PushRange(itemStack, src.Where(i => i.Id_Parent == item.Id));
        yield return item;
    }
}

我包含了PushRange 辅助函数,因为Stack 没有。

最后,这是一个不使用任何堆栈的变体,无论是隐式的还是显式的。

public IEnumerable<Item> ItemDescendantsFlat2(IEnumerable<Item> src, int parent_id) {
    var children = src.Where(s => s.Id_Parent == parent_id);
    do {
        foreach (var c in children)
            yield return c;
        children = children.SelectMany(c => src.Where(i => i.Id_Parent == c.Id)).ToList();
    } while (children.Count() > 0);
}

您也可以用Lookup 替换源的多次遍历:

public IEnumerable<Item> ItemDescendantsFlat3(IEnumerable<Item> src, int parent_id) {
    var childItems = src.ToLookup(i => i.Id_Parent);

    var children = childItems[parent_id];
    do {
        foreach (var c in children)
            yield return c;
        children = children.SelectMany(c => childItems[c.Id]).ToList();
    } while (children.Count() > 0);
}

我基于 cmets 优化了上述关于过多嵌套枚举的内容,这极大地提高了性能,但我也受到启发尝试删除可能很慢的 SelectMany,并收集 IEnumerables,正如我所见建议在别处优化Concat

public IEnumerable<Item> ItemDescendantsFlat4(IEnumerable<Item> src, int parent_id) {
    var childItems = src.ToLookup(i => i.Id_Parent);

    var stackOfChildren = new Stack<IEnumerable<Item>>();
    stackOfChildren.Push(childItems[parent_id]);
    do
        foreach (var c in stackOfChildren.Pop()) {
            yield return c;
            stackOfChildren.Push(childItems[c.Id]);
        }
    while (stackOfChildren.Count > 0);
}

@AntonínLejsek 的 GetDescendants 仍然是最快的,虽然现在非常接近,但有时更简单的性能会胜出。

【讨论】:

  • 这是个好主意,但您可以做得更好。首先,请注意您在使用 src 时如何不使用List&lt;Item&gt; 的属性,因此它可能是IEnumerable&lt;Item&gt;。其次,您将整个结果集构建为List&lt;Item&gt;,但您可以改为yield return 依次为每个元素,这样您就不必将整个数据结构放在内存中两次。最后,方法命名错误;这些是项目的后代,而不是项目的孩子
  • 总是谦卑地接受@EricLippert 的教育 :) 顺便说一句,你将如何处理递归调用的yield return? (你什么时候将yield return all 添加到 C# 中?。)
  • 好吧,我已经五年多没有加入 C# 团队了,所以我不太可能添加 yield return all,尽管它会很有用!我会循环执行。
  • @ErikPhilips:确实可以;即使没有,该方法的成本还有一个额外的 O(h) 时间乘数,其中 h 是森林中最高树的高度。有一些机制可以解决这个问题,我们将 O(h) 时间乘数转换为 O(h) 额外空间。
  • IEnumerable 的多个枚举不好,你在最后两个解决方案中一遍又一遍地计算相同的东西。
【解决方案2】:

简单的方法是使用本地函数,如下所示:

int CalculatePrice(int id)
{
    int price = Items.Where(item => item.Id_Parent == id).Sum(child => CalculatePrice(child.Id));
    return price + Items.First(item => item.Id == id).Price;
}
int total = CalculatePrice(3); // 3 is just an example id

另一个更简洁的解决方案是使用Y combinator 创建一个可以内联调用的闭包。假设你有这个

/// <summary>
/// Implements a recursive function that takes a single parameter
/// </summary>
/// <typeparam name="T">The Type of the Func parameter</typeparam>
/// <typeparam name="TResult">The Type of the value returned by the recursive function</typeparam>
/// <param name="f">The function that returns the recursive Func to execute</param>
/// <returns>The recursive Func with the given code</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> f)
{
    Func<T, TResult> g = null;
    g = f(a => g(a));
    return g;
}

那么你就可以像这样得到你的结果:

int total = Y<int, int>(x => y =>
{
    int price = Items.Where(item => item.Id_Parent == y).Sum(child => x(child.Id));
    return price + Items.First(item => item.Id == y).Price;
})(3);

这样做的好处是它允许您以函数式方式快速声明和调用递归函数,这在这种情况下特别方便,您只需要“一次性”函数,您将只使用一次。此外,由于这个函数非常小,使用 Y 组合器进一步减少了必须声明本地函数并在另一行调用它的样板。

【讨论】:

    【解决方案3】:

    解决方案有很多,值得做一个基准测试。我也将我的解决方案添加到混合中,这是最后一个功能。有些函数包括根节点,有些不包括,但除此之外它们返回相同的结果。我测试了每个父母有 2 个孩子的宽树和每个父母只有一个孩子的窄树(深度等于项目数)。结果是:

    ---------- Wide 100000 3 ----------
    ItemDescendents: 9592ms
    ItemDescendentsFlat: 9544ms
    ItemDescendentsFlat2: 45826ms
    ItemDescendentsFlat3: 30ms
    ItemDescendentsFlat4: 11ms
    CalculatePrice: 23849ms
    Y: 24265ms
    GetSum: 62ms
    GetDescendants: 19ms
    
    ---------- Narrow 3000 3 ----------
    ItemDescendents: 100ms
    ItemDescendentsFlat: 24ms
    ItemDescendentsFlat2: 75948ms
    ItemDescendentsFlat3: 1004ms
    ItemDescendentsFlat4: 1ms
    CalculatePrice: 69ms
    Y: 69ms
    GetSum: 915ms
    GetDescendants: 0ms
    

    虽然过早优化不好,但了解什么是渐近行为很重要。渐近行为决定了算法是否会扩展或死亡。

    代码如下

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApp3
    {
        class Program
        {
            public class Test
            {
                public static IEnumerable<Item> ItemDescendents(IEnumerable<Item> src, int parent_id)
                {
                    foreach (var item in src.Where(i => i.Id_Parent == parent_id))
                    {
                        yield return item;
                        foreach (var itemd in ItemDescendents(src, item.Id))
                            yield return itemd;
                    }
                }
    
                public static IEnumerable<Item> ItemDescendentsFlat(IEnumerable<Item> src, int parent_id)
                {
                    void PushRange<T>(Stack<T> s, IEnumerable<T> Ts)
                    {
                        foreach (var aT in Ts)
                            s.Push(aT);
                    }
    
                    var itemStack = new Stack<Item>(src.Where(i => i.Id_Parent == parent_id));
    
                    while (itemStack.Count > 0)
                    {
                        var item = itemStack.Pop();
                        PushRange(itemStack, src.Where(i => i.Id_Parent == item.Id));
                        yield return item;
                    };
                }
    
                public IEnumerable<Item> ItemDescendantsFlat2(IEnumerable<Item> src, int parent_id)
                {
                    var children = src.Where(s => s.Id_Parent == parent_id);
                    do
                    {
                        foreach (var c in children)
                            yield return c;
                        children = children.SelectMany(c => src.Where(i => i.Id_Parent == c.Id));
                    } while (children.Count() > 0);
                }
    
                public IEnumerable<Item> ItemDescendantsFlat3(IEnumerable<Item> src, int parent_id)
                {
                    var childItems = src.ToLookup(i => i.Id_Parent);
    
                    var children = childItems[parent_id];
                    do
                    {
                        foreach (var c in children)
                            yield return c;
                        children = children.SelectMany(c => childItems[c.Id]);
                    } while (children.Count() > 0);
                }
    
                public IEnumerable<Item> ItemDescendantsFlat4(IEnumerable<Item> src, int parent_id)
                {
                    var childItems = src.ToLookup(i => i.Id_Parent);
    
                    var stackOfChildren = new Stack<IEnumerable<Item>>();
                    stackOfChildren.Push(childItems[parent_id]);
                    do
                        foreach (var c in stackOfChildren.Pop())
                        {
                            yield return c;
                            stackOfChildren.Push(childItems[c.Id]);
                        }
                    while (stackOfChildren.Count > 0);
                }
    
                public static int GetSum(IEnumerable<Item> items, int id)
                {
                    // add all matching items
                    var itemsToSum = items.Where(i => i.Id == id).ToList();
                    var oldCount = 0;
                    var currentCount = itemsToSum.Count();
                    // it nothing was added we skip the while
                    while (currentCount != oldCount)
                    {
                        oldCount = currentCount;
                        // find all matching items except the ones already in the list
                        var matchedItems = items
                            .Join(itemsToSum, item => item.Id_Parent, sum => sum.Id, (item, sum) => item)
                            .Except(itemsToSum)
                            .ToList();
                        itemsToSum.AddRange(matchedItems);
                        currentCount = itemsToSum.Count;
                    }
    
                    return itemsToSum.Sum(i => i.Price);
                }
    
                /// <summary>
                /// Implements a recursive function that takes a single parameter
                /// </summary>
                /// <typeparam name="T">The Type of the Func parameter</typeparam>
                /// <typeparam name="TResult">The Type of the value returned by the recursive function</typeparam>
                /// <param name="f">The function that returns the recursive Func to execute</param>
                /// <returns>The recursive Func with the given code</returns>
                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> f)
                {
                    Func<T, TResult> g = null;
                    g = f(a => g(a));
                    return g;
                }
    
    
                public IEnumerable<Item> GetDescendants(IEnumerable<Item> items, int key)
                {
                    var lookup = items.ToLookup(i => i.Id_Parent);
                    Stack<Item> st = new Stack<Item>(lookup[key]);
    
                    while (st.Count > 0)
                    {
                        var item = st.Pop();
                        yield return item;
                        foreach (var i in lookup[item.Id])
                        {
                            st.Push(i);
                        }
                    }
                }
    
                public class Item
                {
                    public int Id;
                    public int Price;
                    public int Id_Parent;
                }
    
                protected Item[] getItems(int count, bool wide)
                {
                    Item[] Items = new Item[count];
                    for (int i = 0; i < count; ++i)
                    {
                        Item ix = new Item()
                        {
                            Id = i,
                            Id_Parent = wide ? i / 2 : i - 1,
                            Price = i % 17
                        };
                        Items[i] = ix;
                    }
                    return Items;
                }
    
                public void test()
                {
                    Item[] items = null;
    
                    int CalculatePrice(int id)
                    {
                        int price = items.Where(item => item.Id_Parent == id).Sum(child => CalculatePrice(child.Id));
                        return price + items.First(item => item.Id == id).Price;
                    }
    
                    var functions = new List<(Func<Item[], int, int>, string)>() {
                    ((it, key) => ItemDescendents(it, key).Sum(i => i.Price), "ItemDescendents"),
                    ((it, key) => ItemDescendentsFlat(it, key).Sum(i => i.Price), "ItemDescendentsFlat"),
                    ((it, key) => ItemDescendantsFlat2(it, key).Sum(i => i.Price), "ItemDescendentsFlat2"),
                    ((it, key) => ItemDescendantsFlat3(it, key).Sum(i => i.Price), "ItemDescendentsFlat3"),
                    ((it, key) => ItemDescendantsFlat4(it, key).Sum(i => i.Price), "ItemDescendentsFlat4"),
                    ((it, key) => CalculatePrice(key), "CalculatePrice"),
                    ((it, key) => Y<int, int>(x => y =>
                    {
                        int price = it.Where(item => item.Id_Parent == y).Sum(child => x(child.Id));
                        return price + it.First(item => item.Id == y).Price;
                    })(key), "Y"),
                    ((it, key) => GetSum(it, key), "GetSum"),
                    ((it, key) => GetDescendants(it, key).Sum(i => i.Price), "GetDescendants" )                 
                    };
    
                    System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch();
    
                    var testSetup = new[]
                    {
                        new { Count = 10, Wide = true, Key=3}, //warmup
                        new { Count = 100000, Wide = true, Key=3},
                        new { Count = 3000, Wide = false, Key=3}
                    };
    
                    List<int> sums = new List<int>();
                    foreach (var setup in testSetup)
                    {
                        items = getItems(setup.Count, setup.Wide);
                        Console.WriteLine("---------- " + (setup.Wide ? "Wide" : "Narrow")
                            + " " + setup.Count + " " + setup.Key + " ----------");
                        foreach (var func in functions)
                        {
                            st.Restart();
                            sums.Add(func.Item1(items, setup.Key));
                            st.Stop();
                            Console.WriteLine(func.Item2 + ": " + st.ElapsedMilliseconds + "ms");
                        }
                        Console.WriteLine();
                        Console.WriteLine("checks: " + string.Join(", ", sums));
                        sums.Clear();
                    }
    
                    Console.WriteLine("---------- END ----------");
    
                }
            }
    
            static void Main(string[] args)
            {
                Test t = new Test();
                t.test();
            }
        }
    }
    

    【讨论】:

    • 不错。你的代码在Y 之前有GetSum,但你的输出是相反的。 GetSumY 的检查似乎也不匹配其他人?
    • @NetMage 你说得对,我忘了在代码中交换它们。 CalculatePrice 和 Y 属于同一个,它们几乎相同。检查有所不同,因为有些算法包括根节点,有些不包括,我已经在答案中解决了这个问题。
    • @NetMage 顺便说一句,我喜欢你的 ItemDescendentsFlat4。但不要陷入过度优化的陷阱。如果它在这里快 30% 和那里慢 10% 并不重要(无论如何它会在未来改变)。重要的是,我想展示的是,不良的渐近行为可以使您的解决方案轻松地慢 1000 倍。这是你应该注意的事情,因为这很重要。
    • 没有任何关于数据的细节,这都是过早的优化,不管行为如何。有时,更聪明的解决方案速度较慢,因为它们对简单问题的开销太大。
    【解决方案4】:

    对于可能会遇到StackOverflowException 的未来读者,我使用的替代方法如下例所示:(dotnetfiddle example)

    using System;
    using System.Collections.Generic;
    using System.Linq;
                        
    public class Program
    {
        public static void Main()
        {
            var items = new List<Item>
            {
                new Item{ Id = 1, Name = "Machine" },
                new Item{ Id = 3, Id_Parent = 1,  Name = "Machine1"},
                new Item{ Id = 5, Id_Parent = 3,  Name = "Machine1-A", Number = 2, Price = 10 },
                new Item{ Id = 9, Id_Parent = 3,  Name = "Machine1-B", Number = 4, Price = 11 },
                new Item{ Id = 100,  Name = "Item" } ,
                new Item{ Id = 112,  Id_Parent = 100, Name = "Item1", Number = 5, Price = 55 }
            };
            
            foreach(var item in items)
            {
                Console.WriteLine("{0} {1} $" + GetSum(items, item.Id).ToString(), item.Name, item.Id);
            }
            
        }
        
        public static int GetSum(IEnumerable<Item> items, int id)
        {
            // add all matching items
            var itemsToSum = items.Where(i => i.Id == id).ToList();
            var oldCount = 0;
            var currentCount = itemsToSum.Count();
            // it nothing was added we skip the while
            while (currentCount != oldCount)
            {
                oldCount = currentCount;
                // find all matching items except the ones already in the list
                var matchedItems = items
                    .Join(itemsToSum, item => item.Id_Parent, sum => sum.Id, (item, sum) => item)
                    .Except(itemsToSum)
                    .ToList();
                itemsToSum.AddRange(matchedItems);
                currentCount = itemsToSum.Count;
            }
            
            return itemsToSum.Sum(i => i.Price);
        }
        
        public class Item
        {
            public int Id { get; set; }
            public int Id_Parent { get; set; }
            public int Number { get; set; }
            public int Price { get; set; }
            public string Name { get; set; }
        
        }
    }
    

    结果:

    机器 1 21 美元

    机器1 3 $21

    机器1-A 5 $10

    机器 1-B 9 11 美元

    项目 100 55 美元

    项目 1 112 55 美元

    基本上,我们创建一个列表,其中包含与传递的 id 匹配的初始项目。如果 id 不匹配,我们没有项目,我们跳过 while 循环。如果我们确实有项目,那么我们加入以查找具有我们当前拥有的项目的父 id 的所有项目。然后,我们从该列表中排除已经在列表中的那些。然后附加我们找到的内容。最终,列表中不再有与父 ID 匹配的项目。

    【讨论】:

    • 这似乎有点过分/昂贵。
    • 正如我所提到的,如果您遇到堆栈溢出,这是另一种选择。
    • 我添加了一个显式的堆栈版本,它不会导致堆栈溢出,并且对每一代的处理都比Join / Except 少得多。
    • @NetMage 加入每个深度步骤的成本都很高,但 src.Where 每个项目的输出可能更糟。
    • @AntonínLejsek 在我的递归函数中,Where 返回父级的所有子级,而不是每个子级。
    猜你喜欢
    • 2014-01-31
    • 1970-01-01
    • 2012-01-29
    • 2011-06-16
    • 2017-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多