linq 选择每组具有最大属性值的记录

nom7f22z  于 2022-12-06  发布在  其他
关注(0)|答案(3)|浏览(145)

我有一个这样的数据集:

GroupName   GroupValue   MemberName   MemberValue
'Group1'    1            'Member1'    1
'Group1'    1            'Member2'    2
'Group2'    2            'Member3'    3
'Group2'    2            'Member4'    2
'Group3'    2            'Member5'    4
'Group3'    2            'Member6'    1

我想选择的是每个GroupNameMemberValue最大的行,但只选择那些GroupValue最大的GroupName,并将它们传递给一个委托函数。如下所示:

'Group2'    2            'Member3'    3
'Group3'    2            'Member5'    4

到目前为止,我已经尝试过这种格式...

data.Where(maxGroupValue => 
    maxGroupValue.GroupValue == data.Max(groupValue => groupValue.GroupValue))
.Select(FunctionThatTakesData)

......但这只会得到Group2和Group3的所有成员。我尝试在Select()之前放置GroupBy(),但这会将输出转换为IGrouping<string, DataType>,因此FunctionThatTakesData()不知道如何处理它,而且我无法执行另一个Where()来仅过滤掉最大的MemberValue
我该怎么做才能正确过滤这个数据集并将其传递到我的函数中呢?

2w3rbyxf

2w3rbyxf1#

您可以使用下面的Linq来实现这一点。

var results = data.GroupBy(r = r.GroupValue)
    .OrderByDescending(g => g.Key)
    .FirstOrDefault()
    ?.GroupBy(r => r.GroupName)
    .Select(g => g.OrderByDescending(r => r.MemberValue).First());

首先,您必须按GroupValue分组,然后按Key的降序对组进行排序(也就是GroupValue),然后取第一个,现在你就有了最大值为GroupValue的所有行。然后,将GroupName上的行进行分组,并从这些组中按降序对MemberValue进行排序,并获取First行以获取每个组中的行如果data是空的,我也会在FirstOrDefault后面使用C#6的null条件运算符?.。如果你没有使用C#6,那么你需要预先处理这种情况,你可以直接使用First

xxls0lw8

xxls0lw82#

所以基本上你需要的是,把你的数据元素分成具有相同GroupName值的组,从每个组中你要取一个元素,也就是具有最大MemberValue值的那个。
每当您有一个项目序列,并且希望根据序列中项目的一个或多个属性的值将此序列划分为组时,您可以使用Enumerable.GroupBy
'GroupBy'将您的序列作为输入和一个额外的输入参数:一个函数,用于选择要比较的项的哪些属性,以决定该项显示在哪个组中。
在您的示例中,您希望将序列分成组,其中组中的所有元素都具有相同的GroupName

var groups = mySequence.GroupBy(element => element.GroupName);

它从mySequence中的每个元素获取属性GroupName,并将该元素放入具有该GroupName值的元素组中。
使用示例数据,您将有三个组:

  • 包含GroupName ==“Group1”的所有元素的组。序列的前两个元素将位于该组中
  • 包含GroupName ==“Group2”的所有元素的组。序列的第三个和第四个元素将位于该组中
  • 包含GroupName ==“Group3”的所有元素的组。序列的最后两个元素将位于该组中

每个组都有一个Key属性,其中包含您的选择值。该键标识该组,并保证在组集合中是唯一的。因此,您将有一个Key ==“Group1”的组,一个Key ==“Group2”的组,依此类推。
Key外,每一个群都是群中元素的序列(注:组一个可枚举序列,而不是:它一个可枚举序列。
第二步是从每个组中获取组中MemberValue值最大的元素。为此,您将按属性MemberValue的值降序对组中的元素进行排序,并获取第一个元素。

var myResult = mySequence.GroupBy(element => element.GroupName)
    // intermediate result: groups where all elements have the same GroupName
    .Select(group => group.OrderByDescending(groupElement => groupElement.MemberValue)
    // intermediate result: groups where all elements are ordered in descending memberValue
    .First();

结果:从按memberValue降序排序的每个组中,取第一个元素,它应该是最大的元素。
如果您只需要memberValue值最大的元素,那么对整个组进行排序的效率不是很高。

voj3qocg

voj3qocg3#

解决此问题更简单的方法是使用新的(.NET 6)MaxBy LINQ运算符沿着GroupBySelect运算符:

IEnumerable<Record> query = records
    .GroupBy(x => x.GroupName)
    .Select(g => g.MaxBy(x => x.MemberValue));

这是一个简单但内存效率不高的解决方案。原因是它在幕后生成了一个完整的Lookup<TKey, TSource>结构,它是一个字典行容器,包含与每个键关联的所有记录。该结构是在开始比较每个分组中包含的元素之前生成的,以便选择最大的元素。
在大多数情况下,这种低效率并不是问题,因为记录并不多,而且它们已经存储在内存中。但是如果你有一个真正延迟的可枚举序列,其中包含大量的元素,你可能会耗尽内存。在这种情况下,你可以使用下面的GroupMaxBy运算符。这个运算符只在内存中存储每个键当前最大的元素:

/// <summary>
/// Groups the elements of a sequence according to a specified key selector
/// function, and then returns the maximum element in each group according to
/// a specified value selector function.
/// </summary>
public static IEnumerable<TSource> GroupMaxBy<TSource, TKey, TValue>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    Func<TSource, TValue> valueSelector,
    IEqualityComparer<TKey> keyComparer = default,
    IComparer<TValue> valueComparer = default)
{
    // Arguments validation omitted
    valueComparer ??= Comparer<TValue>.Default;
    var dictionary = new Dictionary<TKey, (TSource Item, TValue Value)>(keyComparer);
    foreach (var item in source)
    {
        var key = keySelector(item);
        var value = valueSelector(item);
        if (dictionary.TryGetValue(key, out var existing) &&
            valueComparer.Compare(existing.Value, value) >= 0) continue;
        dictionary[key] = (item, value);
    }
    foreach (var entry in dictionary.Values)
        yield return entry.Item;
}

用法示例:

IEnumerable<Record> query = records
    .GroupMaxBy(x => x.GroupName, x => x.MemberValue);

相反的GroupMinBy可以通过用<=替换>=来类似地实现。
下面是两种方法在内存效率方面的差异:

var source = Enumerable.Range(1, 1_000_000);
{
    var mem0 = GC.GetTotalAllocatedBytes(true);
    source.GroupBy(x => x % 1000).Select(g => g.MaxBy(x => x % 3333)).Count();
    var mem1 = GC.GetTotalAllocatedBytes(true);
    Console.WriteLine($"Allocated: {mem1 - mem0:#,0} bytes");
}
{
    var mem0 = GC.GetTotalAllocatedBytes(true);
    source.GroupMaxBy(x => x % 1000, x => x % 3333).Count();
    var mem1 = GC.GetTotalAllocatedBytes(true);
    Console.WriteLine($"Allocated: {mem1 - mem0:#,0} bytes");
}

输出量:

Allocated: 8,571,168 bytes
Allocated: 104,144 bytes

Try it on Fiddle

相关问题