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

鉴于一个集合,有没有办法获得该集合的最后N个元素? 如果在框架中没有方法,写一个扩展方法来做这件事最好的办法是什么?

collection.Skip(Math.Max(0, collection.Count() - N)); 

这种方法保留了项目顺序而不依赖于任何sorting,并且在几个LINQ提供者之间具有广泛的兼容性。

小心不要拨打负号码Skip 。 某些提供程序(如entity framework)在出现负面参数时将产生一个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)); } } 

关于performance的简要说明:

由于对Count()的调用可能导致某些数据结构的枚举,因此这种方法有导致数据传输两次的风险。 这对于大多数枚举来说并不是一个问题。 实际上,对于列表,数组,甚至EF查询,已经存在优化来评估O(1)时间内的Count()操作。

但是,如果您必须使用只向前的枚举,并且希望避免进行两次通过,请考虑像Lasse V. Karlsen或Mark Byers所描述的一次通过algorithm。 这两种方法在枚举时都使用临时缓冲区来存放物品,一旦find集合的末尾,就会产生这些物品。

 coll.Reverse().Take(N).Reverse().ToList(); public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N) { return coll.Reverse().Take(N).Reverse(); } 

更新:为了解决clintp的问题:a)使用我上面定义的TakeLast()方法解决了这个问题,但是如果你真的想要这样做没有额外的方法,那么你只需要认识到,虽然Enumerable.Reverse()可以用作扩展方法,您不需要这样使用它:

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

注意 :我错过了使用Linq的问题标题,所以我的回答实际上并不使用Linq。

如果你想避免caching整个集合的非懒惰副本,你可以编写一个简单的方法,使用链接列表。

下面的方法会将它在原始集合中find的每个值添加到链接列表中,并将链接列表修剪成所需的项目数。 由于它通过遍历集合来保持整个链接列表被修剪到这个数目的项目,所以它将只保留最多N个来自原始集合的项目的副本。

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

用法:

 IEnumerable<int> sequence = Enumerable.Range(1, 10000); IEnumerable<int> last10 = sequence.TakeLast(10); ... 

扩展方法:

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

这里有一个方法适用于任何枚举,但只使用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]; } } } 

用法:

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

它通过使用大小为N的环形缓冲区来存储元素,因为它看到它们,用新元素覆盖旧元素。 当到达枚举的末尾时,环形缓冲区包含最后的N个元素。

我很惊讶没有人提到它,但是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) 

这个解决scheme唯一可以感觉到的好处是你可以select添加一个谓词来创build一个更强大,更有效率的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)); } 

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

(注意:这是IEnumerable版本 – 不是IObservable版本,尽pipe两者的实现几乎完全相同)

如果您不介意将Rx作为monad的一部分,可以使用TakeLast

 IEnumerable<int> source = Enumerable.Range(1, 10000); IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable(); 

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

 collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key); 

如果使用第三方库是一个选项, MoreLinq定义了TakeLast() ,它正是这样做的。

我试图将效率和简单性结合起来,

 public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count) { if (source == null) { throw new ArgumentNullException("source"); } Queue<T> lastElements = new Queue<T>(); foreach (T element in source) { lastElements.Enqueue(element); if (lastElements.Count > count) { lastElements.Dequeue(); } } return lastElements; } 

关于性能:在C#中, Queue<T>是使用循环缓冲区实现的,所以每个循环都没有对象实例化(只有在队列长大的时候)。 我没有设置队列容量(使用专用构造函数),因为有人可能用count = int.MaxValue来调用这个扩展。 为了获得额外的性能,你可以检查源代码是否实现IList<T> ,如果是,直接使用数组索引提取最后的值。

使用LINQ获取集合的最后一个N是有点低效的,因为以上所有的解决scheme都需要迭代集合。 System.Interactive TakeLast(int n)也有这个问题。

如果你有一个列表,更有效率的事情是使用下面的方法进行分片

 /// Select from start to end exclusive of end using the same semantics /// as python slice. /// <param name="list"> the list to slice</param> /// <param name="start">The starting index</param> /// <param name="end">The ending index. The result does not include this index</param> public static List<T> Slice<T> (this IReadOnlyList<T> list, int start, int? end = null) { if (end == null) { end = list.Count(); } if (start < 0) { start = list.Count + start; } if (start >= 0 && end.Value > 0 && end.Value > start) { return list.GetRange(start, end.Value - start); } if (end < 0) { return list.GetRange(start, (list.Count() + end.Value) - start); } if (end == start) { return new List<T>(); } throw new IndexOutOfRangeException( "count = " + list.Count() + " start = " + start + " end = " + end); } 

 public static List<T> GetRange<T>( this IReadOnlyList<T> list, int index, int count ) { List<T> r = new List<T>(count); for ( int i = 0; i < count; i++ ) { int j=i + index; if ( j >= list.Count ) { break; } r.Add(list[j]); } return r; } 

和一些testing用例

 [Fact] public void GetRange() { IReadOnlyList<int> l = new List<int>() { 0, 10, 20, 30, 40, 50, 60 }; l .GetRange(2, 3) .ShouldAllBeEquivalentTo(new[] { 20, 30, 40 }); l .GetRange(5, 10) .ShouldAllBeEquivalentTo(new[] { 50, 60 }); } [Fact] void SliceMethodShouldWork() { var list = new List<int>() { 1, 3, 5, 7, 9, 11 }; list.Slice(1, 4).ShouldBeEquivalentTo(new[] { 3, 5, 7 }); list.Slice(1, -2).ShouldBeEquivalentTo(new[] { 3, 5, 7 }); list.Slice(1, null).ShouldBeEquivalentTo(new[] { 3, 5, 7, 9, 11 }); list.Slice(-2) .Should() .BeEquivalentTo(new[] {9, 11}); list.Slice(-2,-1 ) .Should() .BeEquivalentTo(new[] {9}); } 

我知道要回答这个问题迟到了。 但是,如果您正在处理IList <>types的集合,并且不关心返回的集合的顺序,那么此方法运行得更快。 我已经使用了马克·拜尔斯的回答,并做了一些修改。 所以现在方法TakeLast是:

 public static IEnumerable<T> TakeLast<T>(IList<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; } if (source.Count > takeCount) { for (int z = source.Count - 1; takeCount > 0; z--) { takeCount--; yield return source[z]; } } else { for(int i = 0; i < source.Count; i++) { yield return source[i]; } } } 

为了testing,我使用了Mark Byers方法和kbrimington的和 。 这是testing:

 IList<int> test = new List<int>(); for(int i = 0; i<1000000; i++) { test.Add(i); } Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); IList<int> result = TakeLast(test, 10).ToList(); stopwatch.Stop(); Stopwatch stopwatch1 = new Stopwatch(); stopwatch1.Start(); IList<int> result1 = TakeLast2(test, 10).ToList(); stopwatch1.Stop(); Stopwatch stopwatch2 = new Stopwatch(); stopwatch2.Start(); IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList(); stopwatch2.Stop(); 

这里有10个元素的结果:

在这里输入图像描述

并取得1000001个元素的结果是: 在这里输入图像描述

这是我的解决scheme:

 public static class EnumerationExtensions { public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count) { if (count <= 0) yield break; var inputList = input as IList<T>; if (inputList != null) { int last = inputList.Count; int first = last - count; if (first < 0) first = 0; for (int i = first; i < last; i++) yield return inputList[i]; } else { // Use a ring buffer. We have to enumerate the input, and we don't know in advance how many elements it will contain. T[] buffer = new T[count]; int index = 0; count = 0; foreach (T item in input) { buffer[index] = item; index = (index + 1) % buffer.Length; count++; } // The index variable now points at the next buffer entry that would be filled. If the buffer isn't completely // full, then there are 'count' elements preceding index. If the buffer *is* full, then index is pointing at // the oldest entry, which is the first one to return. // // If the buffer isn't full, which means that the enumeration has fewer than 'count' elements, we'll fix up // 'index' to point at the first entry to return. That's easy to do; if the buffer isn't full, then the oldest // entry is the first one. :-) // // We'll also set 'count' to the number of elements to be returned. It only needs adjustment if we've wrapped // past the end of the buffer and have enumerated more than the original count value. if (count < buffer.Length) index = 0; else count = buffer.Length; // Return the values in the correct order. while (count > 0) { yield return buffer[index]; index = (index + 1) % buffer.Length; count--; } } } public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count) { if (count <= 0) return input; else return input.SkipLastIter(count); } private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count) { var inputList = input as IList<T>; if (inputList != null) { int first = 0; int last = inputList.Count - count; if (last < 0) last = 0; for (int i = first; i < last; i++) yield return inputList[i]; } else { // Aim to leave 'count' items in the queue. If the input has fewer than 'count' // items, then the queue won't ever fill and we return nothing. Queue<T> elements = new Queue<T>(); foreach (T item in input) { elements.Enqueue(item); if (elements.Count > count) yield return elements.Dequeue(); } } } } 

这个代码有点笨重,但是作为一个可重用的组件,在大多数情况下它的performance应该是一样的,而且它会保持使用它的代码的简洁。 🙂

我的TakeLast for non- IList`1基于与@Mark Byers和@MackieChan的答案相同的环缓冲algorithm。 有趣的是他们有多类似 – 我是完全独立写的。 猜猜真的只有一种方法来正确地做环形缓冲区。 🙂

看@ kbrimington的答案,额外的检查可以被添加到这个IQuerable<T>回落到entity framework的工作方式 – 假设我在这一点上没有。

在真正的例子下面如何从集合(数组)中取最后3个元素:

 // split address by spaces into array string[] adrParts = adr.Split(new string[] { " " },StringSplitOptions.RemoveEmptyEntries); // take only 3 last items in array adrParts = adrParts.SkipWhile((value, index) => { return adrParts.Length - index > 3; }).ToArray(); 

使用此方法获得所有范围没有错误

  public List<T> GetTsRate( List<T> AllT,int Index,int Count) { List<T> Ts = null; try { Ts = AllT.ToList().GetRange(Index, Count); } catch (Exception ex) { Ts = AllT.Skip(Index).ToList(); } return Ts ; }