Группировка списков по группам из X элементов на группу

У меня возникла проблема, зная наилучший способ сделать метод для группировки списка элементов в группы из (например) не более трех элементов. Я создал метод ниже, но без выполнения ToList в группе до того, как я верну его, у меня есть проблема с ним, если список перечисляется несколько раз.

Первый раз, когда он перечисляется, является правильным, но любое дополнительное перечисление отбрасывается, потому что две переменные (i и groupKey) кажутся запомненными между итерациями.

Итак, вопросы:

  • Есть ли лучший способ сделать то, что я пытаюсь достичь?
  • Является просто ToListing результирующей группой до того, как она покинет этот метод действительно такая плохая идея?

    public static IEnumerable<igrouping<int, tsource="">> GroupBy<tsource>
     (this IEnumerable<tsource> source, int itemsPerGroup)
    {
     const int initial = 1;
     int i = initial;
     int groupKey = 0;
     var groups = source.GroupBy(x =>
     {
     if (i == initial)
     {
     groupKey = 0;
     }
     if (i > initial)
     {
     //Increase the group key if we've counted past the items per group
     if (itemsPerGroup == initial || i % itemsPerGroup == 1)
     {
     groupKey++;
     }
     }
     i++;
     return groupKey;
     });
     return groups;
    }
    </tsource></tsource></igrouping<int,>
5 ответов

Здесь один из способов сделать это с помощью LINQ...

public static IEnumerable<igrouping<int, tsource="">> GroupBy<tsource>
 (this IEnumerable<tsource> source, int itemsPerGroup)
{
 return source.Zip(Enumerable.Range(0, source.Count()),
 (s, r) => new { Group = r / itemsPerGroup, Item = s })
 .GroupBy(i => i.Group, g => g.Item)
 .ToList();
}
</tsource></tsource></igrouping<int,>

Live Demo


Я думаю, вы ищете что-то вроде этого:

return source.Select((x, idx) => new { x, idx })
 .GroupBy(x => x.idx / itemsPerGroup)
 .Select(g => g.Select(a => a.x));

Вам нужно изменить свой тип возврата как IEnumerable<ienumerable<tsource>></ienumerable<tsource>


.net Fiddle

По существу у вас есть IEnumerable, и вы хотите сгруппировать его в IEnumerable из IGroupables, каждый из которых содержит ключ как индекс, а группу - как значения. Кажется, что ваша версия выполняется на первом проходе, но я думаю, что вы можете определенно потоковой передачи немного.

Использование skip и take - наиболее желательный способ, на мой взгляд, но настраиваемый ключ для группировки - это проблема. Для этого существует способ создания собственного класса в качестве шаблона группировки (в этом ответе: qaru.site/questions/154134/...).

Конечным результатом является следующее:

public static class GroupExtension
{
 public static IEnumerable<igrouping<int, t="">> GroupAt<t>(this IEnumerable<t> source, int itemsPerGroup)
 {
 for(int i = 0; i < (int)Math.Ceiling( (******)source.Count() / itemsPerGroup ); i++)
 {
 var currentGroup = new Grouping<int,t>{ Key = i };
 currentGroup.AddRange(source.Skip(itemsPerGroup*i).Take(itemsPerGroup));
 yield return currentGroup;
 }
 }
 private class Grouping<tkey, telement=""> : List<telement>, IGrouping<tkey, telement="">
 {
 public TKey Key { get; set; }
 }
}
</tkey,></telement></tkey,></int,t></t></t></igrouping<int,>

И вот демо в скрипке, которая потребляет его на простой строке

public class Program
{
 public void Main(){
 foreach(var p in getLine().Select(s => s).GroupAt(3))
 Console.WriteLine(p.Aggregate("",(s,val) => s += val));
 }
 public string getLine(){ return "Hello World, how are you doing, this just some text to show how the grouping works"; }
}

изменить

Альтернативно, как только IEnumerable из IEnumerable

public static IEnumerable<ienumerable<t>> GroupAt<t>(this IEnumerable<t> source, int itemsPerGroup)
{
 for(int i = 0; i < (int)Math.Ceiling( (******)source.Count() / itemsPerGroup ); i++)
 yield return source.Skip(itemsPerGroup*i).Take(itemsPerGroup);
}
</t></t></ienumerable<t>


Проблема с использованием GroupBy() заключается в том, что, если у нее не будет знаний под капотом, что вход упорядочен по значению ключа, он должен прочитать всю последовательность и выделить все в свое ведро, прежде чем он сможет испускать одну группу. Это переполнение в этом случае, поскольку ключ является функцией его порядкового положения в последовательности.

Мне нравится подход source.Skip(m).Take(n), но это делает предположения, что элементы в source могут быть напрямую адресованы. Если это неверно или Skip() и Take() не знают о базовой реализации, то производство каждой группы будет выполняться как операция O (n/2) в среднем, так как она повторно повторяется над source для создания группы.

Это делает общую операцию секционирования потенциально довольно дорогостоящей.

  • IF, производящий группу, представляет собой операцию O (n/2) в среднем, и
  • Учитывая размер группы s, требуется создание примерно n/s-групп,

Тогда общая стоимость операции - это что-то вроде O (n 2/2s), right?

Итак, я бы сделал что-то такое, операцию O (n) (не стесняйтесь использовать реализацию IGrouping, если хотите):

public static IEnumerable<************<int,t[]>> Partition<t>( this IEnumerable<t> source , int partitionSize )
{
 if ( source == null ) throw new ArgumentNullException("source") ;
 if ( partitionSize < 1 ) throw new ArgumentOutOfRangeException("partitionSize") ;
 int i = 0 ;
 List<t> partition = new List<t>( partitionSize ) ;
 foreach( T item in source )
 {
 partition.Add(item) ;
 if ( partition.Count == partitionSize )
 {
 yield return new ************<int,t[]>( ++i , partition.ToArray() ) ;
 partition.Clear() ;
 }
 }
 // return the last partition if necessary
 if ( partition.Count > 0 )
 {
 yield return new Partition<int,t>( ++i , items.ToArray() ) ;
 }
}
</int,t></int,t[]></t></t></t></t></************<int,t[]>


Это основано на Selman Select с идеей индекса, но с использованием ToLookup для объединения как GroupBy, так и Select вместе:

public static IEnumerable<ienumerable<tsource>> GroupBy<tsource>
 (this IEnumerable<tsource> source, int itemsPerGroup)
{ 
 return source.Select((x, idx) => new { x, idx })
 .ToLookup(q => q.idx / itemsPerGroup, q => q.x);
}
</tsource></tsource></ienumerable<tsource>

Основное отличие состоит в том, что ToLookup фактически оценивает результаты немедленно (как это кратко объясняется здесь: qaru.site/questions/79483/...), что может быть или не быть желательным.

licensed under cc by-sa 3.0 with attribution.