如何使用LINQ查找最小值

49

我有一个 class A { public float Score; ... } 和一个 IEnumerable<A> items,想要找到具有最小分数的 A 实例。

使用 items.Min(x => x.Score) 只能得到最小分数而不能得到具有最小分数的实例。

如何通过只迭代一次数据来获取实例呢?

编辑:目前有三种主要解决方案:

  • 编写扩展方法(由Svish提出)。优点:易于使用并且每个项目仅评估一次分数。 缺点:需要扩展方法。(我选择了这个解决方案应用在我的工程中。)

  • 使用Aggregate(由Daniel Renshaw提出)。优点:使用内置的LINQ方法。缺点:对于不熟悉的人稍微有些难以理解,并且会多次调用评估器。

  • 实现IComparable(由cyberzed提出)。优点:可以直接使用Linq.Min。缺点:固定为一个比较器 - 在执行最小计算时无法自由选择比较器。


2
不完全正确。仅知道索引并不能直接帮助IEnumerable。在某些情况下,必须再次使用索引进行迭代以找到所需的实例。 - Danvil
2
.NET 6现在支持 items.MinBy(x => x.Score),详见重复内容。 - Petr R.
7个回答

76

使用聚合函数:

items.Aggregate((c, d) => c.Score < d.Score ? c : d)

如建议的一样,使用更友好的名称,确切地编写同一行:

items.Aggregate((minItem, nextItem) => minItem.Score < nextItem.Score ? minItem : nextItem)

4
有趣的回答。尽管如此,它相当难懂——如果我不知道它在做什么,我可能会对那段代码的意图感到困惑。 - Jamie Penney
Jamie:我同意,它符合Danvil的要求,但对于不熟悉Haskell或F#等函数式语言的人来说,解析可能会很困难。 - Daniel Renshaw
1
@Jamie - 当行以 minItem = 开头时不要这样做。这有点棘手,因为你重新实现了 Min,但我认为这只是小问题。 - Kobi
这很不错 :) 它仅使用了 Linq 并且只迭代了一次。 - Danvil
31
对于 LINQ 新手来说,使用更有意义的变量名可能有助于理解这里正在发生的事情:items.Aggregate((minItem, nextItem) => minItem.Score < nextItem.Score ? minItem : nextItem) - DavidRR
这里是.NET Framework 4.5的文档,介绍了Aggregate方法的重载。示例展示了如何使用Aggregate方法来反转一个字符串数组中的项目顺序。 - DavidRR

23
尝试使用 items.OrderBy(s => s.Score).FirstOrDefault();

12
简洁明了,但要注意排序操作会比较“慢”。如果元素数量较大,建议进行基准测试以确保性能可接受。 - Paul Turner
6
这并不是只迭代一次!它的时间复杂度是O(n log n),而不是O(n)。 - Danvil
检查Svish的答案。我正要编写一个扩展方法,这正是Jon Skeet已经完成的。 - Jamie Penney
1
回复:“这不仅迭代一次!” 这是正确的。但它确实回答了我来到这个页面的问题:“如何使用linq查找最小值。” 是的,当订购大量项目时,将会付出代价。 - DavidRR

15
请查看MoreLINQ中的MinBy扩展方法(由Jon Skeet创建,现由Atif Aziz主要维护)。

整个方法体是否可以替换为以下代码?return source.Aggregate((c, d) => comparer.Compare(selector(c), selector(d)) < 0 ? c : d); - Daniel Renshaw
@Daniel:你得问Jon Skeet那个问题:p 当然,其中一些内容是错误检查,对于像这样的库来说,你当然希望拥有它。 - Svish
我看到了一点不同之处,在MinBy中,选择器仅针对每个实例调用一次,而在Aggregate中则调用更多次。 - Danvil
顺便提一下:MinBy<> 运算符也被 Rx 支持(http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx)。 - Frank

7
这可以通过简单迭代来解决:
float minScore = float.MaxValue;
A minItem = null;

foreach(A item in items)
{
   if(item.Score < minScore)
       minItem = item;
}

return minItem;

这并不是一个很好的LINQ查询,但它避免了排序操作并仅满足问题要求一次迭代列表。


当然。我想知道是否可以使用LINQ来表示此模式... - Danvil
答案是,LINQ没有提供MinItem操作。如果您希望拥有同样简洁的东西,可以将我的答案包装在扩展方法中。或者,如果您对列表进行两次遍历,也可以使用LINQ来完成它。 - Paul Turner
只是一个提醒,代码中的这一行 minItem = item; 应该改为 { minItem = item; minScore = item.Score; } - mosheb

4

在我看来,快速的方法是为您的A类实现IComparable接口(如果可能的话)。

class A : IComparable<A>

这是一个简单的实现,您需要编写CompareTo(A other)方法。可参考MSDN上的IEnumerable(of T).Min参考指南


这是一个好主意。但是如果我有Score1和Score2,并且想要更改用于查找最小值的分数,该怎么办? - Danvil
嗯,这取决于……某种程度上,我不介意对象知道定义它比较基准的方式,但我可以理解你希望采用另一种方式的概念。我认为Jon Skeet的方式对MinBy来说是显而易见的。 - cyberzed

3
一个小折叠就可以:
var minItem = items.Aggregate((acc, c) => acc.Score < c.Score? acc : c);

啊...太慢了。

1
杰米·佩尼得到了我的支持。你也可以说:
items.OrderBy(s => s.Score).Take(1);

使用Take(5)来获取最低的5个元素,等同于这个效果。


4
排序不仅仅迭代一次! - Danvil
2
回复:“排序不仅迭代一次!”这是正确的。但它确实回答了我来到这个页面的问题:“如何使用linq找到最小值。”是的,当对大量项目进行排序时,会付出一定的代价。 - DavidRR

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