【问题标题】:Pair-wise iteration in C# or sliding window enumeratorC# 或滑动窗口枚举器中的成对迭代
【发布时间】:2010-10-09 07:41:57
【问题描述】:

如果我有一个像这样的 IEnumerable:

string[] items = new string[] { "a", "b", "c", "d" };

我想循环遍历所有成对的连续项目(大小为 2 的滑动窗口)。会是

("a","b"), ("b", "c"), ("c", "d")

我的解决方案是这样的

    public static IEnumerable<Pair<T, T>> Pairs(IEnumerable<T> enumerable) {
        IEnumerator<T> e = enumerable.GetEnumerator(); e.MoveNext();
        T current = e.Current;
        while ( e.MoveNext() ) {
            T next = e.Current;
            yield return new Pair<T, T>(current, next);
            current = next;
        }
    }

 // used like this :
 foreach (Pair<String,String> pair in IterTools<String>.Pairs(items)) {
    System.Out.PrintLine("{0}, {1}", pair.First, pair.Second)
 }

当我编写这段代码时,我想知道 .NET 框架中是否已经有函数可以做同样的事情,而且不仅适用于对,而且适用于任何大小的元组。 恕我直言,应该有一种很好的方法来执行这种滑动窗口操作。

我使用 C# 2.0,我可以想象使用 C# 3.0(w/LINQ)有更多(更好)的方法可以做到这一点,但我主要对 C# 2.0 解决方案感兴趣。不过,我也会欣赏 C# 3.0 解决方案。

【问题讨论】:

标签: c# .net iterator ienumerable


【解决方案1】:

我在 @dahlbyk 的回答中创建了 2020 年末更新代码的略微修改版本。它更适合启用可空引用类型的项目 (&lt;Nullable&gt;enable&lt;/Nullable&gt;)。我还添加了基本文档。

/// <summary>
/// Enumerates over tuples of pairs of the elements from the original sequence. I.e. { 1, 2, 3 } becomes { (1, 2), (2, 3) }. Note that { 1 } becomes { }.
/// </summary>
public static IEnumerable<(T, T)> Pairwise<T>(this IEnumerable<T> source)
{
    using var it = source.GetEnumerator();
        
    if (!it.MoveNext())
        yield break;

    var previous = it.Current;

    while (it.MoveNext())
        yield return (previous, previous = it.Current);
}

【讨论】:

    【解决方案2】:

    与其要求一个元组(对)类型,不如直接接受一个选择器:

    public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
    {
        TSource previous = default(TSource);
    
        using (var it = source.GetEnumerator())
        {
            if (it.MoveNext())
                previous = it.Current;
    
            while (it.MoveNext())
                yield return resultSelector(previous, previous = it.Current);
        }
    }
    

    如果您愿意,可以跳过中间对象:

    string[] items = new string[] { "a", "b", "c", "d" };
    var pairs = items.Pairwise((x, y) => string.Format("{0},{1}", x, y));
    
    foreach(var pair in pairs)
        Console.WriteLine(pair);
    

    或者你可以使用匿名类型:

    var pairs = items.Pairwise((x, y) => new { First = x, Second = y });
    

    更新:我刚刚在一个真实的项目中实现了这一点,并改用了C# 7.0 ValueTuple

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

    【讨论】:

    • 我想知道yield return …(previous, previous = …)中的执行顺序。 C# 语言是否保证在计算第二个参数之前准备好第一个参数?
    • 我有理由确定它确实如此,但我必须检查规范才能确定。
    • 是的,请参阅 C# 规范的 section 7.4.1“在函数成员调用的运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序计算,如下所示:...”
    • 只是想插播一下,我已经对此版本进行了一些性能分析,并使用了带有 Dequeue/Peek 的队列和 Zip 方法。 Queue 方法实际上比 GetEnumerator 方法快 2 倍,比 Zip 快 6 倍,我认为它比两者都更具可读性。例如var queue = new Queue(可枚举); while(queue.Count() > 1){ yield return func(queue.Dequeue,queue.Peek); }
    • 非常有趣...您可以将您的基准发布到 Gist 或其他内容吗?
    【解决方案3】:

    这是我使用堆栈的解决方案。简洁明了。

    string[] items = new string[] { "a", "b", "c", "d" };
    
    Stack<string> stack = new Stack<string>(items.Reverse());
    
    while(stack.Count > 1)
    {
      Console.WriteLine("{0},{1}", stack.Pop(), stack.Peek());
    }
    

    您可以采用相同的概念并使用队列,这避免了反转项目的需要并且更简单:

    var queue = new Queue<string>(items);
    
    while (queue.Count > 1)
    {
       Console.WriteLine("{0},{1}", queue.Dequeue(), queue.Peek());
    }
    

    关于性能的简短介绍:

    我认为重要的是要认识到,除非您知道某项任务会导致您的实际应用程序出现瓶颈,否则可能不值得弄清楚真正最快的方法是什么。相反,编写为您完成工作的代码。另外,使用你能记住的代码,这样下次你需要它时它很容易从你手中流走。

    不过,如果您关心 10.000.000 个随机字符串的一些性能数据:

    Run #1
      InputZip             00:00:00.7355567
      PairwiseExtension    00:00:00.5290042
      Stack                00:00:00.6451204
      Queue                00:00:00.3245580
      ForLoop              00:00:00.7808004
      TupleExtension       00:00:03.9661995
    
    Run #2
      InputZip             00:00:00.7386347
      PairwiseExtension    00:00:00.5369850
      Stack                00:00:00.6910079
      Queue                00:00:00.3246276
      ForLoop              00:00:00.8272945
      TupleExtension       00:00:03.9415258
    

    使用 Jon Skeet 的微型基准测试工具进行了测试。

    如果您想查看测试源代码,请访问此处:gist here

    【讨论】:

    • 这是非常低效的,特别是如果集合有很多元素。您的空间复杂度是 O(n) 而不是 O(1)。与此处的其他解决方案一样,您的时间复杂度也是 O(n),但仍慢一个常数因子。
    • 这与过早优化无关。您正在使用更多代码做比必要更多的工作。这只是糟糕的设计。
    • 嗯,这个页面上的一些更好的解决方案是可以使用的泛型方法,并且可以复制 - 粘贴到带有一些小参数检查的项目中。你的只是一个 3 行的想法。而且不是一个好人。您正在将空间复杂度从非常可口的 O(1) 增加到平庸的 O(n),并在任何情况下以零增益将执行时间加倍。
    • 确实 string.format 影响了结果 - 我复制/粘贴了原始解决方案 - 修复了该问题并将所有类型更改为 ValueTuple (好建议)也将测试切换为使用 James Holwell 的解决方案。从结果来看,我认为将任何给定的解决方案称为“低效”是不公平的
    • 赞成测试这一点。仍然不喜欢您的 O(n) 空间解决方案:D
    【解决方案4】:

    如果我忽略了某些东西,请原谅我,但为什么不做一些简单的事情,比如 for 循环?:

    public static List <int []> ListOfPairs (int [] items)
    {
        List <int> output = new List <int>();
        for (int i=0; i < items.Length-1; i++)
        {
            Int [] pair = new int [2];
            pair [0]=items [i];
            pair [1]=items [i+1];
            output.Add (pair);
        }
        return output;
    }
    

    【讨论】:

      【解决方案5】:

      在 .NET 4 中,这变得更加容易:-

      var input = new[] { "a", "b", "c", "d", "e", "f" };
      var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
      

      【讨论】:

      • 值得一提的是,这会计算两次 input - 对于数组来说不是问题,但如果它是惰性计算的,那可能会很昂贵。
      • 另外,Zip 的第二个参数可以作为方法组传递:…input.Zip(input.Skip(1), Tuple.Create);
      • 我只是在单元测试中做了这个,只是为了看看有什么不同。使用Enumerable.Range(0, count) 作为迭代器,我不得不将计数增加到大约 100 万,延迟才明显,并且在延迟到足以打扰我之前增加大约 1000 万。不过,@dahlbyk 的解决方案优雅地避免了这种情况,所以我随时都会使用它。 (扩展方法的整个point是能够将不太可读的代码隐藏在视线之外,所以这里的优先级应该是直截了当的......)。
      【解决方案6】:

      晚了一点,但作为所有这些扩展方法的替代方法,可以使用实际的“滑动”Collection 来保存(并丢弃)数据。

      这是我今天完成的一个:

      public class SlidingWindowCollection<T> : ICollection<T>
      {
          private int _windowSize;
          private Queue<T> _source;
      
          public SlidingWindowCollection(int windowSize)
          {
              _windowSize = windowSize;
              _source = new Queue<T>(windowSize);
          }
      
          public void Add(T item)
          {
              if (_source.Count == _windowSize)
              {
                  _source.Dequeue();
              }
              _source.Enqueue(item);
          }
      
          public void Clear()
          {
              _source.Clear();
          }
      
          ...and just keep forwarding all other ICollection<T> methods to _source.
      }
      

      用法:

      int pairSize = 2;
      var slider = new SlidingWindowCollection<string>(pairSize);
      foreach(var item in items)
      {
          slider.Add(item);
          Console.WriteLine(string.Join(", ", slider));
      }
      

      【讨论】:

        【解决方案7】:

        为方便起见,这里是@dahlbyk 答案的无选择器版本。

        public static IEnumerable<Tuple<T, T>> Pairwise<T>(this IEnumerable<T> enumerable)
        {
            var previous = default(T);
        
            using (var e = enumerable.GetEnumerator())
            {
                if (e.MoveNext())
                    previous = e.Current;
        
                while (e.MoveNext())
                    yield return Tuple.Create(previous, previous = e.Current);
            }
        }
        

        【讨论】:

        • 我觉得这个比原版还要干净。在现代 C# 中,这可以用作:foreach (var (previous, next) in Enumerable.Range(0, 10).PairWise()) Console.WriteLine(previous + "-" + next);
        【解决方案8】:

        最简单的方法是使用 ReactiveExtensions

        using System.Reactive;
        using System.Reactive.Linq;
        

        并让自己成为一个扩展方法来将它组合在一起

        public static IEnumerable<IList<T>> Buffer<T>(this IEnumerable<T> seq, int bufferSize, int stepSize)
        {
            return seq.ToObservable().Buffer(bufferSize, stepSize).ToEnumerable();
        }
        

        【讨论】:

        【解决方案9】:

        通过显式使用传递的迭代器扩展previous answer 以避免 O(n2) 方法:

        public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> input, int groupCount) {
          if (null == input) throw new ArgumentException("input");
          if (groupCount < 1) throw new ArgumentException("groupCount");
        
          var e = input.GetEnumerator();
        
          bool done = false;
          while (!done) {
            var l = new List<T>();
            for (var n = 0; n < groupCount; ++n) {
              if (!e.MoveNext()) {
                if (n != 0) {
                  yield return l;
                }
                yield break;
              }
              l.Add(e.Current);
            }
            yield return l;
          }
        }
        

        对于 C# 2,在扩展方法之前,从输入参数中删除“this”并作为静态方法调用。

        【讨论】:

        • 这不会返回问题要求的结果。 Enumerable.Range(1, 5).Tuples(2) 返回 {{1, 2}, {3, 4}, {5}} 而不是所需的 {{1, 2}, {2, 3}, {3, 4}, {4, 5}} 即滑动窗口。
        【解决方案10】:

        类似这样的:

        public static IEnumerable<TResult> Pairwise<T, TResult>(this IEnumerable<T> enumerable, Func<T, T, TResult> selector)
        {
            var previous = enumerable.First();
            foreach (var item in enumerable.Skip(1))
            {
                yield return selector(previous, item);
                previous = item;
            }
        }
        

        【讨论】:

          【解决方案11】:

          F#Seq 模块在IEnumerable&lt;T&gt; 上定义了pairwise 函数,但该函数不在.NET 框架中。

          如果它已经在 .NET 框架中,它可能会接受选择器函数,而不是返回对,因为 C# 和 VB 等语言不支持元组。

          var pairs = ns.Pairwise( (a, b) => new { First = a, Second = b };
          

          我认为这里的任何答案都没有真正改善您的简单迭代器实现,这对我来说似乎是最natural(以及从外观上看是海报dahlbyk!)。

          【讨论】:

            【解决方案12】:

            替代Pairs 实现,使用最后一对来存储先前的值:

            static IEnumerable<Pair<T, T>> Pairs( IEnumerable<T> collection ) {
              Pair<T, T> pair = null;
              foreach( T item in collection ) {
                if( pair == null )
                  pair = Pair.Create( default( T ), item );
                else
                  yield return pair = Pair.Create( pair.Second, item );
              }
            }
            

            简单的Window 实现(仅限私人使用,如果调用者不保存返回的数组;见注释):

            static IEnumerable<T[]> Window( IEnumerable<T> collection, int windowSize ) {
              if( windowSize < 1 )
                yield break;
            
              int index = 0;
              T[] window = new T[windowSize];
              foreach( var item in collection ) {
                bool initializing = index < windowSize;
            
                // Shift initialized window to accomodate new item.
                if( !initializing )
                  Array.Copy( window, 1, window, 0, windowSize - 1 );
            
                // Add current item to window.
                int itemIndex = initializing ? index : windowSize - 1;
                window[itemIndex] = item;
            
                index++;
                bool initialized = index >= windowSize;
                if( initialized )
                  //NOTE: For public API, should return array copy to prevent 
                  // modifcation by user, or use a different type for the window.
                  yield return window;
              }
            }
            

            使用示例:

            for( int i = 0; i <= items.Length; ++i ) {
              Console.WriteLine( "Window size {0}:", i );
              foreach( string[] window in IterTools<string>.Window( items, i ) )
                Console.WriteLine( string.Join( ", ", window ) );
              Console.WriteLine( );
            }
            

            【讨论】:

              【解决方案13】:

              C# 3.0 解决方案(对不起:)

              public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> sequence, int nTuple)
              {
                  if(nTuple <= 0) throw new ArgumentOutOfRangeException("nTuple");
              
                  for(int i = 0; i <= sequence.Count() - nTuple; i++)
                      yield return sequence.Skip(i).Take(nTuple);
              }
              

              这不是世界上性能最好的,但看起来确实令人愉快。

              真的,唯一使它成为 C# 3.0 解决方案的是 .Skip.Take 构造,因此,如果您只是将其更改为将该范围内的元素添加到列表中,那么它对于 2.0 来说应该是黄金。也就是说,它仍然没有性能。

              【讨论】:

              • 不是最好的?这是一个 O(n*n) 的实现!对于一个 10 项的小列表,整个列表被遍历了 20 次
              • 确实如此,但它也是两行(真实)代码,显然是一个简单的实现。由 OP 决定他是否需要一个快速的解决方案——也许他只需要对几十个项目的列表进行此操作。
              猜你喜欢
              • 2011-10-12
              • 2021-05-19
              • 2012-10-12
              • 2013-01-08
              • 1970-01-01
              • 2016-11-25
              • 2016-09-07
              • 2021-12-18
              • 1970-01-01
              相关资源
              最近更新 更多