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



我正在尝试使用 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}

请仔细阅读问题并回答。

谢谢和问候。

只是为了补充过多的答案:

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()可能会更高性能 - 但前提是检测指示排序导致性能问题。

如果要在一个查询中在纯 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 中引入的。

您可以将结果存储在 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

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

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

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

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

我想你可能想使用 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 的seen为 0。我们递增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;我new,因为与新领导者一起使用初始值设定项是一个方便的衬里

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

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

你非常接近。只需在代码中再添加一行即可。

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}

最新更新