【问题标题】:How to take all but the last element in a sequence using LINQ?如何使用 LINQ 获取序列中除最后一个元素之外的所有元素?
【发布时间】:2009-11-22 16:06:05
【问题描述】:

假设我有一个序列。

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

获取序列并不便宜,而且是动态生成的,我只想迭代一次。

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

我知道我可以这样做:

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

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

是否有一个 LINQ 结构可以让我这样做:

sequence.TakeAllButTheLastElement();

【问题讨论】:

  • 您必须在 O(2n) 时间或 O(count) 空间效率算法之间进行选择,后者还需要在内部数组中移动项目。
  • 达里奥,请你为那些不太喜欢大 o-notation 的人解释一下吗?
  • 另请参阅此重复问题:stackoverflow.com/q/4166493/240733
  • 我最终通过将集合转换为列表然后调用sequenceList.RemoveAt(sequence.Count - 1); 来缓存它。就我而言,这是可以接受的,因为在所有 LINQ 操作之后,我必须将其转换为数组或 IReadOnlyCollection。我想知道您甚至不考虑缓存的用例是什么?正如我所看到的,即使是批准的答案也会进行一些缓存,因此在我看来,简单地转换为 List 会更容易、更短。

标签: c# .net linq


【解决方案1】:

我不知道 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 个项目(使用 cmets 中建议的队列):

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

【讨论】:

  • 现在你能把它概括为除了最后的 n 之外的所有东西吗?
  • 不错。一个小错误;队列大小应初始化为 n + 1,因为这是队列的最大大小。
  • ReSharper 不理解您的代码(条件表达式中的赋值)但我有点喜欢 +1
【解决方案2】:

Enumerable.SkipLast(IEnumerable&lt;TSource&gt;, Int32) 方法已添加到 .NET Standard 2.1 中。它完全符合您的要求。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

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

【讨论】:

  • 这也存在于MoreLinq中
  • +1 表示 SkipLast。我不知道这一点,因为我最近来自 .NET Framework,他们也懒得在里面添加它。
  • @Leperkawn 感谢您指出这一点,因为 SkipLast 方法在 4.7.2 中不存在。
【解决方案3】:

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

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

【讨论】:

  • 请注意,这需要您有足够的内存来存储整个序列,当然它仍然会迭代整个序列两次,一次构建反向序列,一次迭代它。无论您如何切片,这几乎都比 Count 解决方案差;它更慢并且占用更多内存。
  • 我不知道“反向”方法是如何工作的,所以我不确定它使用的内存量。但我同意两次迭代。此方法不应用于大型集合或性能很重要的情况。
  • 那么,将如何实现 Reverse?你能想出一种方法一般在不存储整个序列的情况下做到这一点吗?
  • 我喜欢,确实符合不生成两次序列的标准。
  • 此外,您还需要再次反转整个序列以保持它,因为它是 equence.Reverse().Skip(1).Reverse() 不是一个好的解决方案
【解决方案4】:

因为我不喜欢明确使用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();
    }
}

我现在缓冲 before yielding 而不是 after yielding,这样n == 0 的情况就不需要特殊处理了。

【讨论】:

  • 在第一个示例中,在分配 buffer 之前在 else 子句中设置 buffered=false 可能会稍微快一些。无论如何已经检查了条件,但这将避免每次循环都重复设置buffered
  • 有人能告诉我这个与所选答案的优缺点吗?
  • 在缺少输入检查的单独方法中实现有什么好处?另外,我只是放弃单个实现并为多个实现赋予默认值。
  • @jpmc26 使用单独的方法进行检查,您实际上在调用DropLast 的那一刻就得到了验证。否则,验证仅在您实际枚举序列时发生(在第一次调用 MoveNext 时,结果为 IEnumerator)。当在生成 IEnumerable 和实际枚举它之间可能存在任意数量的代码时,调试并不是一件有趣的事情。现在我会把InternalDropLast写成DropLast的内部函数,但是9年前我写这段代码时,C#中还没有这个功能。
【解决方案5】:

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,然后永远循环”。
  • 代码似乎有“编译时”错误。可能你想纠正它。但是,是的,我们可以编写一个扩展器,它迭代一次并返回除最后一项之外的所有项。
  • @Caleb:你是绝对正确的——我写这篇文章真的很着急!现在修好了。 @Amby:Erm,不确定您使用的是什么编译器。我什么都没有。 :P
  • @RobertSchmidt 哎呀,是的。我现在添加了using 声明。
【解决方案6】:

在 C# 8.0 中,您可以使用 Ranges and indices

var allButLast = sequence[..^1];

默认情况下,C# 8.0 需要 .NET Core 3.0 或 .NET Standard 2.1(或更高版本)。选中 this thread 以用于较旧的实现。

【讨论】:

    【解决方案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();
        }
    }
    

    【讨论】:

      【解决方案8】:

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

      shrink 实现了一个简单的向前计数以删除第一个 left 多个元素,并使用相同的丢弃缓冲区删除最后一个 right 多个元素。

      【讨论】:

        【解决方案9】:

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

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

        【讨论】:

          【解决方案10】:

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

          数组示例

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

          IEnumerable&lt;T&gt; 为例

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

          【讨论】:

          • 问题是关于 IEnumerables 而你的解决方案是 OP 试图避免的。您的代码会影响性能。
          【解决方案11】:

          接受的答案略有不同,(根据我的口味)有点简单:

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

          【讨论】:

            【解决方案12】:

            为什么不只是在序列上.ToList&lt;type&gt;(),然后像你最初那样调用 count 和 take ......但是由于它已被拉入列表,它不应该进行两次昂贵的枚举。对吧?

            【讨论】:

              【解决方案13】:

              我用于这个问题的解决方案稍微复杂一些。

              我的 util 静态类包含一个扩展方法 MarkEnd,它转换 T-items 中的 EndMarkedItem&lt;T&gt;-items。每个元素都标有一个额外的int,即0;或者(如果对最后 3 项特别感兴趣)-3-2-1 表示最后 3 项。

              这可能很有用,例如,当您想在一个简单的foreach-loop 中创建一个列表时,除了最后 2 个元素之外的每个元素后都有逗号,第二个到-最后一项后跟一个连词(例如“and”或“or”),最后一个元素后跟一个点。

              为了生成没有最后 n 个项目的整个列表,扩展方法 ButLast 简单地迭代 EndMarkedItem&lt;T&gt;s 而 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);
                                  }
                              }
                          }    
                      }
                  }
              }
              

              【讨论】:

                【解决方案14】:
                    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;
                                }
                            }
                        }
                    }
                

                【讨论】:

                  【解决方案15】:

                  我认为没有比这更简洁的了 - 还要确保 Dispose IEnumerator&lt;T&gt;:

                  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 相同。

                  【讨论】:

                    【解决方案16】:

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

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

                    【讨论】:

                      【解决方案17】:

                      你可以写:

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

                      【讨论】:

                        【解决方案18】:

                        我传统的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 是 IDisposable。
                        • 正确/已注明。谢谢。
                        【解决方案19】:

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

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

                        【讨论】:

                        • Take 用于获取已知数量的项目。足够大的可枚举队列是可怕的。
                        【解决方案20】:

                        可能是:

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

                        我想它应该像 de "Where" 但保留顺序(?)。

                        【讨论】:

                        • 这是一种非常低效的方法。要评估 sequence.Last() 它必须遍历整个序列,对序列中的每个元素都这样做。 O(n^2) 效率。
                        • 你是对的。我不知道我在写这篇文章时在想什么XD。无论如何,您确定 Last() 会遍历整个序列吗?对于 IEnumerable 的某些实现(例如 Array),这应该是 O(1)。我没有检查 List 实现,但它也可以有一个“反向”迭代器,从最后一个元素开始,这也需要 O(1)。此外,至少从技术上讲,您应该考虑到 O(n) = O(2n)。因此,如果此过程对您的应用程序性能不是绝对批评,我会坚持使用更清晰的 sequence.Take(sequence.Count() - 1).Regards!
                        • @Mike 我不同意你的观点,sequence.Last() 是 O(1),所以它不需要遍历整个序列。 stackoverflow.com/a/1377895/812598
                        • @GoRoS,如果序列实现了 ICollection/IList 或者是一个数组,这只是 O(1)。所有其他序列都是 O(N)。在我的问题中,我不认为 O(1) 来源之一
                        • 序列可能有几个满足这个条件的item e == sequence.Last(),例如new [] { 1, 1, 1, 1 }
                        【解决方案21】:

                        如果速度是一个要求,这种老式的方式应该是最快的,即使代码看起来不像 linq 可以做到的那样流畅。

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

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

                        【讨论】:

                        • 您正在处理一个不允许通过索引访问元素的 IEnumerable。您的解决方案不起作用。假设你做对了,它需要遍历序列来确定长度,分配一个长度为 n-1 的数组,复制所有元素。 - 1. 2n-1 次操作和 (2n-1) * (4 或 8 字节) 内存。这甚至不算快。
                        【解决方案22】:

                        我可能会这样做:

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

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

                        【讨论】:

                        • 这不是一件好事的两个原因。 1) .LastOrDefault() 需要迭代整个序列,并且为序列中的每个元素调用(在 .Where() 中)。 2)如果序列是 [1,2,1,2,1,2] 并且您使用了您的技术,那么您将得到 [1,1,1]。
                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 2023-01-05
                        • 2018-06-14
                        • 2011-02-04
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多