【问题标题】:IEnumerable: Get all before the last that matches a predicateIEnumerable:获取最后一个匹配谓词之前的所有内容
【发布时间】:2016-11-25 10:07:53
【问题描述】:

我有一个像这样的IEnumerable<int>,只是更长:

5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0

现在我想返回最后一个非零值之前的所有元素:

5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0

似乎sequence.Last() 在这里没有帮助,因为它返回最后一次出现,而不是最后一次出现的索引。

我曾想过使用

var lastIndex = sequence.LastIndex(x=>x!=0);
subsequence = sequence.Take(lastIndex);

这在一般情况下可以工作,但LastIndex 不存在,或者

var last = sequence.Last(y=>y!=0);
subsequence = sequence.TakeWhile(x=>x!=last)

这适用于示例,但不适用于可能存在重复的非零值的一般情况。

有什么想法吗?

【问题讨论】:

    标签: c# linq


    【解决方案1】:

    你可以试试这个

    var allDataBeforeLastNonZero= sequence.GetRange(0,sequence.FindLastIndex(x=>x!=0));
    

    【讨论】:

    • 这是迄今为止最不难看的解决方案,但它假定sequenceList<T>。无论如何+1。
    • 这是一个IEnumerable<int>,所以必须先将其转换为列表。
    • 因为无论如何你都会必须遍历整个 IEnumerable - 首先将其转换为列表没有害处。
    • @Evk 问题所需的内存量足以包含非零元素之间的最大序列,而这使用了保存整个序列所需的内存量,也不能在读取最后一个元素之前给出任何结果。如果序列足够大,那肯定是有害的。
    【解决方案2】:

    在一般情况下可以工作,但 LastIndex 不存在

    不,但您可以通过以下方式找到它:

    var lastIndex = sequence
      .Select((x, i) => new {El = x, Idx = i})
      .Where(x => x.El != 0)
      .Select(x => x.Idx).Last();
    

    如果您需要与IQueryable<T> 合作,那将是您所能得到的。

    它有一些问题。一方面,它扫描序列两次,谁说序列甚至允许这样做。我们可以做得更好,但我们必须缓冲,虽然不一定缓冲整个事情:

    public static IEnumerable<T> BeforeLastMatch<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (predicate == null) throw new ArgumentNullException(nameof(predicate));
      return BeforeLastMatchImpl(source, predicate);
    }
    
    public static IEnumerable<T> BeforeLastMatchImpl<T>(IEnumerable<T> source, Func<T, bool> predicate)
    {
      var buffer = new List<T>();
      foreach(T item in source)
      {
        if (predicate(item) && buffer.Count != 0)
        {
          foreach(T allowed in buffer)
          {
              yield return allowed;
          }
          buffer.Clear();
        }
        buffer.Add(item);
      }
    }
    

    致电sequence.BeforeLastMatch(x =&gt; x != 0),您会收到5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0

    如果你真的需要它同时使用 IEnumerableIQueryable 也可以处理,但它有点复杂。如果您知道您只会拥有内存中的IEnumerable,请不要打扰。 (另外一些供应商对不同的特性有不同的支持,所以你可能不得不做上面的内存版本):

    private class ElementAndIndex<T>
    {
      public T Element { get; set; }
      public int Index { get; set; }
    }
    
    public static IQueryable<T> BeforeLastMatch<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (predicate == null) throw new ArgumentNullException(nameof(predicate));
      // If source is actually an in-memory enumerable, the other method will be more efficient,
      // so use it instead.
      var asEnum = source as EnumerableQuery<T>;
      if (asEnum != null && asEnum.Expression.NodeType == ExpressionType.Constant)
      {
        // On any other IQueryable calling `AsEnumerable()` will force it
        // to be loaded into memory, but on an EnumerableQuery it just
        // unwraps the wrapped enumerable this will chain back to the
        // contained GetEnumerator.
        return BeforeLastMatchImpl(source.AsEnumerable(), predicate.Compile()).AsQueryable();
      }
    
      // We have a lambda from (T x) => bool, and we need one from
      // (ElementAndIndex<T> x) => bool, so build it here.
    
      var param = Expression.Parameter(typeof(ElementAndIndex<T>));
      var indexingPredicate = Expression.Lambda<Func<ElementAndIndex<T>, bool>>(
        Expression.Invoke(predicate, Expression.Property(param, "Element")),
        param
      );
    
      return source.Take( // We're going to Take based on the last index this finds.
        source
          // Elements and indices together
          .Select((x, i) => new ElementAndIndex<T>{ Element = x, Index = i}) 
          // The new predicate we created from that passed to us.
          .Where(indexingPredicate)
          // The last matching element.
          .Select(x => x.Index).Last());
    }
    

    【讨论】:

    • 是的,这是我认为最好的方式。
    • 使用队列代替缓冲区会更容易。
    • @JeffMercado 不是真的。如果我们使用队列,我们​​可以执行一堆Pop()s,但它会比foreachClear() 更高效,如果我们使用队列这样做,那么字面上唯一的区别就是@987654333 这个词@ 被替换为 Push
    • @TimSchmelter OP 使用的谓词是x =&gt; x != 0,他们想要5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0 的结果,这是该谓词最后一次匹配之前的序列,这就是上面产生的结果。跨度>
    【解决方案3】:

    也许有更有效的方法,但这个是可读的,不是吗?

    var allBeforeLastNonZero = sequence
        .Reverse()                // look from the end
        .SkipWhile(i => i == 0)   // skip the zeros
        .Skip(1)                  // skip last non-zero
        .Reverse();               // get original order
    

    【讨论】:

    • 如果给定的IEnumerable&lt;T&gt; 不是ICollection&lt;T&gt; 并且序列很长,则执行.Reverse() 调用将花费大量时间和内存。
    • Reverse() 不会反转集合。它创建一个反向迭代器,从集合的末尾开始。
    • @Dmitry: 是的,但 Oliver 是对的,如果输入序列不是一个集合,Buffer used 将创建一个新序列以便能够loop it backwards。因此,如果输入序列是一个集合,则第一个 Reverse 不需要创建临时数组,但第二个 Reverse 肯定需要它。
    • 感谢您的回答,也感谢您的参考。看起来最好的解决方案是一个很好的 for(;;) 循环
    【解决方案4】:

    您可以将列表转换为字符串并使用String.Trim:

    var str = String.Join(",", myInputArray);
    var result = str.TrimEnd(',', '0').Split(',').Select(x => Convert.ToInt32(x)).ToList();
    result.RemoveAt(result.Count - 1);
    

    不得不承认看起来有点难看,但应该可以。

    【讨论】:

    • 现在它更丑了,但你得到了我对那个我没想到的创意的支持
    • @fubo 您对删除最后一项有何建议?只是出于好奇。我没有找到任何不丑的解决方案。
    • 我认为没有更好的方法,因为需要对最后一个字符进行特殊处理:( Tim 的方法还需要 .Skip(1)
    【解决方案5】:
    IEnumerable<int> source = new List<int> {5,0,0, 4,0,0,3, 0, 0};
    List<int> result = new List<int>();
    List<int> buffer = new List<int>();
    foreach (var i in source)
    {
        buffer.Add(i);
        if (i != 0)
        {
            result.AddRange(buffer);
            buffer.Clear();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2020-01-25
      • 2011-03-12
      • 1970-01-01
      • 2020-03-09
      • 1970-01-01
      • 1970-01-01
      • 2020-10-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多