C#或滑动窗口枚举器中的成对迭代

时间:2009-02-23 13:17:01

标签: c# .net iterator ienumerable

如果我有一个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解决方案。

13 个答案:

答案 0 :(得分:50)

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

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

答案 1 :(得分:35)

不是要求元组(对)类型,为什么不接受选择器:

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

答案 2 :(得分:9)

最简单的方法是使用ReactiveExtensions

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

并将自己作为套件bash的扩展方法

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

答案 3 :(得分:5)

派对有点晚了,但作为所有这些扩展方法的替代方法,可以使用实际的“滑动”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));
}

答案 4 :(得分:3)

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

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”并作为静态方法调用。

答案 5 :(得分:2)

为方便起见,这里是@ 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);
    }
}

答案 6 :(得分:2)

这是我使用Stack的解决方案。它简洁明了。

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

答案 7 :(得分:2)

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

#include <iostream>
using namespace std;

void swap(int &a, int &b){
    b = (a+b) - (a=b);
}

int main() {
    int a=1,b=6;
    swap(a,b);
    cout<<a<<b;
    return 0;
}

答案 8 :(得分:1)

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的黄金。也就是说,它仍然没有表现。

答案 9 :(得分:0)

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

答案 10 :(得分:0)

F#Seq模块定义IEnumerable<T>上的成对函数,但此函数不在.NET框架中。

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

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

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

答案 11 :(得分:0)

这样的事情:

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

答案 12 :(得分:0)

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

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