【问题标题】:Using LINQ to create an IEnumerable<> of delta values使用 LINQ 创建增量值的 IEnumerable<>
【发布时间】:2011-04-21 21:37:54
【问题描述】:

我有一个时间戳列表(以滴答为单位),我想从这个列表中创建另一个表示条目之间的增量时间的列表。

例如,假设我的主时间表如下所示:

  1. 10
  2. 20
  3. 30
  4. 50
  5. 60
  6. 70

我想要回的是这个:

  1. 10
  2. 10
  3. 20
  4. 10
  5. 10

我在这里尝试完成的是通过计算标准偏差来检测输出表中的#3 是异常值。我以前没有统计过,但我认为如果我在输出列表中查找普遍值并丢弃 1 sigma 之外的任何内容,这对我来说就足够了。

我希望能够使用单个 LINQ 查询创建输出列表,但我还没有弄清楚。目前我只是用循环蛮力强制它。

【问题讨论】:

    标签: c# linq .net-3.5 statistics


    【解决方案1】:

    如果您运行的是 .NET 4.0,这应该可以正常工作:

    var deltas = list.Zip(list.Skip(1), (current, next) => next - current);
    

    除了多个枚举器之外,这是非常有效的;它应该适用于任何类型的序列。

    这是 .NET 3.5 的替代方案:

    var deltas = list.Skip(1)
                     .Select((next, index) => next - list[index]);
    

    显然,这个想法只有在使用列表的索引器时才会有效。修改它以使用ElementAt 可能不是一个好主意:非IList&lt;T&gt; 序列将出现二次运行时。在这种情况下,编写自定义迭代器是一个很好的解决方案。

    编辑:如果您不喜欢Zip + Skip(1) 的想法,那么编写这样的扩展(未经测试)可能在这些情况下有用:

    public class CurrentNext<T>
    {
        public T Current { get; private set; }
        public T Next { get; private set; }
    
        public CurrentNext(T current, T next)
        {
            Current = current;
            Next = next;
        }
    }
    
    ...
    
    public static IEnumerable<CurrentNext<T>> ToCurrentNextEnumerable<T>(this IEnumerable<T> source)
    {
        if (source == null)
            throw new ArgumentException("source");
    
        using (var source = enumerable.GetEnumerator())
        {
            if (!enumerator.MoveNext())
                yield break;
    
            T current = enumerator.Current;
    
            while (enumerator.MoveNext())
            {
                yield return new CurrentNext<T>(current, enumerator.Current);
                current = enumerator.Current;
            }
        }
    }
    

    然后你可以用作:

    var deltas = list.ToCurrentNextEnumerable()
                     .Select(c=> c.Next - c.Current);
    

    【讨论】:

    • +1。杰出的!!当然这是 .NET 4.0。不要忘记你会错过列表中的最后一个元素。您需要在 list.Skip(1) 列表中添加 0。
    • @Steven:你能解释一下吗?我不明白怎么做。 Count(deltas) 应该是 Count(sourceEnumerable) - 1,不是吗?
    • 老实说,在 3.5 中,我会编写自己的扩展方法,而不是在 LINQ 中使用索引访问器。不过,如果你真的想要,你可以获取 Zip 扩展的源代码并在 3.5 中使用它 - blogs.msdn.com/b/ericwhite/archive/2009/07/05/…
    • @Steven - OP 在源列表中有 6 个元素,在输出中有 5 个元素。通常,连续实体之间的差异将比原始实体少一个。五层楼只有四层楼梯。
    • @Ani, @Jeffrey:你说得对,序列必须短一个。
    【解决方案2】:

    你可以用阿尼的回答:-

    var deltas = list.Zip(list.Skip(1), (current, next) => next - current);
    

    Zip 扩展方法的超简单实现:-

    public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
      this IEnumerable<TFirst> first,
      IEnumerable<TSecond> second,
      Func<TFirst, TSecond, TResult> func)
    {
      var ie1 = first.GetEnumerator();
      var ie2 = second.GetEnumerator();
    
      while (ie1.MoveNext() && ie2.MoveNext())
        yield return func(ie1.Current, ie2.Current);
    }
    

    这适用于 3.5。

    【讨论】:

    • 只需要添加对枚举数的处理即可。
    【解决方案3】:

    这应该可以解决问题:

    static IEnumerable<int> GetDeltas(IEnumerable<int> collection)
    {
        int? previous = null;
    
        foreach (int value in collection)
        { 
            if (previous != null)
            {
                yield return value - (int)previous;
            }
            previous = value;
        }
    }
    

    现在你可以这样调用你的集合了:

    var masterTimetable = GetMasterTimeTable();
    
    var deltas = GetDeltas(masterTimetable);
    

    这不是真正的 LINQ,但可以有效地解决问题。

    【讨论】:

    • 请注意,这也会给出第一项和 0 之间的差异,这并不是 OP 所要求的。
    • 谢谢!但我是否还需要忽略 / Skip() 您的代码生成的输出 IEnumerable 中的第一个条目?
    • @Dave:我现在看到我不明白你想如何计算增量。仍然使用迭代器方法将非常有效。特别是当直接从数据库或其他来源(如文件系统)获取结果作为流时。带有list.Zip(list.Skip(1)) 的解决方案将调用数据库两次。
    • 我成功地使用了 Ani 的方法。我也喜欢你的,但我认为的问题是你检查第一个元素和前一个元素之间的增量(0),但我希望第一个增量值是 value[ 1] - 值[0]。
    • @Dave:现在我明白了。我修复了这个错误:-)
    【解决方案4】:

    看起来已经有足够的答案让你开始了,但我在春天问了一个类似的问题:

    How to zip one ienumerable with itself

    在回答我的问题时,我了解到“Pairwise”和“Pairwise

    我记得,显式实现您自己的“Pairwise”枚举器确实意味着您只遍历列表一次,而根据 .Zip + .Skip(1) 实现“Pairwise”意味着您最终将遍历您的列表两次。

    在我的帖子中,我还包含了几个几何(对点列表进行操作)处理代码的示例,例如长度/距离、面积、质心。

    【讨论】:

      【解决方案5】:

      不是我推荐这样做,而是完全滥用 LINQ,以下是可行的:

      var vals = new[] {10, 20, 30, 50, 60, 70};
      
      int previous = 0;
      var newvals = vals.Select(i =>
                                  {
                                      int dif = i - previous;
                                      previous = i;
                                      return dif;
                                  });
      foreach (var newval in newvals)
      {
          Console.WriteLine(newval);
      }
      

      【讨论】:

        【解决方案6】:

        一个班轮给你:

        int[] i = new int[] { 10, 20, 30, 50, 60, 70 };
        IEnumerable<int> x = Enumerable.Range(1, i.Count()-1).Select(W => i[W] - i[W - 1]);
        

        【讨论】:

          【解决方案7】:

          LINQ 并不是真正为您在此处尝试执行的操作而设计的,因为它通常逐个评估值,很像 for 循环的极其有效的组合。 如果没有某种解决方法,您必须知道您当前的索引,而您不知道。

          【讨论】:

          • 你当然知道你当前的索引,如果你使用一个没有索引访问器的集合(例如 IEnumerable),那就太糟糕了。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-12-01
          • 2010-11-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多