2009-02-23 1 views
37

Если у меня есть IEnumerable как:попарной итерация в C# или скользящего окно перечислитель

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.

+0

Это похоже на то, что он может поделиться большой реализацией с помощью «SmartEnumerator» Джона Скита, который сообщает вам, является ли элемент последним в списке. http://msmvps.com/blogs/jon_skeet/archive/2007/07/27/smart-enumerations.aspx –

+0

Для справки эта функция называется «Оконная» в F #: http://stackoverflow.com/questions/8874901/is-there-a-a-an-a-f-seq-windowed-in-c – Benjol

ответ

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. Тем не менее, он все еще не работает.

+5

Не самая результативная? Это реализация O (n * n)! Для небольшого списка из 10 предметов весь список был пройден 20 раз – configurator

+0

Это правда, но это также две строки (реального) кода и, очевидно, простая реализация. Это зависит от OP, чтобы решить, нужно ли ему быстрое решение - возможно, ему нужна эта операция только в списках из нескольких десятков элементов. – mquander

2

Расширение на previous answer, чтобы избежать из O (N) подход явным образом с помощью переданный итератора:

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

+0

Это не возвращает результат, на который задает вопрос. 'Enumerable.Range (1, 5) .Tuples (2)' возвращает '{{1, 2}, {3, 4}, {5}}' вместо желаемых '{{1, 2}, {2, 3}, {3, 4}, {4, 5}} 'это скользящее окно. –

0

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

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

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

Интересно о порядке выполнения в 'yield return ... (previous, previous = ...)'. Язык C# гарантирует, что первый аргумент будет подготовлен до того, как будет оценен второй аргумент? – stakx

+0

Я уверен, что это так, но я должен проверить спецификацию, чтобы быть уверенным. – dahlbyk

+2

Да, см. [Раздел 7.4.1] (http://msdn.microsoft.com/en-us/library/aa691335%28v=vs.71%29.aspx) спецификации C#. * «Во время обработки вызова функции-члена выражения или ссылки переменных списка аргументов оцениваются по порядку слева направо, следующим образом: ...«* –

0

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

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

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

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

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

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

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

Стоит отметить, что это дважды оценивает «ввод» - не проблема для массива, но если он лениво оценивается, что может быть дорогостоящим. – dahlbyk

+5

Кроме того, второй аргумент 'Zip' может быть передан как группа методов:' ... input.Zip (input.Skip (1), Tup le.Create); ' – Jay

+2

Я просто сделал это в модульном тесте, просто чтобы увидеть разницу. С 'Enumerable.Range (0, count)' в качестве итератора мне пришлось увеличить счет до ~ 1 миллиона, прежде чем задержка была заметной, и ~ 10 миллионов, прежде чем она была достаточно медленной, чтобы беспокоить меня. Тем не менее, решение @ dahlbyk элегантно избегает этого, поэтому я буду использовать его в любой день. (Вся * точка * методов расширения должна уметь скрывать нечитаемо читаемый код от прицела, поэтому приоритеты здесь должны быть простыми ...). –

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

Вместо того, чтобы принуждать к/от наблюдаемого, компаньон для интерактивных расширений для Rx (https://www.nuget.org/packages/ix-main) поставляется с 'IEnumerable ' версией 'Buffer()': https://github.com/Reactive-Extensions/Rx.NET/blob/2252cb4edbb25aca12005b9a912311edd2f095f3/Ix.NET/Source/System.Interactive/EnumerableEx.Single.cs#L211-L229 – dahlbyk

0

Для удобства здесь приведена безрезультатная версия ответа @ 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); 
    } 
} 
3

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

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

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