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


当前回答

老实说,我对这个答案并不是特别自豪,但对于小的集合,你可以使用以下方法:

var lastN = collection.Reverse().Take(n).Reverse();

有点俗气,但它的工作;)

其他回答

如果你正在处理一个有键的集合(例如,来自数据库的条目),一个快速(即比选择的答案更快)的解决方案将是

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

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

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

下面是如何从一个集合(数组)中获取最后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();

注意:我错过了你的问题标题说使用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;
    }
}