是否可以将其作为单个有效的LINQ查询执行?

时间:2017-10-24 21:16:26

标签: c# algorithm linq time-complexity

我有一个像

这样的课程
public class Foo
{
   public string X;
   public string Y;
   public int Z;
}

我希望实现的查询是IEnumerable<Foo>,名为foos

  

“按X分组,然后按Y分组,并选择最大的子组   来自每个超级群体;如果有领带,请选择一个   最大的Z。“

换句话说,一个不那么紧凑的解决方案看起来像

var outer = foos.GroupBy(f => f.X);
foreach(var g1 in outer)
{
   var inner = g1.GroupBy(g2 => g2.Y);
   int maxCount = inner.Max(g3 => g3.Count());
   var winners = inner.Where(g4 => g4.Count() == maxCount));
   if(winners.Count() > 1)
   {
      yield return winners.MaxBy(w => w.Z);
   }
   else
   {
      yield return winners.Single();
   }
}

和一个效率不高的解决方案就像

from foo in foos
group foo by new { foo.X, foo.Y } into g
order by g.Key.X, g.Count(), g.Max(f => f.Z)
. . . // can't figure the rest out

但理想情况下我喜欢既紧凑又高效。

4 个答案:

答案 0 :(得分:1)

你正在重复使用可枚举,导致整个可枚举再次执行,这可能会导致性能显着下降。

你不那么紧凑的代码可以简化为这个。

foreach (var byX in foos.GroupBy(f => f.X))
{
    yield return byX.GroupBy(f => f.Y, f => f, (_, byY) => byY.ToList())
                    .MaxBy(l => l.Count)
                    .MaxBy(f => f.Z);
}

这是怎么回事,

项目按x分组,因此变量名为byX,这意味着整个byX枚举包含类似的X

现在,您按Y对此分组项目进行分组。名为byY的变量意味着整个byY枚举包含类似的Y,它们也有类似X

最后,您选择最大的列表,例如winnersMaxyBy(l => l.Count)),并从获奖者中选择最高ZMaxBy(f => f.Z))的项目。

我使用byY.ToList()的原因是为了防止重复枚举,否则会由Count()MaxBy()引起。

或者,您可以将整个迭代器更改为单个return语句。

return foos.GroupBy(f => f.X, f => f, (_, byX) => 
        byX.GroupBy(f => f.Y, f => f,(__, byY) => byY.ToList())
            .MaxBy(l => l.Count)
            .MaxBy(f => f.Z));

答案 1 :(得分:1)

根据您问题的措辞,我假设您希望结果为IEnumerable<IEnumerable<Foo>>。元素按XY分组,因此特定内部序列中的所有元素将具有XY的相同值。此外,每个内部序列将具有X的不同(唯一)值。

鉴于以下数据

X Y Z
-----
A p 1
A p 2
A q 1
A r 3
B p 1
B q 2

生成的序列序列应由两个序列组成(X = AX = B

X Y Z
-----
A p 1
A p 2

X Y Z
-----
B q 2

您可以使用以下LINQ表达式获得此结果:

var result = foos
    .GroupBy(
        outerFoo => outerFoo.X,
        (x, xFoos) => xFoos
            .GroupBy(
                innerFoo => innerFoo.Y,
                (y, yFoos) => yFoos)
            .OrderByDescending(yFoos => yFoos.Count())
            .ThenByDescending(yFoos => yFoos.Select(foo => foo.Z).Max())
            .First());

如果你真的关心性能,你很可能会以一些复杂性为代价来改进它:

当选择具有大多数元素或最高Z值的组时,对每组中的元素执行两次传递。首先使用yFoos.Count()计算元素,然后使用yFoos.Select(foo => foo.Z).Max()计算最大Z值。但是,您可以使用Aggregate一次性完成相同操作。

此外,没有必要对所有组进行排序以找到最大的&#34;组。相反,可以对所有组进行单次传递,以找到最大的&#34;再次使用Aggregate

result = foos
    .GroupBy(
        outerFoo => outerFoo.X,
        (x, xFoos) => xFoos
            .GroupBy(
                innerFoo => innerFoo.Y,
                (y, yFoos) => new
                {
                    Foos = yFoos,
                    Aggregate = yFoos.Aggregate(
                        (Count: 0, MaxZ: int.MinValue),
                        (accumulator, foo) =>
                            (Count: accumulator.Count + 1,
                             MaxZ: Math.Max(accumulator.MaxZ, foo.Z)))
                })
            .Aggregate(
                new
                {
                    Foos = Enumerable.Empty<Foo>(),
                    Aggregate = (Count: 0, MaxZ: int.MinValue)
                },
                (accumulator, grouping) =>
                    grouping.Aggregate.Count > accumulator.Aggregate.Count
                        || grouping.Aggregate.Count == accumulator.Aggregate.Count
                            && grouping.Aggregate.MaxZ > accumulator.Aggregate.MaxZ
                        ? grouping : accumulator)
            .Foos);

我在ValueTuple中使用Aggregate作为累加器,因为我希望它具有良好的性能。但是,如果你真的想知道你应该测量。

答案 2 :(得分:1)

你可以更多地忽略外部分组,剩下的只是一点点推荐MaxBy,类似于两个参数排序。如果你实现了这个,你最终会得到类似的东西:

public IEnumerable<IGrouping<string, Foo>> GetFoo2(IEnumerable<Foo> foos)
{
    return foos.GroupBy(f => f.X)
               .Select(f => f.GroupBy(g => g.Y)
                             .MaxBy2(g => g.Count(), g => g.Max(m => m.Z)));
}

当你将所有功能转移到非常普通的功能时,你可以调用这种linq方法是多么值得怀疑。您还可以使用聚合实现功能。有两种选择。种子和没有种子。我喜欢后一种选择:

public IEnumerable<IGrouping<string, Foo>> GetFoo3(IEnumerable<Foo> foos)
{
    return foos.GroupBy(f => f.X)
               .Select(f => f.GroupBy(g => g.Y)
                             .Aggregate((a, b) =>
                                    a.Count() > b.Count() ? a :
                                    a.Count() < b.Count() ? b :
                                    a.Max(m => m.Z) >= b.Max(m => m.Z) ? a : b
                             ));
}

如果Count()不是恒定时间,性能会受到影响,这是不能保证的,但在我的测试中它运行良好。带种子的变体会更复杂,但如果做得好可能会更快。

答案 3 :(得分:0)

进一步考虑这一点,我意识到你的orderby可以大大简化一切,但仍然不确定这是可以理解的。

var ans = foos.GroupBy(f => f.X, (_, gXfs) => gXfs.GroupBy(gXf => gXf.Y).Select(gXgYfs => gXgYfs.ToList())
                                                  .OrderByDescending(gXgYfs => gXgYfs.Count).ThenByDescending(gXgYfs => gXgYfs.Max(gXgYf => gXgYf.Z)).First());

虽然可以在LINQ中执行此操作,但如果在使用查询理解语法时将其设置为一个语句,我发现它不再紧凑或易懂:

var ans = from foo in foos
          group foo by foo.X into foogX
          let foogYs = (from foo in foogX
                        group foo by foo.Y into rfoogY
                        select rfoogY)
          let maxYCount = foogYs.Max(y => y.Count())
          let foogYsmZ = from fooY in foogYs
                      where fooY.Count() == maxYCount
                      select new { maxZ = fooY.Max(f => f.Z), fooY = from f in fooY select f }
          let maxMaxZ = foogYsmZ.Max(y => y.maxZ)
          select (from foogY in foogYsmZ where foogY.maxZ == maxMaxZ select foogY.fooY).First();

如果您愿意使用lambda语法,有些事情会变得更容易和更短,但不一定更容易理解:

var ans = from foogX in foos.GroupBy(f => f.X)
          let foogYs = foogX.GroupBy(f => f.Y)
          let maxYCount = foogYs.Max(foogY => foogY.Count())
          let foogYmCmZs = foogYs.Where(fooY => fooY.Count() == maxYCount).Select(fooY => new { maxZ = fooY.Max(f => f.Z), fooY })
          let maxMaxZ = foogYmCmZs.Max(foogYmZ => foogYmZ.maxZ)
          select foogYmCmZs.Where(foogYmZ => foogYmZ.maxZ == maxMaxZ).First().fooY.Select(y => y);

使用大量lambda语法,你可以完全理解:

var ans = foos.GroupBy(f => f.X, (_, gXfs) => gXfs.GroupBy(gXf => gXf.Y).Select(gXgYf => new { fCount = gXgYf.Count(), maxZ = gXgYf.Max(f => f.Z), gXgYfs = gXgYf.Select(f => f) }))
              .Select(fC_mZ_gXgYfs_s => {
                  var maxfCount = fC_mZ_gXgYfs_s.Max(fC_mZ_gXgYfs => fC_mZ_gXgYfs.fCount);
                  var fC_mZ_gXgYfs_mCs = fC_mZ_gXgYfs_s.Where(fC_mZ_gXgYfs => fC_mZ_gXgYfs.fCount == maxfCount).ToList();
                  var maxMaxZ = fC_mZ_gXgYfs_mCs.Max(fC_mZ_gXgYfs => fC_mZ_gXgYfs.maxZ);
                  return fC_mZ_gXgYfs_mCs.Where(fC_mZ_gXgYfs => fC_mZ_gXgYfs.maxZ == maxMaxZ).First().gXgYfs;
              });

(我修改了第三种可能性以减少重复计算并且更加干燥,但这确实使它更加冗长。)