【问题标题】:C# - elegant way of partitioning a list?C# - 分区列表的优雅方式?
【发布时间】:2009-09-08 20:06:17
【问题描述】:

我想通过指定每个分区中的元素数将一个列表划分为一个列表列表。

例如,假设我有一个列表 {1, 2, ... 11},并且希望将它划分为每个集合有 4 个元素,最后一个集合填充尽可能多的元素。生成的分区看起来像 {{1..4}, {5..8}, {9..11}}

写这个的优雅方式是什么?

【问题讨论】:

  • 我相信有人会发布一个不错的 linqy 声明。
  • @Preet - 我根据您的要求发布了 linq 答案;)

标签: c# list data-partitioning


【解决方案1】:

这是一个扩展方法,可以做你想做的事:

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

编辑:这是一个更简洁的函数版本:

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

【讨论】:

  • for (int i = 0; i
  • 这个方法的一个不幸的效果是给定的数组不能通过索引访问。这里有一个方法返回一个 List 而不是 vcskicks.com/partition-list.php
  • 请注意,在实际的 LINQ 实现中,SkipTake 只是在给定序列上循环,如果源正在实现 IList 并因此可以访问,则没有检查/优化按索引。因此,它们是 O(m)(其中 m 是您要跳过或采用的元素数量),并且此 Partition() 扩展可能无法提供预期的性能。
  • @George:(至少现在)您可以在枚举上调用.ToList() 以获取可索引列表。
【解决方案2】:

使用 LINQ,您可以像这样在一行代码中拆分组...

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

然后您可以像下面这样遍历组...

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

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

你会得到一个看起来像这样的输出......

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

【讨论】:

  • 不完全符合问题的要求,但 +1 的想法有点不同。
  • RichardOD - 你是对的 - 我更新了示例,以便输出是一组整数而不是一组匿名类型。
  • 我想你让我大吃一惊。我真的很想知道你在哪里学过这样的语法(我真的很喜欢)。我见过的所有 LINQ 文档都很好——但它们并没有很好地涵盖分组。
  • 大量修补 + 阅读 SO 问题。 LINQ 绝对是我在 3.5 中最喜欢的新特性之一——我只是在这里闲逛就学到了很多。 GroupBy 的这种重载是我以前没有使用过的东西 - 所以这对我来说也是新的:)
  • @ScottIvey 非常好的分组逻辑,非常适合我基于内部 List.Count() 将出站 UDP 命令分解为多个数据包所需的一些逻辑。好一个!感谢分享。
【解决方案3】:

类似(未经测试的空气代码):

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

这将返回列表的枚举而不是列表的列表,但您可以轻松地将结果包装在列表中:

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

【讨论】:

  • 我喜欢这个解决方案,但是如果将大量数字传递给 maxCount 可能会导致问题(例如:PartitionList(list, enablePartition ? 500 : int.MaxValue) 一个可能的改进是仅在源实现 ICollection 并将 maxCount 限制为元素数量时才设置列表容量在集合中。
  • @tigrou - 我不确定我是否会保护调用者免受传递过大数字的后果,但为了能够处理任意大分区,您可能会使用枚举而不是列表 - 例如一种方法IEnumerable&lt;IEnumerable&lt;T&gt;&gt; PartitionEnumeration&lt;T&gt; (IEnumerable&lt;T&gt; enumeration, int maxCount),无需分配列表即可轻松实现。
  • 如果您返回 IEnumerable&lt;IEnumerable&lt;T&gt;&gt; 并依赖从不分配任何东西的实现(例如:它只从源中产生元素),如果结果没有按顺序枚举,您将遇到麻烦(例如:分区 4 是在分区 2 之前枚举或仅部分枚举某些分区)。我认为列表更安全。
【解决方案4】:

为了避免分组、数学和重复。

该方法避免了不必要的计算、比较和分配。包括参数验证。

这是working demonstration on fiddle

public static IEnumerable<IList<T>> Partition<T>(
    this IEnumerable<T> source,
    int size)
{
    if (size < 2)
    {
        throw new ArgumentOutOfRangeException(
            nameof(size),
            size,
            "Must be greater or equal to 2.");  
    }

    T[] partition;
    int count;

    using (var e = source.GetEnumerator())
    {
        if (e.MoveNext())
        {
            partition = new T[size];
            partition[0] = e.Current;
            count = 1;
        }
        else
        {
            yield break;    
        }

        while(e.MoveNext())
        {
            partition[count] = e.Current;
            count++;

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

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

【讨论】:

  • 你的是所有可能的解决方案中最优雅和资源消耗最少的,我不知道为什么它没有更多的支持
  • 我喜欢这个,为什么ArgumentOutOfRangeException 换成1?您可以将其更改为size &lt; 1,然后在分配给partition[0] 之后的if (e.MoveNext() 块中添加if (size == 1) yield return partition; else count = 1;
  • 我确实考虑过这一点,但是,如果您想要小于 2 的分区,调用该函数是非常浪费的:只需枚举列表,但我接受它会使函数变脆,或者提供信息。
  • 感谢您转达您的想法,我想您已经考虑过了,并得出结论认为该场景有更好的实现 - 并且由调用函数(职责范围)来做出决定.
【解决方案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();
}

如果您需要实际的 List&lt;List&lt;T&gt;&gt; 而不是 IEnumerable&lt;IEnumerable&lt;T&gt;&gt;,请按如下方式更改查询:

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

【讨论】:

    【解决方案6】:

    或者在 .Net 2.0 中你会这样做:

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

      对于字符串的特殊情况

      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】:

        使用 ArraySegments 可能是一种可读且简短的解决方案(需要将列表转换为数组):

        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】:

          我不确定为什么 Jochems 使用 ArraySegment 的回答被否决了。只要您不需要扩展段(强制转换为 IList),它就会非常有用。例如,假设您尝试将段传递到 TPL DataFlow 管道以进行并发处理。将段作为 IList 实例传递允许相同的代码以不可知的方式处理数组和列表。

          当然,这引出了一个问题:为什么不直接通过调用 ToArray() 派生一个不需要浪费内存的 ListSegment 类?答案是,在某些情况下,数组实际上可以稍微快一点(索引稍微快一点)。但是你必须做一些相当硬核的处理才能注意到很大的不同。更重要的是,没有好的方法可以防止其他引用列表的代码进行随机插入和删除操作。

          在我的工作站上调用一百万值数字列表的 ToArray() 大约需要 3 毫秒。当您使用它来获得并发操作中更强大的线程安全性的好处时,这通常不会付出太大的代价,而不会产生高昂的锁定成本。

          【讨论】:

            【解决方案10】:

            您可以使用扩展方法:

            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】:

              为避免多次检查、不必要的实例化和重复迭代,您可以使用以下代码:

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

              【讨论】:

                猜你喜欢
                • 2018-04-01
                • 2020-09-04
                • 1970-01-01
                • 2018-10-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-03-25
                相关资源
                最近更新 更多