如何使用 LinQ 获取列表的前 N 个元素?

7

我有一个按照考试分数排序的列表,我想要获取这个列表中前N个元素。
如果第N个和第N+1个学生的考试分数相同,则列表必须包含它们两个。

例如,我有一个像这样的列表:

john.   80  
mike.   75  
james.  70  
ashley. 70
kate.   60

前三名应该返回john、mike、james、ashley
我尝试了Take(),但它只返回john、mike、james

英语不是我的主要语言,如果我没有说清楚,抱歉
谢谢


2
你的意思基本上是在 SQL 中的 WITH TIES,是吗? - Marc Gravell
Take(n) 不考虑平局解决方法。 - Marco
如果有超过两个学生的分数相同怎么办?你会全部录取吗? - Shaharyar
没有WITH TIES的本地LINQ支持。有几种方法可以实现这一点。请参见上面引用的重复问题。 - VoteCoffee
1
@AhmetEmre90:您能否通过编辑您的问题并提供更有意义的示例数据来澄清您的要求?例如,如果有两个80、两个70和两个60,您是想得到六个项目还是想得到两个80 + 两个70? - Tim Schmelter
显示剩余2条评论
5个回答

10

这是一个仅需要单次遍历的实现:

public static IEnumerable<TSource> TopWithTies<TSource, TValue>(
    this IEnumerable<TSource> source,
    int count,
    Func<TSource, TValue> selector)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    if (count < 0) throw new ArgumentOutOfRangeException("count");
    if (count == 0) yield break;
    using(var iter = source.OrderByDescending(selector).GetEnumerator())
    {
        if(iter.MoveNext())
        {
            yield return iter.Current;
            while (--count >= 0)
            {
                if(!iter.MoveNext()) yield break;
                yield return iter.Current;    
            }
            var lastVal = selector(iter.Current);
            var eq = EqualityComparer<TValue>.Default;
            while(iter.MoveNext() && eq.Equals(lastVal, selector(iter.Current)))
            {
                yield return iter.Current;
            }
        }
    }
}

使用示例:

var data = new[]
{
    new { name = "john", value = 80 },
    new { name = "mike", value = 75 },
    new { name = "james", value = 70 },
    new { name = "ashley", value = 70 },
    new { name = "kate", value = 60 }
};
var top = data.TopWithTies(3, x => x.value).ToList();
foreach(var row in top)
{
    Console.WriteLine("{0}: {1}", row.name, row.value);
}

这并不是一次遍历,因为 OrderByDescending 本身会执行 n*Log(n) 的排序。 - Charlieface

3
如果有超过两个学生的成绩相同,你会选择他们吗? OP:是的。
您可以按分数分组,然后使用 OrderByDescending + Take + SelectMany
var topThreePoints = users.GroupBy(u => u.Points)
                          .OrderByDescending(g => g.Key)
                          .Take(3)
                          .SelectMany(g => g);

3
这只针对前三组,如果第一组有200个项目,那么您将不会从其他任何组中选取。 - Marc Gravell
@Marc,我不理解你的评论。如果第一组有200个项目,并且SelectMany()从前3个组中进行投影,那么结果肯定会包括这200个项目以及其他两个组中的项目,对吧? - Frédéric Hamidi
1
如果意图是“TOP WITH TIES”,那么不行。@FrédéricHamidi - Marc Gravell
@MarcGravell:也许我对问题的理解有误。但我猜测 OP 希望将所有平局都包括在内。 - Tim Schmelter
1
就我所知,我和Tim一样理解了这个问题。另外请注意,提问者从未承认过Marc的评论,也许并不是真的在寻找WITH TIES的解决方案——这一点并不清楚。 - Frédéric Hamidi

2
你可能想要做的是:
  1. 获取第n个
  2. 当>=第n个时获取全部
即:
var nth = users.Skip(n-1).FirstOrDefault()
var top = users.TakeWhile(user => user.Score >= nth.Score)

(这假设列表按降序排列,就像问题中给出的示例一样。如果输入列表中的元素小于n,则会引发错误)

0

可能是这样吗?

list.TakeWhile((item, index) => index < N || list[index] == list[index + 1]);

1
这看起来很不错,但它可能会索引list[list.Length]。我认为list[index] == list[index - 1]没有这个问题,因为当index为0时,index < N成立。 - Lumen

0

我在LINQPad中创建了一个示例案例。

var a = new List<Tuple<string,int>>();
a.Add(new Tuple<string,int>("john",80));
a.Add(new Tuple<string,int>("mike",75));
a.Add(new Tuple<string,int>("james",70));
a.Add(new Tuple<string,int>("ashley",70 ));
a.Add(new Tuple<string,int>("kate",60  ));

a.Where(x=>x.Item2>=a.OrderBy(i=>i.Item2).Skip(2).Take(1).SingleOrDefault ().Item2).Dump();

虽然不知道它是否足够高效。


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