55

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

4

14 回答 14

59

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

var input = new[] { "a", "b", "c", "d", "e", "f" };
var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
于 2010-08-02T18:13:40.753 回答
43

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

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.0ValueTuple

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);
    }
}
于 2009-10-17T05:19:45.757 回答
12

最简单的方法是使用 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();
}
于 2013-03-13T08:29:49.100 回答
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);
    }
}
于 2013-10-23T15:26:26.267 回答
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));
}
于 2014-02-08T22:30:09.110 回答
4

这是我使用堆栈的解决方案。它简短而简洁。

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

于 2014-09-25T20:59:44.390 回答
2

通过显式使用传递的迭代器扩展先前的答案以避免 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”并作为静态方法调用。

于 2009-02-23T13:50:42.453 回答
2

像这样的东西:

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;
    }
}
于 2010-03-30T07:21:31.443 回答
2

如果我忽略了某些东西,请原谅我,但为什么不做一些简单的事情,比如 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;
}
于 2018-08-25T05:18:20.513 回答
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 来说应该是黄金。也就是说,它仍然没有性能。

于 2009-02-23T13:27:22.320 回答
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( );
}
于 2009-03-04T14:41:59.790 回答
0

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

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

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

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

于 2010-01-10T18:01:28.423 回答
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);
}
于 2021-05-19T14:37:37.233 回答
0

新的 C# 语言允许执行以下操作:

        var pairlist = new (string, string)[] { ("a", "b"), ("b", "c"), ("c", "d") };

        foreach (var pair in pairlist)
        {
            //do something with pair.Item1 & pair.Item2
于 2022-01-19T13:22:23.543 回答