如何使用LINQ获取序列中除最后一个元素以外的所有元素?


131

假设我有一个序列。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

获取序列并不便宜,它是动态生成的,我只希望对其进行一次迭代。

我想得到0-999999(即除最后一个元素外的所有内容)

我知道我可以做类似的事情:

sequence.Take(sequence.Count() - 1);

但这导致对大序列进行两次枚举。

是否有让我做的LINQ构造:

sequence.TakeAllButTheLastElement();

3
您必须在O(2n)时间或O(count)空间效率算法之间进行选择,后者还需要在内部数组中移动项目。
Dykam,

1
达里奥,您能为一个不那么喜欢O的人解释一下吗?
亚历森

另请参阅此重复的问题:stackoverflow.com/q/4166493/240733
stakx-不再贡献

我最终通过将collection转换为List然后调用来对其进行缓存sequenceList.RemoveAt(sequence.Count - 1);。就我而言,这是可以接受的,因为在所有LINQ操作之后,我都必须将其转换为数组或以IReadOnlyCollection任何方式转换。我想知道您什至不考虑缓存的用例是什么?如我所见,即使是已批准的答案也可以进行一些缓存,所以List我认为转换为简单易行的操作要容易得多且短得多。
Pavels Ahmadulins,2016年

Answers:


64

我不知道Linq解决方案-但是您可以自己使用生成器(收益率回报)轻松地编写算法代码。

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.TakeAllButLast().Select(x => x.ToString()).ToArray()));
}

或作为丢弃最后n个项目的通用解决方案(使用注释中建议的队列):

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}

7
现在您可以将其归纳为除最后一个n以外的所有值吗?
Eric Lippert

4
真好 一个小错误;队列大小应初始化为n + 1,因为这是队列的最大大小。
埃里克·利珀特

ReSharper的不理解你的代码(分配条件表达式),但我还挺喜欢它+1
Грозный

44

作为创建自己的方法的替代方法,并且在元素顺序不重要的情况下,下一个方法可以工作:

var result = sequence.Reverse().Skip(1);

49
请注意,这需要您有足够的内存来存储整个序列,并且当然它仍然会重复整个序列两次,一次构建反向序列,一次迭代。无论您如何分割,这几乎都比Count解决方案差。它速度较慢,占用的内存更多。
Eric Lippert

我不知道“反向”方法的工作原理,所以我不确定它使用的内存量。但是我同意两次迭代。如果大型集合或性能很重要,则不应使用此方法。
Kamarey

5
那么,将如何实施反向?您能找到一种通常的方法而不存储整个序列吗?
Eric Lippert

2
我喜欢它,并且确实满足不两次生成序列的标准。
艾米B

12
此外,您还需要再次颠倒整个序列以保持其顺序,因为这equence.Reverse().Skip(1).Reverse()不是一个好的解决方案
shashwat 2013年

42

因为我不喜欢明确使用Enumerator,所以这是一个替代方案。请注意,需要包装方法来使无效参数尽早抛出,而不是将检查推迟到实际枚举序列之前。

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");

    return InternalDropLast(source);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source)
{
    T buffer = default(T);
    bool buffered = false;

    foreach (T x in source)
    {
        if (buffered)
            yield return buffer;

        buffer = x;
        buffered = true;
    }
}

按照埃里克·利珀特(Eric Lippert)的建议,它很容易推广为n个项目:

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (n < 0)
        throw new ArgumentOutOfRangeException("n", 
            "Argument n should be non-negative.");

    return InternalDropLast(source, n);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
    Queue<T> buffer = new Queue<T>(n + 1);

    foreach (T x in source)
    {
        buffer.Enqueue(x);

        if (buffer.Count == n + 1)
            yield return buffer.Dequeue();
    }
}

我现在屈服之前而不是屈服之后进行缓冲的位置,因此该n == 0案例不需要特殊处理。


在第一个示例中,buffered=false在赋值之前在else子句中进行设置可能会稍微快一些buffer。无论如何都已经检查了该条件,但这可以避免buffered每次循环都重复设置。
詹姆斯,

有人可以告诉我这个相对于所选答案的利弊吗?
Sinjai

在没有输入检查的单独方法中实现的好处是什么?另外,我只是删除单个实现,并为多个实现提供默认值。
jpmc26,16

@ jpmc26使用单独的方法签入,您实际上在调用时就获得了验证DropLast。否则,验证情况只有在你真正枚举序列(在第一次调用MoveNext上由此而来IEnumerator)。在生成IEnumerable和实际枚举之间可能有任意数量的代码时,调试不是一件有趣的事情。如今,我将InternalDropLast作为的内部函数进行编写DropLast,但是9年前编写此代码时,该功能在C#中并不存在。
Joren

28

Enumerable.SkipLast(IEnumerable<TSource>, Int32)方法是在.NET Standard 2.1中添加的。它正是您想要的。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

来自https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.skiplast

返回一个新的可枚举的集合,其中包含来自source的元素,而省略了source集合的最后count个元素。


2
MoreLinq
Leperkawn '19

为SkipLast +1。自从我最近来自.NET Framework以来,我就不知道这一点,他们也没有费心在其中添加它。
PRMan

12

BCL(或我相信的MoreLinq)中没有任何内容,但是您可以创建自己的扩展方法。

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        bool first = true;
        T prev;
        while(enumerator.MoveNext())
        {
            if (!first)
                yield return prev;
            first = false;
            prev = enumerator.Current;
        }
    }
}

这段代码将无法正常工作...应该可以,if (!first)并且first = false退出if。
加勒布

此代码看起来不对。逻辑似乎是“ prev在第一次迭代中返回未初始化的变量,然后在那之后永远循环”。
Phil Miller

该代码似乎有“编译时”错误。可能是您想更正它。但是可以,我们可以编写一个扩展器,该扩展器将迭代一次并返回除最后一项之外的所有项。
Manish Basantani 09年

@Caleb:你是绝对正确的-我写的真的很仓促!立即修复。@Amby:Erm,不确定您使用的是哪个编译器。我什么都没有。:P
Noldorin

@RobertSchmidt糟糕,是的。我using现在添加了一条声明。
Noldorin

7

如果.NET Framework附带了这样的扩展方法,将很有帮助。

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    var enumerator = source.GetEnumerator();
    var queue = new Queue<T>(count + 1);

    while (true)
    {
        if (!enumerator.MoveNext())
            break;
        queue.Enqueue(enumerator.Current);
        if (queue.Count > count)
            yield return queue.Dequeue();
    }
}

3

对Joren的优雅解决方案进行了一些扩展:

public static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
    int i = 0;
    var buffer = new Queue<T>(right + 1);

    foreach (T x in source)
    {
        if (i >= left) // Read past left many elements at the start
        {
            buffer.Enqueue(x);
            if (buffer.Count > right) // Build a buffer to drop right many elements at the end
                yield return buffer.Dequeue();    
        } 
        else i++;
    }
}
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(n, 0);
}

where收缩实现简单的向前计数以删除前left许多元素,而相同的丢弃缓冲区则删除后right许多元素。


3

如果您没有时间推出自己的扩展程序,这是一种更快的方法:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });

2

对接受的答案进行一些细微的改动(根据我的喜好)比较简单:

    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        // for efficiency, handle degenerate n == 0 case separately 
        if (n == 0)
        {
            foreach (var item in enumerable)
                yield return item;
            yield break;
        }

        var queue = new Queue<T>(n);
        foreach (var item in enumerable)
        {
            if (queue.Count == n)
                yield return queue.Dequeue();

            queue.Enqueue(item);
        }
    }

2

如果您可以获得CountLength的枚举,在大多数情况下都可以,那么就Take(n - 1)

数组示例

int[] arr = new int[] { 1, 2, 3, 4, 5 };
int[] sub = arr.Take(arr.Length - 1).ToArray();

例子 IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);

问题与IEnumerables有关,您的解决方案是OP试图避免的问题。您的代码会对性能产生影响。
nawfal

1

为什么不只.ToList<type>()按顺序排列,然后按原来的顺序调用计数并采取..但由于它已被放入列表中,因此它不应进行两次昂贵的枚举。对?


1

我针对此问题使用的解决方案略为详尽。

我的util静态类包含一个扩展方法MarkEnd,该方法将T-items 转换为EndMarkedItem<T>-items。每个元素都标有一个额外的int0 ; 或(如果对最后3个项目特别感兴趣)-3-2-1表示最后3个项目。

此功能可能非常有用,例如,当您要在简单的foreach-loop中创建一个列表,并在每个元素(除了最后2个除外)之后使用逗号分隔,倒数第二个项目后跟一个连词(例如“ ”或“ ”),最后一个元素后跟一个点。

为了生成不带最后n个项的整个列表,扩展方法ButLast仅对EndMarkedItem<T>s while进行迭代EndMark == 0

如果您未指定tailLength,则仅最后一项被标记(在中MarkEnd())或被删除(在中ButLast())。

像其他解决方案一样,这通过缓冲起作用。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Adhemar.Util.Linq {

    public struct EndMarkedItem<T> {
        public T Item { get; private set; }
        public int EndMark { get; private set; }

        public EndMarkedItem(T item, int endMark) : this() {
            Item = item;
            EndMark = endMark;
        }
    }

    public static class TailEnumerables {

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts) {
            return ts.ButLast(1);
        }

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts, int tailLength) {
            return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts) {
            return ts.MarkEnd(1);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts, int tailLength) {
            if (tailLength < 0) {
                throw new ArgumentOutOfRangeException("tailLength");
            }
            else if (tailLength == 0) {
                foreach (var t in ts) {
                    yield return new EndMarkedItem<T>(t, 0);
                }
            }
            else {
                var buffer = new T[tailLength];
                var index = -buffer.Length;
                foreach (var t in ts) {
                    if (index < 0) {
                        buffer[buffer.Length + index] = t;
                        index++;
                    }
                    else {
                        yield return new EndMarkedItem<T>(buffer[index], 0);
                        buffer[index] = t;
                        index++;
                        if (index == buffer.Length) {
                            index = 0;
                        }
                    }
                }
                if (index >= 0) {
                    for (var i = index; i < buffer.Length; i++) {
                        yield return new EndMarkedItem<T>(buffer[i], i - buffer.Length - index);
                    }
                    for (var j = 0; j < index; j++) {
                        yield return new EndMarkedItem<T>(buffer[j], j - index);
                    }
                }
                else {
                    for (var k = 0; k < buffer.Length + index; k++) {
                        yield return new EndMarkedItem<T>(buffer[k], k - buffer.Length - index);
                    }
                }
            }    
        }
    }
}

1
    public static IEnumerable<T> NoLast<T> (this IEnumerable<T> items) {
        if (items != null) {
            var e = items.GetEnumerator();
            if (e.MoveNext ()) {
                T head = e.Current;
                while (e.MoveNext ()) {
                    yield return head; ;
                    head = e.Current;
                }
            }
        }
    }

1

我认为它不会比这更简洁-还要确保处置IEnumerator<T>

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
        {
            var item = it.Current;
            while (it.MoveNext())
            {
                yield return item;
                item = it.Current;
            }
        }
    }
}

编辑:技术上与此答案相同。



0

您可以这样写:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);

0

这是一种通用且恕我直言的优雅解决方案,可以正确处理所有情况:

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        IEnumerable<int> r = Enumerable.Range(1, 20);
        foreach (int i in r.AllButLast(3))
            Console.WriteLine(i);

        Console.ReadKey();
    }
}

public static class LinqExt
{
    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        using (IEnumerator<T> enumerator = enumerable.GetEnumerator())
        {
            Queue<T> queue = new Queue<T>(n);

            for (int i = 0; i < n && enumerator.MoveNext(); i++)
                queue.Enqueue(enumerator.Current);

            while (enumerator.MoveNext())
            {
                queue.Enqueue(enumerator.Current);
                yield return queue.Dequeue();
            }
        }
    }
}

-1

我的传统IEnumerable方法:

/// <summary>
/// Skips first element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping first element</returns>
private IEnumerable<U> SkipFirstEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        for (;e.MoveNext();) yield return e.Current;
        yield return e.Current;
    }
}

/// <summary>
/// Skips last element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping last element</returns>
private IEnumerable<U> SkipLastEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        yield return e.Current;
        for (;e.MoveNext();) yield return e.Current;
    }
}

您的SkipLastEnumerable可能是传统的,但已损坏。它返回的第一个元素始终是未定义的U-即使“模型”具有1个元素。在后一种情况下,我期望结果为空。
罗伯·施密特

同样,IEnumerator <T>是IDisposable的。
罗伯特·施密特

正确/已注意到。谢谢。
Chibueze Opata

-1

一种简单的方法是只转换为队列并出队,直到只剩下要跳过的项目数。

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int n)
{
    var queue = new Queue<T>(source);

    while (queue.Count() > n)
    {
        yield return queue.Dequeue();
    }
}

还有就是就拿用于拍摄物品的已知数量。并且排队足够大的枚举是可怕的。
Sinatr

-2

可能:

var allBuLast = sequence.TakeWhile(e => e != sequence.Last());

我猜应该像de“ Where”一样,但是要保留order(?)。


3
这是一种非常低效的方法。为了评估sequence.Last(),它必须遍历整个序列,对序列中的每个元素进行遍历。O(n ^ 2)效率。
Mike

你是对的。编写此XD时,我不知道在想什么。无论如何,您确定Last()将遍历整个序列吗?对于IEnumerable的某些实现(例如Array),应为O(1)。我没有检查List实现,但是它也可以有一个“反向”迭代器,从最后一个元素开始,它也需要O(1)。另外,至少从技术上讲,您应考虑O(n)= O(2n)。因此,如果此过程并非绝对批评您的应用程序性能,我会坚持使用更清晰的sequence.Take(sequence.Count()-1)。
吉列尔莫·阿瑞斯

@Mike我不同意你的看法,sequence.Last()是O(1),因此它不需要遍历整个序列。stackoverflow.com/a/1377895/812598
GoRoS 2014年

1
@GoRoS,如果序列实现ICollection / IList或是数组,则只有O(1)。所有其他序列均为O(N)。在我的问题中,我不认为O(1)来源之一
Mike

3
该序列可能有几个满足此条件的项目e == sequence.Last(),例如new [] {
1,1,1,1

-2

如果需要速度,那么即使代码看起来不像linq所能做到的那样平滑,这种老式的方法也应该是最快的方法。

int[] newSequence = int[sequence.Length - 1];
for (int x = 0; x < sequence.Length - 1; x++)
{
    newSequence[x] = sequence[x];
}

这就要求该序列是一个数组,因为它具有固定的长度和索引项。


2
您正在处理的IEnumerable不允许通过索引访问元素。您的解决方案不起作用。假设操作正确,则需要遍历序列以确定长度,分配长度为n-1的数组,然后复制所有元素。-1. 2n-1操作和(2n-1)*(4或8字节)内存。那还不算快。
塔里克

-4

我可能会做这样的事情:

sequence.Where(x => x != sequence.LastOrDefault())

这是一次迭代,并检查它不是每次都为最后一次。


3
这不是一件好事的两个原因。1).LastOrDefault()需要迭代整个序列,并且需要对序列中的每个元素(在.Where()中)进行调用。2)如果序列为[1,2,1,2,1,2],并且您使用的是技术,那么您将剩下[1,1,1]。
Mike
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.