リストをNサイズの小さなリストに分割する


209

リストを一連の小さなリストに分割しようとしています。

私の問題:リストを分割する機能で、リストを正しいサイズのリストに分割できません。それらをサイズ30のリストに分割する必要がありますが、代わりにサイズ114のリストに分割しますか?

関数でリストをX個のサイズ30以下のリストに分割するにはどうすればよいですか?

public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30) 
{       
    List<List<float[]>> list = new List<List<float[]>>();

    for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
        List <float[]> subLocat = new List <float[]>(locations); 

        if (subLocat.Count >= ((i*nSize)+nSize))
            subLocat.RemoveRange(i*nSize, nSize);
        else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));

        Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
        list.Add (subLocat);
    }

    return list;
}

サイズ144のリストで関数を使用すると、出力は次のようになります。

インデックス:4、サイズ:120
インデックス:3、サイズ:114
インデックス:2、サイズ:114
インデックス:1、サイズ:114
インデックス:0、サイズ:114


1
LINQソリューションが受け入れられる場合、この質問はいくつかの助けになるかもしれません

具体的には、その前の質問に対するSam Saffronの回答。そして、これが学校の課題でない限り、私は彼のコードを使用して停止します。
jcolebrand 2012

回答:


268
public static List<List<float[]>> SplitList(List<float[]> locations, int nSize=30)  
{        
    var list = new List<List<float[]>>(); 

    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); 
    } 

    return list; 
} 

一般的なバージョン:

public static IEnumerable<List<T>> SplitList<T>(List<T> locations, int nSize=30)  
{        
    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); 
    }  
} 

したがって、リストの長さが数十億で、長さ30の小さなリストに分割し、すべての小さなリストからTake(1)のみを取得する場合でも、30アイテムのリストを作成し、そのうち29アイテムを捨てます。これはよりスマートに行うことができます!
Harald Coppoolse 2018年

これは実際に機能しますか?nSizeからnSizeの範囲を取得しているので、最初の分割で失敗しませんか?たとえば、nSizeが3で、配列がサイズ5の場合、返される最初のインデックス範囲はGetRange(3, 3)
Matthew Pigram

2
@MatthewPigramがテストされ、動作しています。Math.Minは最小値を取得するため、最後のチャンクがnSize(2 <3)未満の場合、残りのアイテムを含むリストを作成します。
Phate01 2018年

1
@HaraldCoppoolse OPは選択を要求しませんでした。リストを分割するためだけです
Phate01

@MatthewPigram最初の反復-GetRange(0,3)、2番目の反復-GetRange(3,2)
Serj-Tm 2018

381

この拡張メソッドを使用して、指定されたチャンクサイズでソースリストをサブリストにチャンク化することをお勧めします。

/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
    public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize) 
    {
        return source
            .Select((x, i) => new { Index = i, Value = x })
            .GroupBy(x => x.Index / chunkSize)
            .Select(x => x.Select(v => v.Value).ToList())
            .ToList();
    }
}

たとえば、18アイテムのリストをチャンクごとに5アイテムずつチャンク化すると、4つのサブリストのリストに、次のアイテムが含まれます:5-5-5-3。


25
これを本番環境で使用する前に、実行時のメモリとパフォーマンスへの影響を理解しておく必要があります。LINQが簡潔であり得るからといって、それが良いアイデアであるとは限りません。
Nick

4
確かに、@ Nickは一般的に何かをする前に考えることをお勧めします。LINQによるチャンク化は、何千回も繰り返されることの多い操作ではありません。通常、アイテムをバッチごとに、または並行して処理するために、チャンクリストを作成する必要があります。
ドミトリーパブロフ2017年

6
ここでは、メモリとパフォーマンスは大きな問題ではないと思います。たまたま、200,000レコードを超えるリストをそれぞれ約3000の小さなリストに分割する必要があり、このスレッドにたどり着きました。両方の方法をテストしたところ、実行時間はほぼ同じでした。その後、そのリストを3つのレコードからなるリストに分割することをテストしましたが、パフォーマンスは問題ありません。Serj-Tmのソリューションの方がわかりやすく、保守性も優れていると思います。
Silent Sojourner 2017年

2
ToList()s を省略して、遅延評価に魔法をかけるのが最善の場合があることに注意してください。
Yair Halberstadt

3
@DmitryPavlovこの間ずっと、selectステートメントでそのようなインデックスを投影できることを知りませんでした!これは2014年に投稿したことに気づくまでの新機能だと思いました。これを共有してくれてありがとう。また、この拡張メソッドをIEnumerableで使用できるようにし、IEnumerableを返すこともお勧めしませんか?
アイディン

37

どのように:

while(locations.Any())
{    
    list.Add(locations.Take(nSize).ToList());
    locations= locations.Skip(nSize).ToList();
}

これは大量のメモリを消費しますか?locations.Skip.ToListが発生するたびに、より多くのメモリが割り当てられ、スキップされなかったアイテムは新しいリストによって参照されるのでしょうか。
Zasz 2014

2
はい、ループごとに新しいリストが作成されます。はい、メモリを消費します。ただし、メモリの問題が発生している場合は、次のループでそのリストのインスタンスを収集する準備ができているため、これは最適化する場所ではありません。あなたはスキップすることでメモリとパフォーマンスをトレードできますがToList、私はそれを最適化しようとする気にはなりません。この実装から得られる主な利点は、理解しやすいことです。承認された回答を使用したい場合は、それらのリストは作成されませんが、少し複雑になります。
Rafal 14

2
.Skip(n)n呼び出されるたびに要素を反復処理しますが、これは問題ないかもしれませんが、パフォーマンスが重要なコードを検討することが重要です。stackoverflow.com/questions/20002975/...
Chakrava

@Chakrava確かに、私のソリューションはパフォーマンスクリティカルなコードでは使用されませんが、私の経験では、最初に動作するコードを記述してから、パフォーマンスクリティカルなものを決定します。たとえば、オブジェクトへのリンクが50のオブジェクトに対して実行される操作はほとんどありません。これはケースバイケースで評価する必要があります。
Rafal

@Rafal同意.Skip()します。会社のコードベースで多数のを見つけました。それらは「最適」ではないかもしれませんが、うまく機能します。とにかく、DB操作のようなものははるかに長くかかります。しかし、私は.Skip()(予想されるように)n番目の要素に直接ジャンプするのではなく、途中で各要素<nに「触れる」ことに注意することが重要だと思います。イテレータに要素への接触による副作用がある場合、.Skip()見つけにくいバグの原因となる可能性があります。
チャクラバ

11

Serj-Tmソリューションは問題ありません。これもリストの拡張メソッドとしての一般的なバージョンです(静的クラスに入れます):

public static List<List<T>> Split<T>(this List<T> items, int sliceSize = 30)
{
    List<List<T>> list = new List<List<T>>();
    for (int i = 0; i < items.Count; i += sliceSize)
        list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i)));
    return list;
} 

10

受け入れられた回答(Serj-Tm)が最も堅牢であると思いますが、一般的なバージョンを提案したいと思います。

public static List<List<T>> splitList<T>(List<T> locations, int nSize = 30)
{
    var list = new List<List<T>>();

    for (int i = 0; i < locations.Count; i += nSize)
    {
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
    }

    return list;
}

8

ライブラリMoreLinqはメソッドが呼び出されました Batch

List<int> ids = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements
int counter = 1;
foreach(var batch in ids.Batch(2))
{
    foreach(var eachId in batch)
    {
        Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId);
    }
    counter++;
}

結果は

Batch: 1, Id: 1
Batch: 1, Id: 2
Batch: 2, Id: 3
Batch: 2, Id: 4
Batch: 3, Id: 5
Batch: 3, Id: 6
Batch: 4, Id: 7
Batch: 4, Id: 8
Batch: 5, Id: 9
Batch: 5, Id: 0

ids 2つの要素を持つ5つのチャンクに分割されます。


これは受け入れられる答えである必要があります。または、少なくともこのページでかなり高い。
Zar Shardan

7

floatを含むすべての型をとるジェネリックメソッドがあり、ユニットテストされています。

    /// <summary>
    /// Breaks the list into groups with each group containing no more than the specified group size
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="values">The values.</param>
    /// <param name="groupSize">Size of the group.</param>
    /// <returns></returns>
    public static List<List<T>> SplitList<T>(IEnumerable<T> values, int groupSize, int? maxCount = null)
    {
        List<List<T>> result = new List<List<T>>();
        // Quick and special scenario
        if (values.Count() <= groupSize)
        {
            result.Add(values.ToList());
        }
        else
        {
            List<T> valueList = values.ToList();
            int startIndex = 0;
            int count = valueList.Count;
            int elementCount = 0;

            while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount)))
            {
                elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize;
                result.Add(valueList.GetRange(startIndex, elementCount));
                startIndex += elementCount;
            }
        }


        return result;
    }

ありがとう。maxCountパラメータ定義でコメントを更新できるかどうか疑問に思いますか?セーフティネット?
Andrew Jens

2
enumerableの複数の列挙に注意してください。values.Count()完全な列挙を引き起こし、次にvalues.ToList()別の列挙を引き起こします。values = values.ToList()それを行うのがより安全なので、すでに実現しています。
mhand

7

上記の答えの多くはうまく機能しますが、それらはすべて、終わることのないシーケンス(または本当に長いシーケンス)でひどく失敗します。以下は、可能な限り最高の時間とメモリの複雑さを保証する完全にオンラインの実装です。列挙可能なソースを正確に1回だけ反復し、遅延評価にyield returnを使用します。コンシューマは、反復ごとにリストを破棄し、メモリフットプリントbatchSizeを要素数のあるリストのメモリフットプリントと等しくすることができます。

public static IEnumerable<List<T>> BatchBy<T>(this IEnumerable<T> enumerable, int batchSize)
{
    using (var enumerator = enumerable.GetEnumerator())
    {
        List<T> list = null;
        while (enumerator.MoveNext())
        {
            if (list == null)
            {
                list = new List<T> {enumerator.Current};
            }
            else if (list.Count < batchSize)
            {
                list.Add(enumerator.Current);
            }
            else
            {
                yield return list;
                list = new List<T> {enumerator.Current};
            }
        }

        if (list?.Count > 0)
        {
            yield return list;
        }
    }
}

編集:今、OPを実現すると、List<T>をより小さなList<T>に分割することを求められるので、無限の列挙型に関する私のコメントはOPには適用されませんが、ここで最終的に他の人を助けるかもしれません。これらのコメントは、IEnumerable<T>機能への入力として使用する他の投稿されたソリューションへの応答ですが、列挙可能なソースを複数回列挙します。


IEnumerable<IEnumerable<T>>あまりList工事がかからないのでバージョンの方がいいと思います。
NetMage

@NetMage-の1つの問題IEnumerable<IEnumerable<T>>は、実装が、生成された各内部列挙型を完全に列挙するコンシューマーに依存する可能性が高いことです。私はその問題を回避する方法で解決策を語ることができると確信していますが、結果として生じるコードはかなり迅速に複雑になる可能性があると思います。また、遅延であるため、一度に1つのリストしか生成せず、サイズが事前にわかっているため、メモリ割り当てはリストごとに1回だけ行われます。
mhand 2018年

そうです-私の実装では、標準の列挙子をラップして現在の位置を追跡し、新しい位置に移動できる新しいタイプの列挙子(Position Enumerator)を使用しています。
NetMage 2018年

6

最後に非常に役立つmhandのコメントの後の追加

元の答え

ほとんどの解決策はうまくいくかもしれませんが、私は非常に効率的ではないと思います。最初のいくつかのチャンクの最初のいくつかのアイテムだけが必要であると仮定します。次に、シーケンス内のすべての(ジリオン)アイテムを反復処理する必要はありません。

以下は、最大で2回列挙されます。1回はテイク、もう1回はスキップです。使用するよりも多くの要素を列挙しません。

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
    (this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                     // while there are elements left
    {   // still something to chunk:
        yield return source.Take(chunkSize); // return a chunk of chunkSize
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}

これは何回シーケンスを列挙しますか?

ソースをのチャンクに分割するとしますchunkSize。最初のN個のチャンクのみを列挙します。列挙されたすべてのチャンクから、最初のM要素のみを列挙します。

While(source.Any())
{
     ...
}

AnyはEnumeratorを取得し、1 MoveNext()を実行して、Enumeratorを破棄した後に戻り値を返します。これはN回行われます

yield return source.Take(chunkSize);

参照ソースによると、これは次のようになります。

public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
    return TakeIterator<TSource>(source, count);
}

static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
    foreach (TSource element in source)
    {
        yield return element;
        if (--count == 0) break;
    }
}

フェッチされたチャンクの列挙を開始するまで、これはあまり効果がありません。複数のチャンクをフェッチしたが、最初のチャンクを列挙しないことにした場合、デバッガーに表示されるため、foreachは実行されません。

最初のチャンクの最初のM要素を取得することにした場合、yield returnは正確にM回実行されます。これの意味は:

  • 列挙子を取得する
  • MoveNext()とCurrent M回呼び出します。
  • 列挙子を破棄する

最初のチャンクが生成されて返された後、この最初のチャンクをスキップします。

source = source.Skip(chunkSize);

もう一度:参照ソースを見て、skipiterator

static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
    using (IEnumerator<TSource> e = source.GetEnumerator()) 
    {
        while (count > 0 && e.MoveNext()) count--;
        if (count <= 0) 
        {
            while (e.MoveNext()) yield return e.Current;
        }
    }
}

ご覧のとおり、チャンク内のすべての要素に対して1回ずつSkipIterator呼び出しますMoveNext()それは呼びませんCurrent

したがって、チャンクごとに、次のことが行われていることがわかります。

  • Any():GetEnumerator; 1 MoveNext(); 列挙子を破棄します。
  • 取る():

    • チャンクの内容が列挙されていない場合は何もありません。
    • コンテンツが列挙されている場合:GetEnumerator()、列挙されたアイテムごとに1つのMoveNextと1つのCurrent、Dispose列挙子。

    • Skip():列挙されたすべてのチャンク(チャンクのコンテンツではない):GetEnumerator()、MoveNext()chunkSize回、Currentなし!列挙子を破棄する

列挙子で何が発生するかを見ると、MoveNext()への呼び出しが多く、Current実際にアクセスすることに決めたTSource項目への呼び出しのみであることがわかります。

サイズchunkSizeのN個のチャンクを取得する場合は、MoveNext()を呼び出します。

  • Any()のN回
  • チャンクを列挙しない限り、まだテイクの時間はありません
  • Skip()のN倍のchunkSize

フェッチされたすべてのチャンクの最初のM要素のみを列挙することにした場合、列挙されたチャンクごとにMoveNextをM回呼び出す必要があります。

合計

MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)

したがって、すべてのチャンクのすべての要素を列挙することにした場合:

MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once

MoveNextが多くの作業であるかどうかは、ソースシーケンスのタイプによって異なります。リストと配列の場合、範囲外のチェックが行われる可能性がある、単純なインデックスの増分です。

ただし、IEnumerableがデータベースクエリの結果である場合は、データが実際にコンピューター上で実体化されていることを確認してください。そうでない場合、データは数回フェッチされます。DbContextとDapperは、アクセスする前にデータをローカルプロセスに適切に転送します。同じシーケンスを数回列挙すると、数回フェッチされません。Dapperはリストであるオブジェクトを返し、DbContextはデータがすでにフェッチされていることを記憶しています。

チャンクで項目の分割を開始する前にAsEnumerable()またはToLists()を呼び出すのが賢明かどうかは、リポジトリによって異なります


これはバッチごとに 2回列挙されませんか?ソース2*chunkSizeタイムを本当に列挙していますか?これは、列挙可能なソース(恐らくDBでサポートされているか、その他のメモ化されていないソース)によっては致命的です。この列挙型を入力として想像してみてEnumerable.Range(0, 10000).Select(i => DateTime.UtcNow)ください。列挙型はメモされていないため、列挙するたびに異なる時間になります
mhand

検討してくださいEnumerable.Range(0, 10).Select(i => DateTime.UtcNow)。呼び出すAnyことにより、毎回現在時刻を再計算します。ではそれほど悪くはありませんDateTime.UtcNowが、データベース接続/ SQLカーソルなどに裏打ちされた列挙型を検討してください。私は、開発者が「可算の複数の列挙」の潜在的な影響を理解していなかったので、DBコールの数千人が発行された例を見てきました- ReSharperのは、同様にこのためのヒントを提供
mhand

4
public static IEnumerable<IEnumerable<T>> SplitIntoSets<T>
    (this IEnumerable<T> source, int itemsPerSet) 
{
    var sourceList = source as List<T> ?? source.ToList();
    for (var index = 0; index < sourceList.Count; index += itemsPerSet)
    {
        yield return sourceList.Skip(index).Take(itemsPerSet);
    }
}

3
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
    return items.Select((item, index) => new { item, index })
                .GroupBy(x => x.index / maxItems)
                .Select(g => g.Select(x => x.item));
}

2

これはどう?アイデアは、1つのループのみを使用することでした。そして、ご存じのとおり、コード全体でIList実装のみを使用していて、Listにキャストしたくない場合もあります。

private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
    IList<T> auxList = new List<T>();
    int totalItems = list.Count();

    if (totalChunks <= 0)
    {
        yield return auxList;
    }
    else 
    {
        for (int i = 0; i < totalItems; i++)
        {               
            auxList.Add(list[i]);           

            if ((i + 1) % totalChunks == 0)
            {
                yield return auxList;
                auxList = new List<T>();                
            }

            else if (i == totalItems - 1)
            {
                yield return auxList;
            }
        }
    }   
}

1

もう一つ

public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
    var chunks = new List<IList<T>>();
    List<T> chunk = null;
    for (var i = 0; i < list.Count; i++)
    {
        if (i % chunkSize == 0)
        {
            chunk = new List<T>(chunkSize);
            chunks.Add(chunk);
        }
        chunk.Add(list[i]);
    }
    return chunks;
}

1
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
    {           
        var result = new List<List<T>>();
        for (int i = 0; i < source.Count; i += chunkSize)
        {
            var rows = new List<T>();
            for (int j = i; j < i + chunkSize; j++)
            {
                if (j >= source.Count) break;
                rows.Add(source[j]);
            }
            result.Add(rows);
        }
        return result;
    }

0
List<int> list =new List<int>(){1,2,3,4,5,6,7,8,9,10,12};
Dictionary<int,List<int>> dic = new Dictionary <int,List<int>> ();
int batchcount = list.Count/2; //To List into two 2 parts if you want three give three
List<int> lst = new List<int>();
for (int i=0;i<list.Count; i++)
{
lstdocs.Add(list[i]);
if (i % batchCount == 0 && i!=0)
{
Dic.Add(threadId, lstdocs);
lst = new List<int>();**strong text**
threadId++;
}
}
Dic.Add(threadId, lstdocs);

2
コードスニペットを提供するだけでなく、回答を説明する方が望ましい
Kevin

0

私はこれと同じニーズに遭遇し、LinqのSkip ()メソッドとTake()メソッドを組み合わせて使用​​しました。ここまでの反復回数を掛けた数を掛けると、スキップする項目の数が得られ、次のグループを取り出します。

        var categories = Properties.Settings.Default.MovementStatsCategories;
        var items = summariesWithinYear
            .Select(s =>  s.sku).Distinct().ToList();

        //need to run by chunks of 10,000
        var count = items.Count;
        var counter = 0;
        var numToTake = 10000;

        while (count > 0)
        {
            var itemsChunk = items.Skip(numToTake * counter).Take(numToTake).ToList();
            counter += 1;

            MovementHistoryUtilities.RecordMovementHistoryStatsBulk(itemsChunk, categories, nLogger);

            count -= numToTake;
        }

0

Dimitry Pavlov answereに基づいて削除し.ToList()ます。また、匿名クラスも避けてください。代わりに、ヒープメモリの割り当てを必要としない構造体を使用するのが好きです。(A ValueTupleも仕事をします。)

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    if (source is null)
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (chunkSize <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(chunkSize), chunkSize, "The argument must be greater than zero.");
    }

    return source
        .Select((x, i) => new ChunkedValue<TSource>(x, i / chunkSize))
        .GroupBy(cv => cv.ChunkIndex)
        .Select(g => g.Select(cv => cv.Value));
} 

[StructLayout(LayoutKind.Auto)]
[DebuggerDisplay("{" + nameof(ChunkedValue<T>.ChunkIndex) + "}: {" + nameof(ChunkedValue<T>.Value) + "}")]
private struct ChunkedValue<T>
{
    public ChunkedValue(T value, int chunkIndex)
    {
        this.ChunkIndex = chunkIndex;
        this.Value = value;
    }

    public int ChunkIndex { get; }

    public T Value { get; }
}

これは次のように使用でき、コレクションを1回だけ反復し、重要なメモリを割り当てません。

int chunkSize = 30;
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    foreach (var item in chunk)
    {
        // your code for item here.
    }
}

具体的なリストが実際に必要な場合は、次のようにします。

int chunkSize = 30;
var chunkList = new List<List<T>>();
foreach (var chunk in collection.ChunkBy(chunkSize))
{
    // create a list with the correct capacity to be able to contain one chunk
    // to avoid the resizing (additional memory allocation and memory copy) within the List<T>.
    var list = new List<T>(chunkSize);
    list.AddRange(chunk);
    chunkList.Add(list);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.