使用Linq获取集合的最后N个元素?

Mat*_*ves 268 c# linq

给定一个集合,有没有办法获得该集合的最后N个元素?如果框架中没有方法,那么编写扩展方法的最佳方法是什么?

kbr*_*ton 397

collection.Skip(Math.Max(0, collection.Count() - N));
Run Code Online (Sandbox Code Playgroud)

此方法保留项目顺序,而不依赖于任何排序,并且具有跨多个LINQ提供程序的广泛兼容性.

务必注意不要拨打Skip负号.某些提供程序(如实体框架)在显示负参数时将生成ArgumentException.呼吁Math.Max避免这种整洁.

下面的类包含扩展方法的所有基本知识,包括:静态类,静态方法和this关键字的使用.

public static class MiscExtensions
{
    // Ex: collection.TakeLast(5);
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
    {
        return source.Skip(Math.Max(0, source.Count() - N));
    }
}
Run Code Online (Sandbox Code Playgroud)

关于绩效的简要说明:

由于调用Count()可能导致某些数据结构的枚举,因此这种方法可能会导致两次数据传递.对于大多数可枚举来说,这不是一个真正的问题; 实际上,Lists,Arrays甚至EF查询已经存在优化,以便Count()在O(1)时间内评估操作.

但是,如果你必须使用一个只进枚举,并想避免两遍,考虑像一个通算法的Lasse五卡尔森马克拜尔斯描述.这两种方法都使用临时缓冲区来保存枚举时的项目,一旦找到集合的末尾就会产生这些项目.

  • 我忽略了`.Take(N)`的意思 - 没有它似乎工作得很好...... (27认同)
  • 取决于收集的性质.Count()可能是O(N). (10认同)
  • 做了一些基准测试.事实证明,LINQ to Objects会根据您正在使用的集合类型执行一些优化.使用数组,`List`s和`LinkedList`,James的解决方案往往更快,但不是一个数量级.如果计算IEnumerable(通过Enumerable.Range,例如),James的解决方案需要更长的时间.在没有了解实现或将值复制到不同数据结构的情况下,我想不出任何保证单次传递的方法. (4认同)
  • @James:绝对正确.如果严格处理IEnumerable集合,则可以是两遍查询.我对看到有保证的1遍算法非常感兴趣.它可能有用. (3认同)
  • +1,因为这适用于Linq to Entities/SQL.我猜它在Linq to Objects中的表现也比James Curran的策略更高效. (2认同)

Jam*_*ran 58

coll.Reverse().Take(N).Reverse().ToList();


public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
    return coll.Reverse().Take(N).Reverse();
}
Run Code Online (Sandbox Code Playgroud)

更新:解决clintp的问题:a)使用上面定义的TakeLast()方法解决了问题,但是如果你真的想要在没有额外方法的情况下做到这一点,那么你只需要认识到Enumerable.Reverse()可以是用作扩展方法,您不需要以这种方式使用它:

List<string> mystring = new List<string>() { "one", "two", "three" }; 
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
Run Code Online (Sandbox Code Playgroud)


ang*_*son 44

注意:我错过了你的问题标题,说使用Linq,所以我的答案实际上并没有使用Linq.

如果你想避免缓存整个集合的非懒副本,你可以写,做它用一个链表的简单方法.

以下方法将在原始集合中找到的每个值添加到链接列表中,并将链接列表修剪为所需的项目数.由于它不断修剪到这个数量的项目通过遍历集合的整个时间的链表,它只会保留一个副本从原始集合最N项.

它不需要您知道原始集合中的项目数,也不需要多次迭代它.

用法:

IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
Run Code Online (Sandbox Code Playgroud)

扩展方法:

public static class Extensions
{
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
        int n)
    {
        if (collection == null)
            throw new ArgumentNullException("collection");
        if (n < 0)
            throw new ArgumentOutOfRangeException("n", "n must be 0 or greater");

        LinkedList<T> temp = new LinkedList<T>();

        foreach (var value in collection)
        {
            temp.AddLast(value);
            if (temp.Count > n)
                temp.RemoveFirst();
        }

        return temp;
    }
}
Run Code Online (Sandbox Code Playgroud)


Mar*_*ers 29

这是一个适用于任何可枚举但只使用O(N)临时存储的方法:

public static class TakeLastExtension
{
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int takeCount)
    {
        if (source == null) { throw new ArgumentNullException("source"); }
        if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
        if (takeCount == 0) { yield break; }

        T[] result = new T[takeCount];
        int i = 0;

        int sourceCount = 0;
        foreach (T element in source)
        {
            result[i] = element;
            i = (i + 1) % takeCount;
            sourceCount++;
        }

        if (sourceCount < takeCount)
        {
            takeCount = sourceCount;
            i = 0;
        }

        for (int j = 0; j < takeCount; ++j)
        {
            yield return result[(i + j) % takeCount];
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
Run Code Online (Sandbox Code Playgroud)

它的工作原理是使用大小为N的环形缓冲区来存储它们看到的元素,用新的元素覆盖旧元素.当到达可枚举的末尾时,环形缓冲区包含最后N个元素.

  • +1:这应该比我的性能更好,但是当集合包含的元素少于`n`时,你应该确保它做正确的事情. (2认同)
  • 可能的优化是检查可枚举实现的IList,如果是,则使用简单的解决方案.然后只需要临时存储方法来真正"流式传输"IEnumerables (2认同)

Ray*_*ega 20

.NET Core 2.0提供LINQ方法TakeLast():

https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast

例如:

Enumerable
    .Range(1, 10)
    .TakeLast(3) // <--- takes last 3 items
    .ToList()
    .ForEach(i => System.Console.WriteLine(i))

// outputs:
// 8
// 9
// 10
Run Code Online (Sandbox Code Playgroud)

  • 这些现在需要更高。无需重新发明轮子 (3认同)

Nic*_*ock 11

我很惊讶没有人提到它,但SkipWhile确实有一个使用元素索引的方法.

public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("Source cannot be null");

    int goldenIndex = source.Count() - n;
    return source.SkipWhile((val, index) => index < goldenIndex);
}

//Or if you like them one-liners (in the spirit of the current accepted answer);
//However, this is most likely impractical due to the repeated calculations
collection.SkipWhile((val, index) => index < collection.Count() - N)
Run Code Online (Sandbox Code Playgroud)

此解决方案与其他解决方案相比,唯一可感知的好处是,您可以选择添加谓词以生成更强大,更高效的LINQ查询,而不是两次单独的操作遍历IEnumerable两次.

public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred)
{
    int goldenIndex = source.Count() - n;
    return source.SkipWhile((val, index) => index < goldenIndex && pred(val));
}
Run Code Online (Sandbox Code Playgroud)


pie*_*rs7 8

在RX的System.Interactive程序集中使用EnumerableEx.TakeLast.它是像@ Mark's一样的O(N)实现,但它使用队列而不是环形缓冲区构造(并在达到缓冲区容量时将项目出列).

(注意:这是IEnumerable版本 - 不是IObservable版本,尽管两者的实现完全相同)


Ric*_*lay 5

如果你不介意作为monad的一部分浸入Rx,你可以使用TakeLast:

IEnumerable<int> source = Enumerable.Range(1, 10000);

IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Run Code Online (Sandbox Code Playgroud)

  • 如果引用RX的System.Interactive而不是System.Reactive,则不需要AsObservable()(请参阅我的回答) (2认同)

dav*_*v_i 5

如果您正在处理带有密钥的集合(例如,来自数据库的条目),那么快速(即比所选答案更快)解决方案将是

collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
Run Code Online (Sandbox Code Playgroud)