给定一个集合,有没有办法获取该集合的最后 N 个元素?如果框架中没有方法,那么编写扩展方法来执行此操作的最佳方法是什么?
19 回答
collection.Skip(Math.Max(0, collection.Count() - N));
这种方法在不依赖任何排序的情况下保留项目顺序,并且在多个 LINQ 提供程序之间具有广泛的兼容性。
重要的是要注意不要Skip用负数打电话。某些提供程序(例如实体框架)在出现否定参数时会产生 ArgumentException。调用 toMath.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));
}
}
关于性能的简要说明:
因为调用Count()可能会导致某些数据结构的枚举,所以这种方法有导致数据两次传递的风险。对于大多数可枚举项来说,这并不是真正的问题。事实上,列表、数组甚至 EF 查询已经存在优化,可以Count()在 O(1) 时间内评估操作。
但是,如果您必须使用仅向前可枚举并且希望避免进行两次传递,请考虑使用像Lasse V. Karlsen或Mark Byers描述的一次性算法。这两种方法都在枚举时使用临时缓冲区来保存项目,一旦找到集合的结尾,就会产生这些项目。
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。
如果您想避免缓存整个集合的非惰性副本,您可以编写一个使用链表的简单方法。
下面的方法会将它在原始集合中找到的每个值添加到一个链表中,并将链表修剪到所需的项目数。由于它在遍历集合的整个过程中将链表修剪为该数量的项目,因此它将仅保留原始集合中最多 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(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(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;
}
}
.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
这是一种适用于任何可枚举但仅使用 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)
与其他解决方案相比,此解决方案提供的唯一可感知的好处是,您可以选择添加谓词以进行更强大和更高效的 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。这是一个像@Mark's 的 O(N) 实现,但它使用队列而不是环形缓冲区构造(并在达到缓冲区容量时将项目出列)。
(注意:这是 IEnumerable 版本 - 不是 IObservable 版本,尽管两者的实现几乎相同)
如果您正在处理带有键的集合(例如数据库中的条目),那么快速(即比所选答案更快)的解决方案将是
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
如果你不介意将 Rx 作为 monad 的一部分,你可以使用TakeLast:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
如果使用第三方库是一个选项,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 个效率有点低,因为上述所有解决方案都需要遍历集合。TakeLast(int n)中System.Interactive也有这个问题。
如果您有一个列表,更有效的做法是使用以下方法对其进行切片
/// 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;
}
和一些测试用例
[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<> 类型的集合并且您不关心返回集合的顺序,那么此方法的工作速度更快。我使用了Mark Byers 的答案并做了一些改动。所以现在方法 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];
}
}
}
对于测试,我使用了Mark Byers 方法和 kbrimington's andswer。这是测试:
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 个元素的结果是:
这是我的解决方案:
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();
}
}
}
}
代码有点笨重,但作为一个可重复使用的插入式组件,它应该在大多数情况下都表现得很好,并且它会保持使用它的代码简洁明了。:-)
我TakeLast的 for non-IList`1基于与@Mark Byers 和@MackieChan 的答案相同的环形缓冲区算法。有趣的是它们有多么相似——我完全独立地写了我的。猜猜实际上只有一种方法可以正确地执行环形缓冲区。:-)
查看@kbrimington 的答案,可以为此添加一个额外的检查,IQuerable<T>以回退到与实体框架配合良好的方法——假设我目前所拥有的没有。
下面的真实示例如何从集合(数组)中获取最后 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 ;
}
使用循环缓冲区的实现略有不同。基准测试表明,该方法比使用Queue(在System.Linq中实现TakeLast)的方法快大约两倍,但并非没有成本 - 它需要一个随着请求的元素数量而增长的缓冲区,即使你有一个小集合可以获得巨大的内存分配。
public IEnumerable<T> TakeLast<T>(IEnumerable<T> source, int count)
{
int i = 0;
if (count < 1)
yield break;
if (source is IList<T> listSource)
{
if (listSource.Count < 1)
yield break;
for (i = listSource.Count < count ? 0 : listSource.Count - count; i < listSource.Count; i++)
yield return listSource[i];
}
else
{
bool move = true;
bool filled = false;
T[] result = new T[count];
using (var enumerator = source.GetEnumerator())
while (move)
{
for (i = 0; (move = enumerator.MoveNext()) && i < count; i++)
result[i] = enumerator.Current;
filled |= move;
}
if (filled)
for (int j = i; j < count; j++)
yield return result[j];
for (int j = 0; j < i; j++)
yield return result[j];
}
}
//detailed code for the problem
//suppose we have a enumerable collection 'collection'
var lastIndexOfCollection=collection.Count-1 ;
var nthIndexFromLast= lastIndexOfCollection- N;
var desiredCollection=collection.GetRange(nthIndexFromLast, N);
---------------------------------------------------------------------
// use this one liner
var desiredCollection=collection.GetRange((collection.Count-(1+N)), N);
老实说,我对答案并不感到非常自豪,但是对于小型收藏,您可以使用以下内容:
var lastN = collection.Reverse().Take(n).Reverse();
有点hacky,但它可以完成工作;)