给定一个集合,有没有办法得到该集合的最后N个元素?如果框架中没有方法,那么编写一个扩展方法来实现这个目的的最佳方式是什么?
当前回答
//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);
其他回答
使用这个方法得到所有范围没有错误
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 ;
}
我的解决方案是基于c#版本8中引入的范围。
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
{
return source.ToArray()[(source.Count()-N)..];
}
在用大多数评价的解决方案(以及我谦卑地提出的解决方案)运行了一个基准测试后:
public static class TakeLastExtension
{
public static IEnumerable<T> TakeLastMarkByers<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];
}
}
public static IEnumerable<T> TakeLastKbrimington<T>(this IEnumerable<T> source, int N)
{
return source.Skip(Math.Max(0, source.Count() - N));
}
public static IEnumerable<T> TakeLastJamesCurran<T>(this IEnumerable<T> source, int N)
{
return source.Reverse().Take(N).Reverse();
}
public static IEnumerable<T> TakeLastAlex<T>(this IEnumerable<T> source, int N)
{
return source.ToArray()[(source.Count()-N)..];
}
}
Test
[MemoryDiagnoser]
public class TakeLastBenchmark
{
[Params(10000)]
public int N;
private readonly List<string> l = new();
[GlobalSetup]
public void Setup()
{
for (var i = 0; i < this.N; i++)
{
this.l.Add($"i");
}
}
[Benchmark]
public void Benchmark1_MarkByers()
{
var lastElements = l.TakeLastMarkByers(3).ToList();
}
[Benchmark]
public void Benchmark2_Kbrimington()
{
var lastElements = l.TakeLastKbrimington(3).ToList();
}
[Benchmark]
public void Benchmark3_JamesCurran()
{
var lastElements = l.TakeLastJamesCurran(3).ToList();
}
[Benchmark]
public void Benchmark4_Alex()
{
var lastElements = l.TakeLastAlex(3).ToList();
}
}
Program.cs:
var summary = BenchmarkRunner.Run(typeof(TakeLastBenchmark).Assembly);
命令dotnet运行——project .\TestsConsole2。csproj -c Release——logBuildOutput
结果如下:
// *摘要* BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19044.1889/21H2/ novber2021update) AMD Ryzen 5 5600X, 1个CPU, 12个逻辑核和6个物理核 . net SDK = 6.0.401 [主机]:.NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2 DefaultJob: .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
Method | N | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|
Benchmark1_MarkByers | 10000 | 89,390.53 ns | 1,735.464 ns | 1,704.457 ns | - | - | 248 B |
Benchmark2_Kbrimington | 10000 | 46.15 ns | 0.410 ns | 0.363 ns | 0.0076 | - | 128 B |
Benchmark3_JamesCurran | 10000 | 2,703.15 ns | 46.298 ns | 67.862 ns | 4.7836 | 0.0038 | 80264 B |
Benchmark4_Alex | 10000 | 2,513.48 ns | 48.661 ns | 45.517 ns | 4.7607 | - | 80152 B |
事实证明,@Kbrimington提出的解决方案在内存分配和原始性能方面是最有效的。
collection.Skip(Math.Max(0, collection.Count() - N));
这种方法保留了项目的顺序,不依赖于任何排序,并且在多个LINQ提供者之间具有广泛的兼容性。
重要的是要注意不要使用负数调用Skip。一些提供程序,比如实体框架,会在提供一个否定的参数时产生一个ArgumentException。对数学的呼唤。马克斯巧妙地避免了这一点。
下面的类具有扩展方法的所有基本要素,即:静态类、静态方法和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()的调用可能导致某些数据结构的枚举,这种方法有导致两次数据传递的风险。对于大多数枚举对象来说,这并不是真正的问题;事实上,对于list、array甚至EF查询,已经有了优化,可以在O(1)时间内计算Count()操作。
但是,如果您必须使用只向前的枚举对象,并且希望避免进行两次传递,则可以考虑Lasse V. Karlsen或Mark Byers描述的一次传递算法。这两种方法都使用临时缓冲区来保存枚举时的项,一旦找到集合的末尾,就会产生这些项。
以下是我的解决方案:
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>添加一个额外的检查,以回到与实体框架一起工作的方法——假设我在这一点上没有。
我知道现在回答这个问题已经太晚了。但是,如果您正在处理类型为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的方法进行测试。这是测试:
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个元素的结果为:
推荐文章
- 实体框架核心:在上一个操作完成之前,在此上下文中开始的第二个操作
- 如何为构造函数定制Visual Studio的私有字段生成快捷方式?
- 如何使用JSON确保字符串是有效的JSON。网
- AppSettings从.config文件中获取值
- 通过HttpClient向REST API发布一个空体
- 如何检查IEnumerable是否为空或空?
- 自动化invokerrequired代码模式
- 在c#代码中设置WPF文本框的背景颜色
- 在c#中,什么是单子?
- c#和Java中的泛型有什么不同?和模板在c++ ?
- c#线程安全快速(est)计数器
- 如何将此foreach代码转换为Parallel.ForEach?
- 如何分裂()一个分隔字符串到一个列表<字符串>
- 如何转换列表<字符串>列表<int>?
- c#对象列表,我如何得到一个属性的和