【问题标题】:I want to get most frequent values using LINQ我想使用 LINQ 获得最常见的值
【发布时间】:2023-03-29 14:02:01
【问题描述】:

我正在尝试使用 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 计数。)

标签: c# arrays linq


【解决方案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 谢谢!我会考虑的。
            猜你喜欢
            • 2011-10-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-10-18
            • 2011-04-13
            • 1970-01-01
            相关资源
            最近更新 更多