我想使用 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()
可能会更高效 - 但前提是检测表明排序导致性能问题.
【讨论】:
你可能能够使用MoreLinq
的MaxBy
来避免完整的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<int>
,这是出现最多的数字列表。
如果你关心列表内部数组的分配,你可以考虑清除列表而不是new
ing;我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 获得最常见的值的主要内容,如果未能解决你的问题,请参考以下文章