Парная итерация в C# или перечислитель скользящего окна

Если у меня есть 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 (с LINQ) есть больше (и более приятных) способов сделать это, но меня в первую очередь интересуют решения C# 2.0. Хотя я также буду признателен за решения C# 3.0.


person f3lix    schedule 23.02.2009    source источник
comment
Кажется, что он может иметь много общего с SmartEnumerator Джона Скита, который сообщает вам, является ли элемент последним в списке. msmvps.com/blogs/jon_skeet/archive/2007/ 27/07/   -  person Ben Voigt    schedule 02.08.2010
comment
Для справки, эта функция называется «Windowed» в F#: c" title="есть ли эквивалент f seq в окне c">stackoverflow.com/questions/8874901/   -  person Benjol    schedule 12.12.2014


Ответы (13)


В .NET 4 это становится еще проще:

var input = new[] { "a", "b", "c", "d", "e", "f" };
var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
person Ian Mercer    schedule 02.08.2010
comment
Стоит отметить, что это оценивает input дважды - не проблема для массива, но если он оценивается лениво, это может быть дорого. - person dahlbyk; 18.08.2012
comment
Кроме того, второй аргумент Zip можно передать как группу методов: …input.Zip(input.Skip(1), Tuple.Create); - person Jay; 11.03.2015
comment
Я только что сделал это в модульном тесте, просто чтобы увидеть разницу. С Enumerable.Range(0, count) в качестве итератора мне пришлось увеличить счет до ~ 1 миллиона, прежде чем задержка стала заметной, и ~ 10 миллионов, прежде чем она стала достаточно медленной, чтобы меня беспокоить. Тем не менее, решение @dahlbyk элегантно избегает этого, поэтому я бы использовал его в любой день. (Вся точка методов расширения заключается в том, чтобы иметь возможность скрыть не очень читаемый код от глаз, поэтому приоритеты здесь должны быть простыми...). - person Tomas Aschan; 08.11.2016

Вместо того, чтобы требовать тип кортежа (пары), почему бы просто не принять селектор:

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);
    }
}
person dahlbyk    schedule 17.10.2009
comment
Меня интересует порядок выполнения в yield return …(previous, previous = …). Гарантирует ли язык C#, что первый аргумент будет подготовлен до того, как будет оценен второй аргумент? - person stakx - no longer contributing; 27.01.2013
comment
Я достаточно уверен, что это так, но я должен проверить спецификацию, чтобы быть уверенным. - person dahlbyk; 28.01.2013
comment
Да, см. раздел 7.4.1 спецификации C#. Во время обработки вызова члена функции во время выполнения выражения или ссылки на переменные списка аргументов оцениваются в порядке слева направо следующим образом:... - person Scott Chamberlain; 14.07.2013
comment
Просто хотел отметить, что я провел некоторый анализ производительности этой версии и использовал метод Queue с Dequeue/Peek и Zip. Метод Queue на самом деле в два раза быстрее, чем метод GetEnumerator, и в 6 раз быстрее, чем Zip, и я считаю его более читабельным, чем оба. например var queue = new Queue‹T›(перечисляемый); while(queue.Count() › 1){ yield return func(queue.Dequeue,queue.Peek); } - person Novaterata; 24.09.2014
comment
Очень интересно ... можете ли вы опубликовать свой тест в Gist или что-то в этом роде? - person dahlbyk; 25.09.2014

Самый простой способ - использовать 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();
}
person bradgonesurfing    schedule 13.03.2013
comment
Вместо того, чтобы принуждать к/от наблюдаемого, компаньон интерактивных расширений для Rx (nuget.org/packages/ix -main) поставляется с IEnumerable<T> версией Buffer(): github.com/Reactive-Extensions/Rx.NET/blob/ - person dahlbyk; 01.05.2016

Просто для удобства вот версия ответа @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);
    }
}
person James Holwell    schedule 23.10.2013
comment
Я думаю, что это даже чище, чем оригинал. В современном C# это можно использовать как: foreach (var (предыдущий, следующий) в Enumerable.Range(0, 10).PairWise()) Console.WriteLine(предыдущий + - + следующий); - person Zar Shardan; 08.11.2019

Немного поздно для вечеринки, но в качестве альтернативы всем этим методам расширения можно использовать фактическое «скользящее» 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));
}
person Sphinxxx    schedule 08.02.2014

Вот мое решение с использованием стека. Он короткий и лаконичный.

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

Протестировано с помощью инструмента микротестирования Джона Скита.

Если вы хотите взглянуть на исходный код для теста, перейдите сюда: суть здесь

person Postlagerkarte    schedule 25.09.2014
comment
это очень неэффективно, особенно если в коллекции много элементов. Ваша космическая сложность равна O(n) вместо O(1). Ваша временная сложность также O (n), как и другие решения здесь, но все же медленнее на постоянный коэффициент. - person Zar Shardan; 08.11.2019
comment
Речь идет не о преждевременной оптимизации. Вы делаете больше работы, чем необходимо, с большим количеством кода. Это просто плохой дизайн. - person Zar Shardan; 08.11.2019
comment
Что ж, некоторые из лучших решений на этой странице — это универсальные методы, которые готовы к использованию и могут быть скопированы и вставлены в проект с некоторой незначительной проверкой параметров. Ваша идея всего в 3 строки. И не хороший. Вы увеличиваете сложность пространства с очень приемлемого O (1) до посредственного O (n) и удваиваете время выполнения при нулевом выигрыше в чем-либо. - person Zar Shardan; 08.11.2019
comment
@ZarShardan: я обновил свой ответ некоторыми результатами тестов. - person Postlagerkarte; 09.11.2019
comment
вы не тестируете одно и то же. в PairwiseExtension вы вызываете String.Format, что намного сложнее, чем Tuple‹string,string›.Create. Попробуйте заменить его решением Джеймса Холвелла и используйте ValueTuple вместо Tuple. Кроме того, часто итерация заканчивается раньше. Так что, возможно, добавьте .Take(N/2) перед ToList(). call, а в ваших решениях Count › N/2, где в вашем тесте N равно 10000000. - person Zar Shardan; 09.11.2019
comment
Действительно, формат string.format влиял на результаты - я скопировал / вставил исходные решения - исправил это и изменил все типы на ValueTuple (хорошее предложение), а также переключил тест на использование решения Джеймса Холвелла. Глядя на результаты, я не думаю, что справедливо называть какое-либо из предложенных решений неэффективным. - person Postlagerkarte; 09.11.2019
comment
проголосовал за попытку проверить это. Все еще не нравится ваше космическое решение O(n): D - person Zar Shardan; 09.11.2019

Что-то вроде этого:

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;
    }
}
person Quiz    schedule 30.03.2010

Расширение предыдущего ответа на избегайте подхода 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" из входного параметра и вызовите его как статический метод.

person Richard    schedule 23.02.2009
comment
Это не возвращает результат, который запрашивает вопрос. Enumerable.Range(1, 5).Tuples(2) возвращает {{1, 2}, {3, 4}, {5}} вместо желаемого {{1, 2}, {2, 3}, {3, 4}, {4, 5}}, которое является скользящим окном. - person Sammy S.; 27.04.2016

Простите, если я что-то упускаю из виду, но почему бы не что-то простое, например, цикл 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;
}
person Tim Earley    schedule 25.08.2018

Решение С# 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. Тем не менее, это все еще не работает.

person mqp    schedule 23.02.2009
comment
Не самый производительный? Это реализация O(n*n)! Для небольшого списка из 10 элементов весь список был пройден 20 раз. - person configurator; 23.02.2009
comment
Это правда, но это также две строки (настоящего) кода и явно простая реализация. ОП должен решить, нужно ли ему быстрое решение - может быть, ему нужна эта операция только для списков из пары десятков элементов. - person mqp; 23.02.2009

Альтернативная реализация 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( );
}
person Emperor XLII    schedule 04.03.2009

Модуль F# Seq определяет парную функцию над IEnumerable<T>, но эта функция отсутствует в платформе .NET.

Если бы он уже был в среде .NET, вместо возврата пар он, вероятно, принял бы функцию селектора из-за отсутствия поддержки кортежей в таких языках, как C# и VB.

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

Я не думаю, что какой-либо из ответов здесь действительно улучшает вашу простую реализацию итератора, которая казалась самой естественно для меня (и постер dahlbyk судя по всему!) тоже.

person Pete Montgomery    schedule 10.01.2010

Я создал слегка измененную версию кода, обновленного в конце 2020 года, в ответе @dahlbyk. Он лучше подходит для проектов с включенными ссылочными типами, допускающими значение NULL (<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);
}
person Georg Jung    schedule 19.05.2021