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

172

假设我有一个序列。

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
Dario,请你为那些不太了解大O符号的人解释一下,好吗? - alexn
请参考此重复问题:https://dev59.com/U2855IYBdhLWcg3wvnKE - stakx - no longer contributing
1
最终我通过将集合转换为List并调用sequenceList.RemoveAt(sequence.Count - 1);来进行缓存。在我的情况下,这是可以接受的,因为在所有LINQ操作之后,我必须将其转换为数组或IReadOnlyCollection。我想知道你的使用情况,在那里你甚至不考虑缓存?正如我所看到的,即使是批准的答案也会进行一些缓存,因此在我看来,简单地转换为List更容易和更短。 - Pavels Ahmadulins
22个回答

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

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;
            }
        }
    }
}

编辑:技术上与this answer相同。

1
我用于解决这个问题的解决方案稍微复杂一些。我的util静态类包含一个扩展方法MarkEnd,将T项转换为EndMarkedItem<T>项。每个元素都带有额外的int标记,可以是0;或者(如果特别关注最后3个项目)为最后3个项目之一的-3,-2或-1。
这本身可能很有用,例如当您想在简单的foreach循环中创建一个列表,并在每个元素之后加上逗号,除了最后2个元素外,第二个到最后一个元素之间加上连词(如“and”或“or”),并在最后一个元素之后加上点时。
要生成整个列表而不包括最后的n个项目,扩展方法ButLast只需在EndMarkedItem<T>中迭代,同时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
为什么不直接在序列上调用.ToList<type>(),然后像最初一样调用count和take呢?但是既然它已经被转换成列表,就不应该再进行昂贵的枚举了。对吧?

1
这是一种通用且我认为优雅的解决方案,可以正确处理所有情况:
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 - 即使“models”只有1个元素。在后一种情况下,我期望得到一个空结果。 - Robert Schmidt
另外,IEnumerator<T>也是IDisposable。 - Robert Schmidt
真的/已注意。谢谢。 - Chibueze Opata

-1
你可以写成这样:
var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);

-3
如果速度是一个要求,这种老派的方式应该是最快的,即使代码看起来不像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字节)的内存。这甚至都不快。 - Tarik

-3

可能是:

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

我猜应该像“Where”一样,但要保留顺序(?)。


4
这是一种非常低效的方法。为了评估sequence.Last(),必须遍历整个序列,并对序列中的每个元素都执行此操作。这将导致O(n^2)的效率。 - Mike
你说得对。我写这个的时候不知道在想什么XD。不过,你确定Last()会遍历整个序列吗?对于某些IEnumerable的实现(例如Array),这应该是O(1)的。我没有检查List的实现,但它也可能有一个“反向”迭代器,从最后一个元素开始,这也需要O(1)。此外,你应该考虑到O(n)=O(2n),至少从技术上讲是这样的。因此,如果这个过程对你的应用程序性能不是绝对关键的话,我建议使用更清晰的sequence.Take(sequence.Count()-1)。祝好! - Guillermo Ares
@Mike,我不同意你的观点,sequence.Last()是O(1)的,因此它不需要遍历整个序列。https://dev59.com/9HM_5IYBdhLWcg3wcCrc#1377895 - GoRoS
1
@GoRoS,只有实现ICollection/IList或者是数组的序列才是O(1)。所有其他序列都是O(N)。在我的问题中,我并不假设其中一个是O(1)的来源。 - Mike
@Mike 嗯,没错,这取决于你在序列上实现了什么。但是似乎不像你所说的 ICollection/IList 是 O(1),只有 IList 才是 O(1)。https://dev59.com/b3XYa4cB1Zd3GeqP8Jt-#18200099 - GoRoS
4
该序列可能有多个符合条件 e == sequence.Last() 的项,例如 new [] { 1, 1, 1, 1 }。 - Sergey Shandar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接