我想使用LINQ获取最常见的值。

3
我将尝试使用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。
请仔细阅读。 这是一个与使用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}。
请仔细阅读问题并回答。
谢谢和问候。

1
你取了五个元素,怎么可能只期望输出包含两个元素呢?看起来你需要过滤那些计数等于最大计数的元素。 - Franz Gleichmann
8个回答

9

仅补充一下众多答案:

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()


1
可能 可以使用 MoreLinqMaxBy 来避免完整的 OrderByDescending 的开销。 - mjwills
@mjwills 是的,那是个好主意,但请注意对于 .net Core 3.1 及更高版本,OrderByDescending() 后跟 First() 实际上被优化为 O(N),因此使用 MaxBy() 不会带来任何性能优势。 - Matthew Watson
在那种情况下,我纠正了! - mjwills
1
这个并没有很好的记录,所以你不能依赖它,但至少在这里有一些东西:https://github.com/dotnet/runtime/issues/14867 ... 实际上考虑一下,也许这种优化只适用于 OrderBy(),所以 MaxBy() 仍然是一个好主意!我会去检查一下 - 敬请关注 ;) - Matthew Watson
1
更新:我已确认我提到的优化在 OrderByDescending() 中也存在。 - Matthew Watson
1
@MatthewWatson 这个链接似乎也与此相关。 - Guru Stron

3
如果你想在一个查询中使用纯 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();

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

更新

使用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);

同时,您也可以使用来自MoreLinqMaxBy,或者在.NET 6中引入的一种方法。


1
你可以将结果存储在一个元组的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


1

我认为您可能想使用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的情况下一次完成此操作,可以使用Dictionary和List跟踪以下内容:
a)您看到一个值的次数 b)您看到最多次数的值 c)您看到那么多次的其他最多值
我们跳过列表,尝试在字典中查找当前值。它要么有效,要么无效 - 如果有效,则TryGetValue告诉我们当前值已经被看到了多少次。如果无效,则TryGetValue为我们提供了一个“seen”的值为0。我们增加了“seen”的值。我们看一下它与我们迄今为止看到的最大值进行比较的情况:
  • It's greater - we have a new leader in the "most frequent" contest - clear the current leaders list and start over with the new n as the leader. Also note the new max

  • It's equal - we have a tie for the lead; add the current n in among its peers

  • It's less - we don't care

      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);
      }
    
你最终会得到一个名为 maxesList<int>,其中包含出现最多的数字列表。
如果你关心 List 内部数组的分配情况,可以考虑清除列表而不是使用 new;我使用 new 是因为它是一种方便的一行代码,可以使用新的 leader 进行初始化。

在一次特别乏味的电话会议进行到一半时,我也有类似的想法,但目前我没有修改它的位置。 - Caius Jard
@mjwills 已经实现了类似的功能。 - Caius Jard

1

使用一个变量来捕获第一个项目的数量,然后使用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();

1
您可以考虑添加一个扩展方法。例如:
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;
        }
    }
}

这样做的好处是高效,无需多次迭代集合。但需要编写更多的代码,即使这可以隐藏在扩展方法中。

0

你可以先像这样对第一个输入进行分组。

 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();
    }

0
你很靠近了。只需在你的代码中再添加一行就可以了。
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}

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接