C# - elegant way of partitioning a list?

2019-01-08 15:16发布

问题:

I'd like to partition a list into a list of lists, by specifying the number of elements in each partition.

For instance, suppose I have the list {1, 2, ... 11}, and would like to partition it such that each set has 4 elements, with the last set filling as many elements as it can. The resulting partition would look like {{1..4}, {5..8}, {9..11}}

What would be an elegant way of writing this?

回答1:

Here is an extension method that will do what you want:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
    for (int i = 0; i < (source.Count / size) + (source.Count % size > 0 ? 1 : 0); i++)
        yield return new List<T>(source.Skip(size * i).Take(size));
}

Edit: Here is a much cleaner version of the function:

public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
    for (int i = 0; i < Math.Ceiling(source.Count / (Double)size); i++)
        yield return new List<T>(source.Skip(size * i).Take(size));
}


回答2:

Using LINQ you could cut your groups up in a single line of code like this...

var x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

var groups = x.Select((i, index) => new
{
    i,
    index
}).GroupBy(group => group.index / 4, element => element.i);

You could then iterate over the groups like the following...

foreach (var group in groups)
{
    Console.WriteLine("Group: {0}", group.Key);

    foreach (var item in group)
    {
        Console.WriteLine("\tValue: {0}", item);
    }
}

and you'll get an output that looks like this...

Group: 0
        Value: 1
        Value: 2
        Value: 3
        Value: 4
Group: 1
        Value: 5
        Value: 6
        Value: 7
        Value: 8
Group: 2
        Value: 9
        Value: 10
        Value: 11


回答3:

Something like (untested air code):

IEnumerable<IList<T>> PartitionList<T>(IList<T> list, int maxCount)
{
    List<T> partialList = new List<T>(maxCount);
    foreach(T item in list)
    {
        if (partialList.Count == maxCount)
        {
           yield return partialList;
           partialList = new List<T>(maxCount);
        }
        partialList.Add(item);
    }
    if (partialList.Count > 0) yield return partialList;
}

This returns an enumeration of lists rather than a list of lists, but you can easily wrap the result in a list:

IList<IList<T>> listOfLists = new List<T>(PartitionList<T>(list, maxCount));


回答4:

To avoid grouping, maths and reiteration.

IEnumerable<IList<T>> Partition<T>(this IEnumerable<T> source, int size)
{
    var partition = new T[size];
    var count = 0;

    foreach(var t in source)
    {
        partition[count] = t;
        count ++;

        if (count == size)
        {
            yield return partition;
            var partition = new T[size];
            var count = 0;
        }
    }

    if (count > 0)
    {
        Array.Resize(ref partition, count);
        yield return partition;
    }
}


回答5:

var yourList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var groupSize = 4;

// here's the actual query that does the grouping...
var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x);

// and here's a quick test to ensure that it worked properly...
foreach (var group in query)
{
    foreach (var item in group)
    {
        Console.Write(item + ",");
    }
    Console.WriteLine();
}

If you need an actual List<List<T>> rather than an IEnumerable<IEnumerable<T>> then change the query as follows:

var query = yourList
    .Select((x, i) => new { x, i })
    .GroupBy(i => i.i / groupSize, x => x.x)
    .Select(g => g.ToList())
    .ToList();


回答6:

Or in .Net 2.0 you would do this:

    static void Main(string[] args)
    {
        int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
        List<int[]> items = new List<int[]>(SplitArray(values, 4));
    }

    static IEnumerable<T[]> SplitArray<T>(T[] items, int size)
    {
        for (int index = 0; index < items.Length; index += size)
        {
            int remains = Math.Min(size, items.Length-index);
            T[] segment = new T[remains];
            Array.Copy(items, index, segment, 0, remains);
            yield return segment;
        }
    }


回答7:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list, int size)
{
    while (list.Any()) { yield return list.Take(size); list = list.Skip(size); }
}

and for the special case of String

public static IEnumerable<string> Partition(this string str, int size)
{
    return str.Partition<char>(size).Select(AsString);
}

public static string AsString(this IEnumerable<char> charList)
{
    return new string(charList.ToArray());
}


回答8:

Using ArraySegments might be a readable and short solution (casting your list to array is required):

var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; //Added 0 in front on purpose in order to enhance simplicity.
int[] array = list.ToArray();
int step = 4;
List<int[]> listSegments = new List<int[]>();

for(int i = 0; i < array.Length; i+=step)
{
     int[] segment = new ArraySegment<int>(array, i, step).ToArray();
     listSegments.Add(segment);
}


回答9:

I'm not sure why Jochems answer using ArraySegment was voted down. It could be really useful as long as you are not going to need to extend the segments (cast to IList). For example, imagine that what you are trying to do is pass segments into a TPL DataFlow pipeline for concurrent processing. Passing the segments in as IList instances allows the same code to deal with arrays and lists agnostically.

Of course, that begs the question: Why not just derive a ListSegment class that does not require wasting memory by calling ToArray()? The answer is that arrays can actually be processed marginally faster in some situations (slightly faster indexing). But you would have to be doing some fairly hardcore processing to notice much of a difference. More importantly, there is no good way to protect against random insert and remove operations by other code holding a reference to the list.

Calling ToArray() on a million value numeric list takes about 3 milliseconds on my workstation. That's usually not too great a price to pay when you're using it to gain the benefits of more robust thread safety in concurrent operations, without incurring the heavy cost of locking.



回答10:

You could use an extension method:

public static IList<HashSet<T>> Partition<T>(this IEnumerable<T> input, Func<T, object> partitionFunc)
{
      Dictionary<object, HashSet> partitions = new Dictionary<object, HashSet<T>>();

  object currentKey = null;
  foreach (T item in input ?? Enumerable.Empty<T>())
  {
      currentKey = partitionFunc(item);

      if (!partitions.ContainsKey(currentKey))
      {
          partitions[currentKey] = new HashSet<T>();
      }

      partitions[currentKey].Add(item);
  }

  return partitions.Values.ToList();

}



回答11:

To avoid multiple checks, unnecessary instantiations, and repetitive iterations, you could use the code:

namespace System.Collections.Generic
{
    using Linq;
    using Runtime.CompilerServices;

    public static class EnumerableExtender
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable?.GetEnumerator()?.MoveNext() ?? true;

        public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (size < 2)
                throw new ArgumentOutOfRangeException(nameof(size));
            IEnumerable<T> items = source;
            IEnumerable<T> partition;
            while (true)
            {
                partition = items.Take(size);
                if (partition.IsEmpty())
                    yield break;
                else
                    yield return partition;
                items = items.Skip(size);
            }
        }
    }
}