Розділити список на списки з LINQ


377

Чи є спосіб я розділити List<SomeObject>на декілька окремих списків SomeObject, використовуючи індекс елемента як роздільник кожного розбиття?

Дозвольте мені пояснити:

У мене є List<SomeObject>і мені потрібен List<List<SomeObject>>або List<SomeObject>[], так що кожен із цих результатів буде містити групу з 3 елементів початкового списку (послідовно).

напр .:

  • Оригінальний список: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Результати списків: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Мені також знадобиться розмір списків, що отримуються, щоб бути параметром цієї функції.

Відповіді:


378

Спробуйте наступний код.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

Ідея полягає в тому, щоб спочатку групувати елементи за індексами. Розподіл на три має ефект групування їх в групи по 3. Потім перетворити кожну групу в список , і IEnumerableз Listдо Listз Listз


21
GroupBy робить неявний вид. Це може вбити продуктивність. Нам потрібна якась обернена функція SelectMany.
yfeldblum

5
@Justice, GroupBy може бути реалізований за допомогою хешування. Як ви знаєте, що впровадження GroupBy "може вбити продуктивність"?
Емі Б

5
GroupBy нічого не повертає, поки не перелічуються всі елементи. Ось чому це повільно. Списки, які ОП хоче, є суміжними, тому кращий метод може дати перший підпис, [a,g,e]перш ніж перераховувати будь-який інший вихідний список.
Полковник Паніка

9
Візьмемо крайній приклад нескінченного безлічі. GroupBy(x=>f(x)).First()ніколи не дасть групу. ОП запитала про списки, але якщо ми пишемо, щоб працювати з IEnumerable, зробивши лише одну ітерацію, ми отримаємо перевагу продуктивності.
Полковник Паніка

8
@Nick Замовлення не збереглося, але ваш шлях. Це все-таки добре знати, але ви б згрупували їх у (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Якщо замовлення не має значення, то це добре, але в цьому випадку це здається, що це має значення.
Reafexus

325

Це питання трохи старе, але я щойно написав це, і я думаю, що це трохи елегантніше, ніж інші запропоновані рішення:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}

14
Любіть це рішення. Я б рекомендував додати цю перевірку осудності , щоб запобігти нескінченний цикл: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach

10
Мені це подобається, але це не надто ефективно
Сем Шафрон

51
Мені це подобається, але ефективність часу є O(n²). Ви можете повторити список і отримати O(n)час.
hIpPy

8
@hIpPy, як це n ^ 2? Мені виглядає лінійно
Vivek Maharajh

13
@vivekmaharajh кожного разу sourceзамінюється загорнутою IEnumerable. Тож отримання елементів з sourceпроходить через шари Skips
Lasse Espeholt

99

Взагалі підхід, запропонований CaseyB, працює чудово, адже якщо ви проїжджаєте в List<T>ньому, це важко винувати, можливо, я б змінив його на:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Що дозволить уникнути масових викликів. Тим не менш, такий підхід має загальний недолік. Це матеріалізує два перерахування за шматок, щоб виділити проблему, спробуйте виконати:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Щоб подолати це, ми можемо спробувати підхід Камерона , який проходить вищевказаний тест на літаючі кольори, оскільки він лише один раз проходить перерахування.

Проблема полягає в тому, що вона має інший недолік, вона матеріалізує кожен предмет у кожному куску, проблема з таким підходом полягає в тому, що ви забираєте високу пам’ять.

Щоб проілюструвати спробу запуску:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Нарешті, будь-яка реалізація повинна мати змогу обробляти нерегулярні ітерації фрагментів, наприклад:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

Багато вкрай оптимальних рішень, як-от моя перша редакція цієї відповіді, не вдалося. Це ж питання можна побачити в оптимізованій відповіді casperOne .

Для вирішення всіх цих проблем ви можете скористатися наступним:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

Існує також цілий ряд оптимізацій, які ви можете ввести для ітерації шматок, що не є впорядку, що тут не вдається.

Щодо методу, який слід вибрати? Це повністю залежить від проблеми, яку ви намагаєтеся вирішити. Якщо вас не хвилює перший недолік, проста відповідь неймовірно приваблива.

Зауважте, як це стосується більшості методів, це не безпечно для багатопотокової нитки, речі можуть стати дивними, якщо ви хочете зробити її безпечною ниткою, вам потрібно буде внести зміни EnumeratorWrapper.


Буде помилка численною? .ToArray () кидає виняток?
Камерон Макфарланд

@SamSaffron Я оновив свою відповідь і надзвичайно спростив код, оскільки я вважаю, що це видатний випадок використання (і визнаю застереження).
casperOne

А що з чмотуванням IQueyable <>? Я думаю, що підхід Take / Skip був би оптимальним, якщо ми хочемо делегувати максимум операцій провайдеру
Guillaume86,

@ Guillaume86 Я згоден, якщо у вас є IList або IQueryable, ви можете приймати всілякі ярлики, які б зробили це набагато швидше (Linq робить це внутрішньо для всіляких інших методів)
Сем Шафрон

1
Це, безумовно, найкраща відповідь щодо ефективності. У мене виникає проблема використання SqlBulkCopy з IEnumerable, який запускає додаткові процеси в кожному стовпчику, тому він повинен працювати ефективно лише з одним проходом. Це дозволить мені розбити IEnumerable на шматочки керованого розміру. (Для тих, хто цікавиться, я включив режим потокового передавання SqlBulkCopy, який, здається, порушений).
Brain2000

64

Ви можете використати ряд запитів, які використовують Takeі Skip, але це додасть занадто багато ітерацій у вихідний список.

Швидше, я думаю, вам слід створити власний ітератор:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Потім ви можете зателефонувати цьому, і це ввімкнено LINQ, щоб ви могли виконувати інші операції над отриманими послідовностями.


У світлі відповіді Сема , я відчував, що існує простіший спосіб зробити це без:

  • Повторне повторення списку (що я не робив спочатку)
  • Матеріалізація елементів у групах перед випуском шматка (для великих фрагментів предметів можуть виникнути проблеми з пам'яттю)
  • Весь код, який виклав Сем

Це сказав, ось ще один пропуск, який я кодифікував у методі розширення, який IEnumerable<T>називається Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

Нічого дивного там, просто основна перевірка помилок.

Перехід до ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

В основному, він отримує IEnumerator<T>і вручну повторює кожен елемент. Він перевіряє, чи є в даний час якісь елементи, які потрібно перерахувати. Після того, як кожен шматок буде перерахований, якщо нічого не залишилося, він виривається.

Після того, як він виявить, що є елементи в послідовності, він делегує відповідальність за внутрішню IEnumerable<T>реалізацію ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Оскільки MoveNextвже було викликано IEnumerator<T>перехід до ChunkSequence, він дає позицію, яку повертає, Currentа потім збільшує кількість, зробивши так, щоб ніколи не повертати більше chunkSizeелементів та переходив до наступного пункту в послідовності після кожної ітерації (але короткозамкнений, якщо число кількість предметів перевищує розмір шматка).

Якщо елементів не залишилося, тоді InternalChunkметод зробить ще один прохід у зовнішньому циклі, але коли він MoveNextбуде викликаний вдруге, він все одно поверне помилковий, відповідно до документації (моє наголос):

Якщо MoveNext передає кінець колекції, нумератор розміщується після останнього елемента колекції, і MoveNext повертає помилкове значення. Коли перелічувач знаходиться в цьому положенні, наступні дзвінки на MoveNext також повертаються помилковими, поки не буде викликано Скидання.

У цей момент цикл розірветься, і послідовність послідовностей припиниться.

Це простий тест:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Вихід:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Важлива зауваження, це не спрацює, якщо ви не осушите всю дочірню послідовність або не перервете в будь-якій точці батьківської послідовності. Це важливий застереження, але якщо ваш випадок використання полягає в тому, що ви будете споживати кожен елемент послідовності послідовностей, то це допоможе вам.

Крім того, це зробить дивні речі, якщо ви будете грати з замовленням, так, як це робив Сем в один момент .


Я думаю, що це найкраще рішення ... Єдина проблема полягає в тому, що список не має довжини ... він має кількість. Але це легко змінити. Ми можемо зробити це кращим, навіть не будуючи списки, а повертаючи іменні величини, що містять посилання на головний список із комбінацією зміщення / довжини. Тож, якщо групування велике, ми не втрачаємо пам'ять. Прокоментуйте, якщо ви хочете, щоб я його написав.
Амір

@Amir Я хотів би, щоб це було написано
samndmoore

Це добре і швидко - Кемерон розмістив дуже схожий і після вашого, лише застереження полягає в тому, що він буферизує шматки, це може призвести до втрати пам’яті, якщо шматки і розміри предметів великі. Дивіться мою відповідь на альтернативну, хоч і чималу нору, відповідь.
Сам Шафран

@SamSaffron Так, якщо у вас є велика кількість елементів, у List<T>вас, очевидно, виникнуть проблеми з пам’яттю через буферизацію. З ретроспективою я мав би зазначити, що у відповіді, але, здавалося, на той момент увага зосереджувалася на занадто багато ітерацій. Однак, ваше рішення справді носить суперечливий характер. Я не перевіряв її, але тепер мене цікавить, чи є менш волохате рішення.
casperOne

@casperOne так ... Google дав мені цю сторінку, коли я шукав спосіб розділити перелічні дані, для мого конкретного випадку використання я розбиваю шалено великий список записів, які повертаються з db, якщо я їх матеріалізую на перерахуйте, що це може підірватись (насправді у Dapper є буфер: помилкова опція саме для цього випадку використання)
Сем Шафрон

48

Гаразд, ось я прийму це:

  • повністю лінивий: працює над нескінченними перелічувачами
  • відсутнє проміжне копіювання / буферизація
  • O (n) час виконання
  • працює також тоді, коли внутрішні послідовності споживаються лише частково

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Приклад використання

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Пояснення

Код працює за допомогою введення двох yieldітераторів на основі.

Зовнішній ітератор повинен відстежувати, скільки елементів ефективно споживається внутрішнім (кусковим) ітератором. Це робиться шляхом закриття над remainingз innerMoveNext(). Непотреблені елементи шматка відкидаються до того, як наступний шматок буде виданий зовнішнім ітератором. Це необхідно, тому що в іншому випадку ви отримуєте непослідовні результати, коли внутрішні перелічувальні дані не (повністю) споживаються (наприклад c3.Count(), повертаються 6).

Примітка: Відповідь було оновлено, щоб усунути недоліки, на які вказував @aolszowka.


2
Дуже хороший. Моє "правильне" рішення було набагато складніше за це. Це відповідь №1 IMHO.
CaseyB

Це страждає від несподіваної (з точки зору API) поведінки, коли викликається ToArray (), він також не є безпечним для потоків.
aolszowka

@aolszowka: Ви можете, будь ласка, докладно?
3dGrabber

@ 3dGrabber Можливо, саме так я повторно визначив ваш код (вибачте, його тут занадто давно минуло, в основному замість методу розширення, який я передав у sourceEnumerator). Я використовував тестовий випадок, який я мав для цього ефект: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Результат джерела вказує на наявність 13 фрагментів (кількість елементів). Це мало сенс для мене, якби ви не запитували внутрішні перерахування, коли перелік не збільшувався.
aolszowka

1
@aolszowka: дуже правильні бали. Я додав попередження та розділ використання. Код передбачає, що ви повторюєте внутрішнє число. Завдяки своєму рішенню ви втрачаєте лінь. Я думаю, що має бути можливість отримати найкраще з обох світів за допомогою користувальницького кешування IEnumerator. Якщо я знайду рішення, опублікую його тут ...
3dGrabber

18

повністю ледачий, не рахуючи чи копіюючи:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}

Це рішення настільки елегантне, що мені шкода, що я не можу підтримати цю відповідь не один раз.
Марк

3
Я не думаю, що це колись не вдасться. Але це, безумовно, може мати якусь дивну поведінку. Якби у вас було 100 предметів, і ви розділили на партії по 10, і ви перерахували всі партії, не перераховуючи жодних елементів з цих партій, ви отримаєте 100 партій з 1.
CaseyB

1
Як уже згадувалося @CaseyB, це страждає від того ж провалу спроби 3dGrabber звернувся тут stackoverflow.com/a/20953521/1037948 , але людина це швидко!
drzaus

1
Це прекрасне рішення. Робить саме те, що обіцяє.
Rod Hartzell

На сьогоднішній день найелегантнішим і точнішим рішенням. Єдине, ви повинні додати чек на від’ємні числа та замінити ArgumentNullException на ArgumentException
Romain Vergnory

13

Я думаю, що наступна пропозиція була б найшвидшою. Я жертвую ледачістю джерела Численні за вміння користуватися Array.Copy і заздалегідь знаючи довжину кожного з моїх списків.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}

Не просто швидкий, він також правильно обробляє подальші численні операції з результатом, тобто items.Chunk (5) .Reverse (). SelectMany (x => x)
теж

9

Ми можемо покращити рішення @ JaredPar, щоб зробити справжню ледачу оцінку. Ми використовуємо GroupAdjacentByметод, який дає групи послідовних елементів одним і тим же ключем:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Оскільки групи отримують одна за одною, це рішення працює ефективно з довгими або нескінченними послідовностями.


8

Я написав метод розширення Clump кілька років тому. Чудово працює, і це найшвидша реалізація тут. : P

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}

це повинно працювати, але воно буферизує на 100% шматки, я намагався цього уникнути ... але це виявляється неймовірно волохатим.
Сем Шафран

@SamSaffron Yep. Особливо, якщо ви кидаєте такі речі, як plinq, в суміш, саме для цього і було моєю реалізацією.
Камерон Макфарланд

розширив свою відповідь, дайте мені знати, що ви думаєте
Сем Шафрон

@CameronMacFarland - чи можете ви пояснити, чому потрібен другий чек на кількість == розмір? Дякую.
dugas

8

Buffer()Для цього передбачена System.Interactive . Деякі швидкі тестування показують, що продуктивність схожа на рішення Сама.


1
чи знаєте ви семантику буферизації? наприклад: якщо у вас є перерахувач, який розплітає струни великі 300 К і намагаєтеся розділити їх на шматки розміром 10 000, у вас залишиться пам'ять?
Сем Шафран

Buffer()повертається, IEnumerable<IList<T>>так, так, напевно, у вас там буде проблема - вона не тече як ваша.
дальбик

7

Ось список розповсюдження списку, який я написав пару місяців тому:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}

6

Я вважаю, що цей маленький фрагмент робить роботу досить непогано.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}

5

Як щодо цього?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Наскільки мені відомо, GetRange () лінійний за кількістю взятих предметів. Тож це має бути добре.


5

Це давнє запитання, але саме з цим я і закінчився; він перераховує нумерацію лише один раз, але створює списки для кожного з розділів. Він не страждає від несподіваної поведінки, коли ToArray()викликається, як це роблять деякі з реалізацій:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }

Було б добре перетворити це на метод розширення:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

+1 для вашої відповіді. Однак я рекомендую дві речі 1. використовувати foreach замість того, а використовуючи блок. 2. Передайте chunkSize в конструктор списку, щоб список знав його максимальний очікуваний розмір.
Usman Zafar

4

Ми виявили, що рішення Девіда Б працює найкраще. Але ми адаптували його до більш загального рішення:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

3
Це приємно, але зовсім відрізняється від того, про що просив оригінальний запитувач.
Емі Б

4

Це наступне рішення є найбільш компактним, що я міг би придумати, це O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}

4

Старий код, але це я використовував:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }

Після публікації я зрозумів, що це майже той самий код casperOne, опублікований 6 років тому зі зміною використання .Any () замість .Count (), оскільки мені не потрібен весь підрахунок, просто потрібно знати, чи існує такий .
Роберт Маккі

3

Якщо список має тип system.collections.generic, ви можете скористатись методом "CopyTo", доступним для копіювання елементів масиву в інші підмасиви. Ви визначаєте початковий елемент та кількість елементів для копіювання.

Ви також можете зробити 3 клони свого оригінального списку і використовувати "RemoveRange" у кожному списку, щоб зменшити список до потрібного розміру.

Або просто створити допоміжний метод, щоб зробити це за вас.


2

Це давнє рішення, але у мене був інший підхід. Я використовую Skipдля переміщення до потрібного зміщення та Takeвилучення потрібної кількості елементів:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}

1
Дуже схожий на підхід, який я використовував, але я рекомендую, щоб джерело не було безліч. Наприклад, якщо джерело є результатом запиту LINQ, то пропуск / прийняття спричинить nbChunk перерахування запиту. Могло дорого коштувати. Краще було б використовувати IList або ICollection як тип джерела. Це взагалі уникає проблеми.
RB Davidson

2

Для всіх, хто цікавиться пакетованим / підтримуваним рішенням, бібліотека MoreLINQ пропонує Batchметод розширення, який відповідає вашому запитуваному поведінці:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

BatchРеалізація схожа на відповідь Камерона MacFarland в , з додаванням перевантаження для перетворення шматка / пакет , перш ніж повернутися, і виконує дуже добре.


це має бути прийнятою відповіддю. Замість того, щоб винаходити колесо, слід використовувати
morelinq

1

Використання модульного розділення:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}

1

Просто поклав два мої центи. Якщо ви хочете "відробити" список (візуалізувати зліва направо), ви можете зробити наступне:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }


1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }

0

Я взяв основну відповідь і зробив це контейнером МОК, щоб визначити, куди поділитись. ( Бо хто насправді хоче розбити лише 3 статті, читаючи цю публікацію, шукаючи відповідь? )

Цей метод дозволяє розділити на будь-який тип предмета за потребою.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Тож для ОП код був би

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );

0

Настільки ефектний, як і підхід Сем Сафрона .

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}


0

Може працювати з нескінченними генераторами:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Демо-код: https://ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Але насправді я вважаю за краще написати відповідний метод без linq.


0

Заціни! У мене є список елементів із лічильником послідовностей та датою. Кожен раз, коли послідовність перезапускається, я хочу створити новий список.

Вих. список повідомлень.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Я хочу розділити список на окремі списки, коли лічильник перезапуститься. Ось код:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }

-1

Щоб вставити два мої центи ...

Скориставшись типом списку для джерела, яке буде чутним, я знайшов ще одне дуже компактне рішення:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.