给定一个集合,有没有办法得到该集合的最后N个元素?如果框架中没有方法,那么编写一个扩展方法来实现这个目的的最佳方式是什么?
当前回答
我的解决方案是基于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提出的解决方案在内存分配和原始性能方面是最有效的。
其他回答
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();
如果你不介意将Rx作为单子的一部分,你可以使用TakeLast:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
使用LINQ获取集合的最后N有点低效,因为所有上述解决方案都需要遍历集合。TakeLast(int n) in 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);
}
with
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});
}
如果可以选择使用第三方库,则MoreLinq定义了TakeLast()。
老实说,我对这个答案并不是特别自豪,但对于小的集合,你可以使用以下方法:
var lastN = collection.Reverse().Take(n).Reverse();
有点俗气,但它的工作;)