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


当前回答

下面是一个方法,它适用于任何枚举对象,但只使用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个元素。

其他回答

我的解决方案是基于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提出的解决方案在内存分配和原始性能方面是最有效的。

使用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});
}

我知道现在回答这个问题已经太晚了。但是,如果您正在处理类型为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个元素的结果为:

下面是一个方法,它适用于任何枚举对象,但只使用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个元素。

使用EnumerableEx。在RX的系统中使用last。交互式装配。这是一个类似于@Mark的O(N)实现,但它使用队列而不是环形缓冲区结构(并且当达到缓冲区容量时将项目从队列中移除)。

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