我想使用 LINQ 获得最常见的值

Posted

技术标签:

【中文标题】我想使用 LINQ 获得最常见的值【英文标题】:I want to get most frequent values using LINQ 【发布时间】:2021-10-14 10:47:54 【问题描述】:

我正在尝试使用 C# 中的 LINQ 获取数组中出现频率最高的值。

例如,

int[] input = 1, 1, 1, 3, 5, 5, 6, 6, 6, 7, 8, 8;

output = 1, 6
int[] input = 1, 2, 2, 3 ,3, 3, 5
output = 3

请告诉我如何构建 LINQ。

请仔细阅读。 这是Select most frequent value using LINQ 的另一个问题

我必须只选择最常见的值。下面的代码类似,但是我不能使用Take(5),因为我不知道结果的数量。

 int[] nums = new[]  1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 ;
 IEnumerable<int> top5 = nums
            .GroupBy(i => i)
            .OrderByDescending(g => g.Count())
            .Take(5)
            .Select(g => g.Key);

这个输出是 1, 2, 3, 4, 5 但我的预期输出 = 1, 2

请仔细阅读问题并回答。

感谢和问候。

【问题讨论】:

Take 五个元素。您如何期望您的输出仅包含 两个 元素? (似乎相反,您必须过滤这些元素 Where 计数等于 Max 计数。) 【参考方案1】:

只是为了添加过多的答案:

int[] input =  1, 1, 1, 3, 5, 5, 6, 6, 6, 7, 8, 8 ;

var result = input
   .GroupBy(i => i)
   .GroupBy(g => g.Count())
   .OrderByDescending(g => g.Key)
   .First()
   .Select(g => g.Key)
   .ToArray();

Console.WriteLine(string.Join(", ", result)); // Prints "1, 6" 

[编辑]

如果有人觉得这很有趣,我将 .net 4.8 和 .net 5.0 之间的上述性能进行了如下比较:

(1) 添加了一个Comparer 类来检测进行的比较次数:

class Comparer : IComparer<int>

    public int Compare(int x, int y)
    
        Console.WriteLine($"Comparing x with y");
        return x.CompareTo(y);
    

(2) 修改对OrderByDescending() 的调用以传递Comparer

.OrderByDescending(g => g.Key, new Comparer())

(3) 将我的测试控制台应用多定位到“net48”和“net5.0”。

进行这些更改后,输出如下:

对于 .net 4.8:

Comparing 1 with 3
Comparing 1 with 1
Comparing 1 with 2
Comparing 3 with 3
Comparing 3 with 2
Comparing 3 with 3
1, 6

对于 .net 5.0:

Comparing 3 with 1
Comparing 3 with 2
1, 6

如您所见,.net 5.0 得到了更好的优化。然而,对于 .net Framework(如 /u/mjwills 下面提到的),使用 MaxBy() 扩展名以避免必须使用 OrderByDescending() 可能会更高效 - 但前提是检测表明排序导致性能问题.

【讨论】:

可能能够使用MoreLinqMaxBy来避免完整的OrderByDescending的开销。 @mjwills 是的,这是个好主意,但请注意,对于 .net Core 3.1 及更高版本,OrderByDescending() 后跟 First() 实际上已优化为 O(N),因此您不会使用MaxBy()实际上不会看到任何性能优势。 它并没有真正记录在案,所以你不能依赖它,但至少这里有一些东西:github.com/dotnet/runtime/issues/14867 ...实际上正在考虑它。也许优化只针对OrderBy(),所以MaxBy() 仍然是个好主意!我去看看——看看这个空间;) 更新:我已经确认我提到的优化也适用于OrderByDescending() @MatthewWatson this 似乎也有关系。【参考方案2】:

如果您想在一个查询中使用纯 LINQ 执行此操作,您可以按计数对组进行分组并选择最大值:

int[] nums = new[]  1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 ;
var tops = nums
     .GroupBy(i => i)
     .GroupBy(grouping => grouping.Count())
     .OrderByDescending(gr => gr.Key)
     .Take(1)
     .SelectMany(g => g.Select(g => g.Key))
     .ToList();

请注意,这不是最有效和最清晰的解决方案。

UPD

使用Aggregate 执行MaxBy 的更有效的版本。请注意,与前一个不同,空集合会失败:

var tops = nums
     .GroupBy(i => i)
     .GroupBy(grouping => grouping.Count())
     .Aggregate((max, curr) => curr.Key > max.Key ? curr : max)
     .Select(gr => gr.Key);

您也可以使用MoreLinq 中的MaxBy 或.NET 6 中引入的一个。

【讨论】:

【参考方案3】:

您可以将结果存储在 IEnumerable 元组中,第一项是数字,第二项是输入数组中数字的计数。然后您查看包含最多元素的组的计数,并获取第二项等于最大值的所有元组。

int[] nums = new[]  1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 ;
var intermediate = nums
            .GroupBy(i => i)
            .Select(g => (g.Key,g.Count()));
int amount = intermediate.Max(x => x.Item2);
IEnumerable<int> mostFrequent = intermediate
            .Where(x => x.Item2 == amount)
            .Select(x => x.Item1);

在线演示:https://dotnetfiddle.net/YCVGam

【讨论】:

【参考方案4】:

使用变量捕获第一个项目的项目数,然后使用TakeWhile 获取具有该项目数的所有组。

void Main()

    var input = new[]  1, 1, 1, 3, 5, 5, 6, 6, 6, 7, 8, 8 ;

    int numberOfItems = 0;
    var output = input
        .GroupBy(i => i)
        .OrderByDescending(group => group.Count());
        
    var maxNumberOfItems = output.FirstOrDefault()?.Count() ?? 0;
        
    var finalOutput = output.TakeWhile(group => group.Count() == maxNumberOfItems).ToList();

    foreach (var item in finalOutput)
    
        Console.WriteLine($"Value item.Key has item.Count() members");
    

您也可以将其作为单个查询来执行:

int? numberOfItems = null;
var finalOutput = input
    .GroupBy(i => i)
    .OrderByDescending(group => group.Count())
    .TakeWhile(i =>
    
        var count = i.Count();
        numberOfItems ??= count;
        return count == numberOfItems;
    )
    .ToList();

【讨论】:

【参考方案5】:

您可以考虑添加扩展方法。类似的东西

public static IEnumerable<T> TakeWhileEqual<T, T2>(this IEnumerable<T> collection, Func<T, T2> predicate)
    where T2 : IEquatable<T2>

    using var iter = collection.GetEnumerator();
    if (iter.MoveNext())
    
        var first = predicate(iter.Current);
        yield return iter.Current;
        while (iter.MoveNext() && predicate(iter.Current).Equals(first))
        
            yield return iter.Current;
        
    

这具有高效的优点,不需要多次迭代集合。但它确实需要更多代码,即使这可以隐藏在扩展方法中。

【讨论】:

【参考方案6】:

我认为您可能想使用 TakeWhile 而不是 Take;

    int[] nums = new[]  1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 ;
    var n = nums
            .GroupBy(i => i)
            .OrderByDescending(g => g.Count());

    var c = n.First().Count();

    var r = n.TakeWhile(g => g.Count() == c)
            .Select(g => g.Key);

如果您想在没有 LINQ 的情况下一次性完成,您可以使用字典和列表轨道

a) 您看到某个值的次数以及 b) 你看到的次数最多的值是什么 c) 你多次看到的其他最有价值的东西是什么

我们跳过列表,尝试在字典中查找当前值。它要么有效,要么无效——如果有效,TryGetValue 会告诉我们当前值被查看了多少次。如果没有,TryGetValue 将使用 0 的 seen。我们增加 seen。我们来看看它与迄今为止我们看到的最大值的比较:

更重要的是——我们在“最频繁”的比赛中有一个新的领导者——清除当前的领导者名单,并以新的 n 作为领导者重新开始。还要注意新的最大值

这是相等的 - 我们领先并列;将当前的 n 添加到其同行中

少了——我们不在乎

  int[] nums = new[]  1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7 ;

  int maxSeen = int.MinValue;
  var seens = new Dictionary<int, int>();
  var maxes = new List<int>();

  foreach(var n in nums)
      seens.TryGetValue(n, out var seen);
      seens[n] = ++seen;

      if(seen > maxSeen)
          maxes = new()n;
          maxSeen = seen;
       else if(seen == maxSeen)
          maxes.Add(n);
  

您最终会得到maxes 作为List&lt;int&gt;,这是出现最多的数字列表。

如果你关心列表内部数组的分配,你可以考虑清除列表而不是newing;我new'd 因为在新领导者中使用初始化程序很方便

【讨论】:

在一个特别乏味的电话会议进行到一半时,我也想到了类似的想法,但我目前无法修改它 @mjwills 类似的东西已经实现了【参考方案7】:

你可以先这样分组第一个输入。

 int[] input =  1, 1, 1, 3, 5, 5, 6, 6, 6, 7, 8, 8 ;

 var tmpResult = from i in input
     group i by i into k
     select new
     
          k.Key,
          count = k.Count()
     ;

然后你可以像这样过滤组的最大值;

var max = tmpResult.Max(s => s.count);

你应该做一个过滤器就足够了

 int[] result = tmpResult.Where(f => f.count == max).Select(s => s.Key).ToArray();

你也可以为此创建一个扩展方法。

public static class Extension

    public static int[] GetMostFrequent(this int[] input)
    
        var tmpResult = from i in input
                        group i by i into k
                        select new
                        
                            k.Key,
                            count = k.Count()
                        ;

        var max = tmpResult.Max(s => s.count);

        return tmpResult.Where(f => f.count == max).Select(s => s.Key).ToArray();
    

【讨论】:

【参考方案8】:

你们很亲密。只需在您的代码中再添加一行即可。

int[] input =  1, 1, 1, 3, 5, 5, 6, 6, 6, 7, 8, 8 ;

var counts = input
    .GroupBy(i => i)
    .Select(i => new  Number = i.Key, Count = i.Count())
    .OrderByDescending(i => i.Count);
            
var maxCount = counts.First().Count;                
var result = counts
    .Where(i=> i.Count == maxCount)
    .Select(i => i.Number);

结果

1,6

【讨论】:

我建议在这里使用值元组而不是匿名类型。 @GuruStron 谢谢!我会考虑的。

以上是关于我想使用 LINQ 获得最常见的值的主要内容,如果未能解决你的问题,请参考以下文章

如何获得Int数组中最常见的值? (C#)

使用 Linq 反对,我怎样才能获得基于同一列表中另一个字段的值

如何使用 LINQ to SQL 在计算列上获得不同的值?

用 linq 查找和替换列表的结果

SmartSql 常见问题

具有多个选项卡的 Linq to Excel [重复]