LINQ ile Listeyi Alt Listelere Böl


377

Her bir bölmenin ayırıcısı olarak öğe dizinini kullanarak List<SomeObject>a'yı birkaç ayrı listeye ayırabilmemin bir yolu var mı SomeObject?

Örnek vereyim:

Ben bir List<SomeObject>ve ben bir List<List<SomeObject>>veya gerekir List<SomeObject>[], böylece bu sonuç listelerinin her biri (sırayla) orijinal listenin 3 öğeden oluşan bir grup içerecektir.

Örneğin.:

  • Orijinal Liste: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Sonuç listeleri: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Ayrıca bu işlevin bir parametre olması için sonuç listelerinin boyutu gerekir.

Yanıtlar:


378

Aşağıdaki kodu deneyin.

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

Fikir, ilk önce elemanları endekslere göre gruplamaktır. Üçe böler listesine her grup dönüştürmek 3 gruplar ayırarak etkisi vardır ve IEnumerablebir Lista Listait Lists


21
GroupBy örtük bir sıralama yapar. Bu performansı öldürebilir. İhtiyacımız olan şey bir çeşit SelectMany'nin tersidir.
yfeldblum

5
@Justice, GroupBy karma yoluyla uygulanabilir. GroupBy'nin "performansı öldürebileceğini" nasıl biliyorsunuz?
Amy B

5
GroupBy, tüm öğeler numaralandırılıncaya kadar hiçbir şey döndürmez. Bu yüzden yavaş. OP'nin istediği listeler bitişiktir, bu nedenle daha iyi bir yöntem [a,g,e]orijinal listenin daha fazla numaralandırılmasından önce ilk alt listeyi verebilir .
Albay Panik

9
Sonsuz bir IEnumerable örneğine bakalım. GroupBy(x=>f(x)).First()asla bir grup vermez. OP listeler hakkında sorular sordu, ancak IEnumerable ile çalışmak için yazdığımızda, yalnızca tek bir yineleme yaparak, performans avantajını elde ederiz.
Albay Panik

8
@Nick Order yine de korunmaz. Bilmek hala iyi bir şey ama onları (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Düzen önemli değilse, sorun olmaz ama bu durumda önemli gibi görünür.
Reafexus

325

Bu soru biraz eski, ama ben sadece yazdım ve sanırım diğer önerilen çözümlerden biraz daha zarif:

/// <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
Bu çözümü seviyorum. Sonsuz bir döngüyü önlemek için bu akıl sağlığı kontrolünü eklemenizi tavsiye ederim: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
Mart'ta

10
Bunu beğendim, ama süper verimli değil
Sam Saffron

51
Bunu beğendim ama zaman verimliliği O(n²). Listeyi tekrarlayabilir ve O(n)zaman kazanabilirsiniz .
hIpPy

8
@hIpPy, n ^ 2 nasıl? Bana doğrusal görünüyor
Vivek Maharajh

13
@vivekmaharajh source, IEnumerableher seferinde bir sarılmış ile değiştirilir . Yani öğeleri almak s sourcekatmanları geçerSkip
Lasse Espeholt

99

Genel olarak CaseyB tarafından önerilen yaklaşım gayet iyi çalışıyor, aslında bir pas geçiyorsa, onu hatalamakList<T> zordur, belki de bunu değiştirirdim:

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

Bu da büyük çağrı zincirlerinden kaçınacaktır. Bununla birlikte, bu yaklaşımın genel bir kusuru vardır. Koşuyu denemeyi vurgulamak için yığın başına iki numaralandırma gerçekleştirir:

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

Bunun üstesinden gelmek için , yukarıdaki sayımı sadece bir kez yürüdüğü için uçan renklerle geçen Cameron'ın yaklaşımını deneyebiliriz .

Sorun şu ki, farklı bir kusuru var, her bir parçadaki her maddeyi gerçekleştiriyor, bu yaklaşımla ilgili sorun bellekte yüksek koşmanız.

Çalışmayı denemek için:

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

Son olarak, herhangi bir uygulama, parçaların sıralı yinelemesini gerçekleştirebilmelidir, örneğin:

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

Bu cevabı ilk kez gözden geçirmem gibi son derece optimal çözümler burada başarısız oldu. Aynı sorun casperOne'un optimize edilmesinde de görülebilir cevabında .

Tüm bu sorunları çözmek için aşağıdakileri kullanabilirsiniz:

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


        }

    }
}

Burada kapsam dışı olan yığınların sıra dışı yinelemesi için tanıtabileceğiniz bir dizi optimizasyon da vardır.

Hangi yöntemi seçmelisiniz? Tamamen çözmeye çalıştığınız soruna bağlıdır. İlk kusurla ilgilenmiyorsanız, basit cevap inanılmaz derecede çekici.

Not çoğu yöntemlerde olduğu gibi bu bilgi size değişiklik gerekir parçacığı güvenli yapmak istiyorsanız çoklu iş parçacığı için, malzeme garip alabilirsiniz güvenli değil EnumeratorWrapper.


Hata Enumerable.Range (0, 100) .Cunk (3) .Reverse (). ToArray () yanlış veya Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () bir istisna mı attı?
Cameron MacFarland

@SamSaffron Cevabımı güncelledim ve önemli kullanım durumu (ve uyarıları kabul ediyorum) hissettiğim için kodu muazzam bir şekilde basitleştirdim.
casperOne

IQueryable <> parçalamaya ne dersiniz? Tahminimce, tedarikçiye en fazla operasyonu devretmek istiyorsak Take / Skip yaklaşımı en uygun olacaktır
Guillaume86

@ Guillaume86 Katılıyorum, eğer bir IList veya IQueryable'ınız varsa, bunu çok daha hızlı hale getirecek her türlü kısayolu alabilirsiniz (Linq bunu her türlü diğer yöntem için dahili olarak yapar)
Sam Saffron

1
Bu, verimlilik için açık ara en iyi cevaptır. Her sütunda ek işlemler çalıştıran bir IEnumerable ile SqlBulkCopy kullanarak bir sorun yaşıyorum, bu yüzden verimli bir şekilde sadece bir geçiş ile çalıştırmanız gerekir. Bu, IEnumerable'ı yönetilebilir boyutlu parçalara ayırmamı sağlayacaktır. (Merak edenler için, SqlBulkCopy'nin bozuk gibi görünen akış modunu etkinleştirdim).
Brain2000

64

Sen olabilir kullanmak sorgu numarasını kullanmak TakeveSkip , ama bu orijinal listedeki çok fazla tekrarlamalar eklersiniz, inanıyorum.

Bunun yerine, kendi yinelemenizi oluşturmanız gerektiğini düşünüyorum:

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

Daha sonra bunu çağırabilirsiniz ve LINQ etkinleştirilir, böylece sonuçlanan diziler üzerinde diğer işlemleri gerçekleştirebilirsiniz.


Sam'in cevabının ışığında, bunu yapmadan daha kolay bir yol olduğunu hissettim:

  • Listeyi tekrar tekrar (ilk başta yapmadığım)
  • Yığını serbest bırakmadan önce öğeleri gruplar halinde gerçekleştirme (öğelerin büyük parçaları için hafıza sorunları olacaktır)
  • Sam'in yayınladığı tüm kodlar

Bununla birlikte, şu şekilde IEnumerable<T>adlandırmak için bir uzantı yönteminde kodladığım başka bir geçiş var 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);
}

Orada şaşırtıcı bir şey yok, sadece temel hata kontrolü.

Şuraya taşınıyor 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);
}

Temel olarak, IEnumerator<T> her öğeyi ve manuel olarak yineler. Şu anda numaralandırılacak herhangi bir öğe olup olmadığını kontrol eder. Her yığın numaralandırıldıktan sonra, herhangi bir öğe kalmazsa, parçalanır.

Sekansta öğeler olduğunu tespit ettikten sonra, iç IEnumerable<T>uygulamanın sorumluluğunu 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());
}

Yana MoveNextzaten çağrıldı IEnumerator<T>geçirilen ChunkSequencebu tarafından döndürülen öğe verir Currentdaha dönmek asla emin yapma ve sonra sayısı artar chunkSizeöğeleri ve her yineleme sonra sırayla sonraki öğeye hareket (ama numarası eğer kısa devre elde edilen öğeler yığın boyutunu aşıyor).

Kalan öğe yoksa, InternalChunkyöntem dış döngüde başka bir geçiş yapar, ancak MoveNextikinci kez çağrıldığında , dokümantasyona göre hala vurgulanır (vurgu mayın):

MoveNext koleksiyonun sonunu geçerse, numaralandırıcı koleksiyondaki son öğeden sonra konumlandırılır ve MoveNext false değerini döndürür. Numaralandırıcı bu konumda olduğunda, MoveNext'e yapılan sonraki çağrılar da Sıfırla çağrılıncaya kadar false değerini döndürür.

Bu noktada, döngü kırılacak ve dizilerin sırası sona erecektir.

Bu basit bir testtir:

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

Çıktı:

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

Önemli bir not, bu olacak değil Üst dizisindeki herhangi bir noktada tüm çocuk sırayı veya mola tahliye yoksa çalışır. Bu önemli bir uyarıdır, ancak kullanım durumunuz her sekans dizisinin elemanını için işe yarayacaktır.

Ek olarak, Sam'in bir noktada yaptığı gibi, siparişle oynarsanız garip şeyler yapar .


Bu en iyi çözüm olduğunu düşünüyorum ... tek sorun listede uzunluğu yok ... Kont vardır. Ama bunu değiştirmek kolay. Listeler bile oluşturmadan, ana listeye referanslar içeren uzunlukları ofset / uzunluk kombinasyonuyla döndürerek bunu daha iyi hale getirebiliriz. O halde, eğer grup boyutu büyükse, hafızayı boşa harcamayız. Yazmamı istiyorsan yorum yap.
Amir

@Amir yazılı olduğunu görmek istiyorum
samandmoore

Bu güzel ve hızlı - Cameron sizinkinden sonra da çok benzer bir şey yayınladı, sadece uyarı, parçaları tamponladığı, parçaların ve öğe boyutlarının büyük olması durumunda bellek yetersizliğine yol açabilir. Daha kıllı da olsa bir alternatif için cevabımı görün.
Sam Saffron

@SamSaffron Evet, içinde çok sayıda öğe List<T>varsa, arabellekleme nedeniyle açıkçası bellek sorunları yaşayacaksınız . Geriye dönüp baktığımda, cevapta bunu belirtmeliydim, ama o zaman odak çok fazla yinelemeye benziyordu. Bununla birlikte, çözümünüz gerçekten daha kıllı. Test etmedim, ama şimdi daha az tüylü bir çözüm olup olmadığını merak ediyorum.
casperOne

@casperOne evet ... Google, numaralandırılabilirleri ayırmanın bir yolunu ararken bu sayfayı bana verdi, özel kullanım durumum için, db'den döndürdüğüm delicesine büyük bir kayıt listesini bölüyorum. Liste havaya uçurur (aslında dapper'ın bir tamponu vardır: sadece bu kullanım durumu için yanlış seçenek)
Sam Saffron

48

Tamam, işte benim almam:

  • tamamen tembel: sonsuz numaralandırılabilir üzerinde çalışır
  • ara kopyalama / tamponlama yok
  • O (n) yürütme süresi
  • iç sekanslar sadece kısmen tüketildiğinde de çalışır

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

Örnek Kullanım

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

açıklamalar

Kod, iki yieldtabanlı yineleyiciyi yuvalayarak çalışır .

Dış yineleyici, iç (yığın) yineleyici tarafından kaç öğenin etkili bir şekilde tüketildiğini takip etmelidir. Bu, remainingile kapatılarak yapılır innerMoveNext(). Bir yığının tüketilmemiş elemanları, bir sonraki yığın dış yineleyici tarafından verilmeden önce atılır. Bu gereklidir, çünkü aksi takdirde iç numaralandırıcılar (tamamen) tüketilmediğinde (örneğin c3.Count()6 dönecektir ) tutarsız sonuçlar elde edersiniz .

Not: Cevap, @aolszowka tarafından belirtilen eksiklikleri gidermek için güncellendi.


2
Çok hoş. Benim "doğru" çözümüm bundan çok daha karmaşıktı. Bu # 1 cevap IMHO.
CaseyB

Bu, ToArray () çağrıldığında beklenmeyen (API açısından) davranıştan muzdariptir, ayrıca iş parçacığı için güvenli değildir.
aolszowka

@aolszowka: Lütfen biraz ayrıntı verebilir misiniz?
3dGrabber

@ 3dGrabber Belki de kodunuzu nasıl yeniden çarpanlara ayırdım (özür dilerim, geçmişte biraz fazla geçmiş, temelde sourceEnumerator'da geçirdiğim bir uzatma yöntemi yerine). Kullandığım test senaryosu bu etkiye bir şeydi: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Tıknaz <int> (arrayToSort, 3) .ToArray (); Sonuçta Kaynak 13 parça (eleman sayısı) olduğunu gösterir. Bu, Numaralandırıcı'nın iç numaralandırmalarını sorgulamadığınız sürece bana mantıklı geldi.
aolszowka

1
@aolszowka: çok geçerli noktalar. Bir uyarı ve kullanım bölümü ekledim. Kod, numaralandırılabilir iç üzerinde yinelediğinizi varsayar. Çözümünüzle tembelliği kaybedersiniz. Özel, önbelleğe alınmış bir IEnumerator ile her iki dünyanın da en iyisini elde etmenin mümkün olması gerektiğini düşünüyorum. Bir çözüm
bulursam

18

tamamen tembel, sayma veya kopyalama yok:

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

Bu çözüm o kadar zarif ki, bu cevabı bir kereden fazla değerlendiremediğim için üzgünüm.
Mark

3
Bunun kesinlikle başarısız olacağını düşünmüyorum. Ama kesinlikle tuhaf bir davranışa sahip olabilir. 100 öğeniz varsa ve 10'luk gruplara bölündüyseniz ve bu grupların herhangi bir öğesini numaralandırmadan tüm partileri numaralandırdıysanız, 100'lük 1 parti ile sonuçlanacaksınız.
CaseyB

1
@CaseyB'in belirttiği gibi, bu burada ele alınan aynı başarısız 3dGrabber'den muzdarip stackoverflow.com/a/20953521/1037948 , ancak adam hızlı!
drzaus

1
Bu güzel bir çözüm. Tam olarak söz verdiği şeyi yapar.
Rod Hartzell

Şimdiye kadar en zarif ve noktaya çözüm. Tek şey, negatif sayılar için bir kontrol eklemeniz ve ArgumentNullException'ı bir ArgumentException ile değiştirmelisiniz
Romain Vergnory

13

Aşağıdaki önerilerin en hızlı olacağını düşünüyorum. Array.Copy kullanma yeteneği için numaralandırılabilir kaynağın tembelliğini feda ediyorum ve alt listelerimin her birinin uzunluğunu önceden biliyorum.

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

Sadece en hızlı değil, aynı zamanda sonuç üzerindeki diğer numaralandırılabilir işlemleri de doğru bir şekilde işler, yani öğeler.Cunk (5) .Reverse (). SelectMany (x => x)
çok

9

Gerçek tembel değerlendirme yapmak için @ JaredPar'ın çözümünü geliştirebiliriz. Biz birGroupAdjacentByAynı anahtarla ardışık eleman grupları veren yöntem :

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

Gruplar tek tek verildiğinden, bu çözelti uzun veya sonsuz dizilerle verimli bir şekilde çalışır.


8

Birkaç yıl önce bir Clump uzatma yöntemi yazdım. Harika çalışıyor ve buradaki en hızlı uygulama. : 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;
        }
    }
}

Çalışmalı ama parçaların% 100'ünü tamponluyor, bundan kaçınmaya çalışıyordum ... ama inanılmaz derecede kıllı olduğu ortaya çıktı.
Sam Saffron

@SamSaffron Yep. Özellikle plinq gibi şeyleri karışıma atarsanız, bu benim uygulamamın asıl amacıydı.
Cameron MacFarland

cevabımı genişlettim, ne düşündüğünü söyle
Sam Saffron

@CameronMacFarland - count == boyutunun ikinci kontrolünün neden gerekli olduğunu açıklayabilir misiniz? Teşekkürler.
dugas

8

System.Interactive sağlayan Buffer()bu amaçla. Bazı hızlı testler performansın Sam'in çözümüne benzediğini göstermektedir.


1
arabelleğe alma anlambilimini biliyor musunuz? örneğin: 300k büyüklüğünde dizeleri tüküren ve 10.000 büyüklüğüne bölmeye çalışan bir numaralandırıcınız varsa, bellek yetersiz kalır mı?
Sam Saffron

Buffer()geri dönüyor IEnumerable<IList<T>>evet, muhtemelen orada bir problemin var - seninki gibi akmıyor.
dahlbyk

7

İşte birkaç ay önce yazdığım rutinleri bölen bir liste:

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

Bu küçük pasajın işi oldukça iyi yaptığını görüyorum.

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

Peki ya bu?

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

Bildiğim kadarıyla, GetRange () alınan öğe sayısı bakımından doğrusaldır. Yani bu iyi bir performans sergilemeli.


5

Bu eski bir soru ama ben sonuçta bu; numaralandırılabilir öğeyi yalnızca bir kez numaralandırır, ancak bölümlerin her biri için listeler oluşturur. ToArray()Bazı uygulamaların yaptığı gibi çağrıldığında beklenmedik davranışlardan muzdarip değildir :

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

Bunu bir Uzantı yöntemine dönüştürmek iyi olur:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

Cevabınız için +1. Ancak iki şey tavsiye 1. foreach yerine süre kullanarak ve blok kullanarak. 2. Listenin beklenen maksimum boyutunu bilmesi için chunkSize öğesini Liste yapıcısına iletin.
Usman Zafar

4

David B'nin çözümünün en iyi sonucu verdiğini gördük. Ancak daha genel bir çözüme uyarladık:

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

3
Bu güzel, ama orijinal askerin istediklerinden oldukça farklı.
Amy B

4

Bu aşağıdaki çözüm, O (n) ile karşılaşabileceğim en kompakt çözümdür.

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

Eski kod, ama kullandığım şey bu:

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

Gönderdikten sonra, bunun neredeyse tam olarak aynı sayıya sahip olduğunu fark ettim. 6 yıl önce .Count () yerine .Aount () yerine yapılan değişiklikle birlikte yayınladım. .
Robert McKee

3

Liste system.collections.generic türündeyse, dizinizin öğelerini diğer alt dizilere kopyalamak için kullanılabilen "CopyTo" yöntemini kullanabilirsiniz. Kopyalanacak başlangıç ​​öğesini ve öğe sayısını belirtirsiniz.

Ayrıca, orijinal listenizin 3 klonunu yapabilir ve listeyi istediğiniz boyuta küçültmek için her listedeki "RemoveRange" kullanabilirsiniz.

Ya da sadece sizin için yapmak için bir yardımcı yöntem oluşturun.


2

Bu eski bir çözüm ama farklı bir yaklaşımım vardı. Kullanmak Skipofset istenen ve hareket Takeelemanlarının özü, istenen sayıda:

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
Kullandığım bir yaklaşıma çok benziyor, ancak bu kaynağın IEnumerable olmamasını tavsiye ediyorum. Örneğin, kaynak bir LINQ sorgusunun sonucuysa, Skip / Take, sorgunun nbChunk numaralandırmalarını tetikler. Pahalı olabilir. Daha iyisi, kaynak türü olarak IList veya ICollection kullanmak olacaktır. Bu, sorunu tamamen önler.
RB Davidson

2

Paketlenmiş / bakımı yapılmış bir çözümle ilgilenen herkes için MoreLINQ kütüphanesi, Batchistediğiniz davranışa uyan uzantı yöntemini sağlar :

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

BatchUygulama benzer Cameron MacFarland cevabı dönmeden önce yığın / toplu dönüştürmek için bir aşırı yük eklenmesiyle, ve gerçekleştirir oldukça iyi.


kabul edilen cevap bu olmalıdır. Tekerleği yeniden icat etmek yerine, morelinq kullanılmalıdır
Otabek Kholikov

1

Modüler bölümlemeyi kullanma:

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

Sadece iki sentimi koyuyorum. Listeyi "gruplamak" istiyorsanız (soldan sağa görselleştirin) aşağıdakileri yapabilirsiniz:

 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

Başka bir yol Rx Buffer operatörünü kullanmaktır

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();

IMHO en porper cevabı.
Stanislav Berkov

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

Birincil cevabı aldım ve nereye ayrılacağımı belirlemek için bir IOC konteyneri yaptım. ( Kim gerçekten sadece 3 maddeye bölünmek istiyor, bir cevap ararken bu yazıyı okurken? )

Bu yöntem, gerektiğinde herhangi bir öğe türüne bölünmesine izin verir.

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

Yani OP için kod

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

Yani performatic Sam Safran 'ın yaklaşımı.

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

Sonsuz jeneratörlerle çalışabilir:

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)

Demo kodu: 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

Ama aslında linq olmadan karşılık gelen yöntemi yazmayı tercih ederim.


0

Şuna bir bak! Bir sıra sayacı ve tarih içeren öğelerin bir listesi var. Sekans her başladığında yeni bir liste oluşturmak istiyorum.

Ör. mesaj listesi.

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

Sayaç yeniden başlarken listeyi ayrı listelere bölmek istiyorum. İşte kod:

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

İki sentimi eklemek için ...

Parçalanacak kaynağın liste türünü kullanarak, çok kompakt bir çözüm daha buldum:

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;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.