【问题标题】:Splitting a list of items evenly between x number of smaller lists在 x 个较小的列表之间平均拆分项目列表
【发布时间】:2014-08-07 03:25:42
【问题描述】:

我再次问这个问题,自从我上次问它以来,它被错误地标记为重复。这次我将包含更多信息,这可能会让我更容易理解我的需求(这很可能是我自己没有正确定义问题的错)。

我正在尝试将一个泛型类型的列表拆分为 4 个列表。为简单起见,我将在此示例中使用整数列表,但这不应该有所作为。

我做了很多搜索,找到了多个答案,如"Split List into Sublists with LINQ"using batch methods to split,我尝试过 MoreLinq 的 Batch 方法等等。这些建议可以很好地发挥它们的作用,但它们并没有按照我需要的方式发挥作用。

如果我有一个包含以下元素的列表(1-25 的整数):

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] 

然后我需要做的是创建 4 个列表,其中包含可变数量的元素,其中元素在同一个列表中递增,而不是使用下一个元素跳转到下一个列表。

[ 1,  2,  3,  4,  5,  6,  7]
[ 8,  9, 10, 11, 12, 13, 14]
[15, 16, 17, 18, 19, 20, 21]
[20, 21, 22, 23, 24, 25]

在链接的任一问题中使用解决方案时,以 4 个“部分”作为参数,我得到这样的列表(这是元素跳转到下一个列表而不是列表的下一个元素的示例):

[1, 5,  9, 13, 17, 21, 25],
[2, 6, 10, 14, 18, 22, 26],
[3, 7, 11, 15, 19, 23, 27],
[4, 8, 12, 16, 20, 24]

或者这个(和MoreLinq的Batch方法一样)

[ 1,  2,  3,  4],
[ 5,  6,  7,  8],
[ 9, 10, 11, 12],
[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24],
[25, 26, 27],

所以第一个解决方案将列表拆分为 4 个列表,但将元素按错误的顺序放置。第二种解决方案以正确的顺序拆分列表,但长度不正确。在最后一个解决方案中,他得到了 X 个列表,每个列表中有 4 个元素,我需要有 4 个列表,每个列表中有 X 个元素。

【问题讨论】:

  • 您要拆分List<T> 或任何IEnumerable<T>
  • 无所谓。我正在使用一个列表,但我猜想使它对所有 IEnumerable 都通用。但如果它让它变得更复杂,它应该只是用于列表。
  • 你在做什么?我不明白。我认为你需要重新考虑你的解释。
  • 我不明白您在 7 而不是 6 之后分割第一个列表的标准。
  • 基本上问题是“在 x 个较小的列表之间平均拆分项目列表”

标签: c# linq list


【解决方案1】:

您可以使用以下扩展方法将列表拆分为所需数量的子列表,并在第一个子列表中包含其他项目:

public static IEnumerable<List<T>> Split<T>(this List<T> source, int count)
{
    int rangeSize = source.Count / count;
    int firstRangeSize = rangeSize + source.Count % count;
    int index = 0;

    yield return source.GetRange(index, firstRangeSize);
    index += firstRangeSize;

    while (index < source.Count)
    {         
        yield return source.GetRange(index, rangeSize);
        index += rangeSize;
    }
}

给定输入

var list = Enumerable.Range(1, 25).ToList();
var result = list.Split(4);

结果是

[
  [ 1, 2, 3, 4, 5, 6, 7 ],
  [ 8, 9, 10, 11, 12, 13 ],
  [ 14, 15, 16, 17, 18, 19 ],
  [ 20, 21, 22, 23, 24, 25 ]
]

更新:此解决方案为每个范围添加了额外的项目

public static IEnumerable<List<T>> Split<T>(this List<T> source, int count)
{
    int rangeSize = source.Count / count;
    int additionalItems = source.Count % count;
    int index = 0;

    while (index < source.Count)
    {   
        int currentRangeSize = rangeSize + ((additionalItems > 0) ? 1 : 0);
        yield return source.GetRange(index, currentRangeSize);
        index += currentRangeSize;
        additionalItems--;
    }
}

【讨论】:

  • 这非常接近我的需要,但由于某种原因,每 4 项以上的剩余项目被放入第一个列表中,而不是拆分到下一个列表中。例如,如果有 27 个元素,我有长度为 9、6、6 和 6 的列表,它们可能应该是 7、7、7 和 6。
  • @Loyalar 好的,从示例输出中不清楚。我会更新答案
  • @Loyalar 我添加了解决方案,可以根据需要将源代码拆分为 7、7、7、6
  • 正是我需要的更新。非常感谢。
  • @Jodrell 当然可以。扩展方法是一种简单的静态方法。 IE。您的扩展方法调用将被编译为Extensions.Segment(someCollection, 4)。这只是语法糖。
【解决方案2】:

这是基于IEnumerable&lt;T&gt; 的另一种解决方案。它具有以下特点:

  • 总是产生batchCount 项,如果源可枚举小于批量大小,它将产生空列表。
  • 倾向于在前面放置较大的列表(例如,当 batchCount 为 2 且大小为 3 时,结果的长度将为 [2,1]。
  • 多次迭代 IEnumerable。这意味着如果在此处执行实体框架查询之类的操作,则应在某处调用 AsEnumerable。

第一个示例针对List&lt;T&gt;进行了优化

public static class BatchOperations
{
    public static IEnumerable<List<T>> Batch<T>(this List<T> items, int batchCount)
    {
        int totalSize = items.Count;
        int remain = totalSize % batchCount;
        int skip = 0;
        for (int i = 0; i < batchCount; i++)
        {
            int size = totalSize / batchCount + (i <= remain ? 1 : 0);
            if (skip + size > items.Count) yield return new List<T>(0);
            else yield return items.GetRange(skip, size);
            skip += size;
        }
    }

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int batchCount)
    {
        int totalSize = items.Count();
        int remain = totalSize%batchCount;
        int skip = 0;
        for (int i = 0; i < batchCount; i++)
        {
            int size = totalSize/batchCount + (i <= remain ? 1 : 0);
            yield return items.Skip(skip).Take(size);
            skip += size;
        }
    }
}

【讨论】:

    【解决方案3】:

    Sergey 的回答显然是最好的,但为了完整起见,如果您出于某种原因不想复制子列表(可能是因为您刚刚输入 IEnumerable&lt;T&gt;),可以使用以下解决方案:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace ConsoleApp1
    {
        public static class EnumerableExt
        {
            public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> input, int blockCount, int count)
            {
                int blockSize = count/blockCount;
                int currentBlockSize = blockSize + count%blockSize;
    
                var enumerator = input.GetEnumerator();
    
                while (enumerator.MoveNext())
                {
                    yield return nextPartition(enumerator, currentBlockSize);
                    currentBlockSize = blockSize;
                }
            }
    
            private static IEnumerable<T> nextPartition<T>(IEnumerator<T> enumerator, int blockSize)
            {
                do
                {
                    yield return enumerator.Current;
                }
                while (--blockSize > 0 && enumerator.MoveNext());
            }
        }
    
        class Program
        {
            private void run()
            {
                var list = Enumerable.Range(1, 25).ToList();
                var sublists = list.Partition(4, list.Count);
    
                foreach (var sublist in sublists)
                    Console.WriteLine(string.Join(" ", sublist.Select(element => element.ToString())));
            }
    
            static void Main()
            {
                new Program().run();
            }
        }
    }
    

    我想这会比使用 Lists 慢得多,但它会使用更少的内存。

    【讨论】:

    • 有趣的解决方案(虽然它将所有“附加”项目添加到第一个块,但它很容易修复,因此您将 blockSize 传递给第二个方法)
    【解决方案4】:
            const int groupSize = 4;
    
            var items = new []{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25};
    
            var currentGroupIndex=-1;
    
            var step1 = items.Select(a =>{
                if (++currentGroupIndex >= groupSize)
                    currentGroupIndex = 0;
                return new {Group = currentGroupIndex, Value = a};
            }).ToArray();
    
    
            var step2 = step1.GroupBy(a => a.Group).Select(a => a.ToArray()).ToArray();
    
            var group1 = step2[0].Select(a => a.Value).ToArray();
            var group2 = step2[1].Select(a => a.Value).ToArray();
            var group3 = step2[2].Select(a => a.Value).ToArray();
            var group4 = step2[3].Select(a => a.Value).ToArray();
    

    它的作用是首先引入一个计数器 (currentGroupIndex),它从零开始,并将随着列表中的每个元素递增。达到组大小时,索引将重置为零。
    变量 step1 现在包含包含 GroupValue 属性的项目。
    然后在GroupBy 语句中使用Group 值。

    【讨论】:

      【解决方案5】:

      Take & Skip 在这里我认为可能会很有帮助,但我个人喜欢使用Func 来做出这些选择,使方法更加灵活。

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Threading.Tasks;
      
      namespace Splitter
      {
          class Program
          {
              static void Main(string[] args)
              {
                  List<int> numbers = Enumerable.Range(1, 25).ToList();
                  int groupCount = 4;
      
                  var lists = numbers.Groupem(groupCount, (e, i) =>
                  {
                      // In what group do i wanna have this element.
                      int divider = numbers.Count / groupCount;
                      int overflow = numbers.Count % divider;
                      int index = (i - overflow) / divider;
                      return index < 0 ? 0 : index;
                  });
      
                  Console.WriteLine("numbers: {0}", numbers.ShowContent());
      
                  Console.WriteLine("Parts");
                  foreach (IEnumerable<int> list in lists)
                  {
                      Console.WriteLine("{0}", list.ShowContent());
                  }
              }
          }
      
          public static class EnumerableExtensions
          {
              private static List<T>[] CreateGroups<T>(int size)
              {
                  List<T>[] groups = new List<T>[size];
      
                  for (int i = 0; i < groups.Length; i++)
                  {
                      groups[i] = new List<T>();
                  }
      
                  return groups;
              }
      
              public static void Each<T>(this IEnumerable<T> source, Action<T, int> action)
              {
                  var i = 0;
                  foreach (var e in source) action(e, i++);
              }
      
              public static IEnumerable<IEnumerable<T>> Groupem<T>(this IEnumerable<T> source, int groupCount, Func<T, int, int> groupPicker, bool failOnOutOfBounds = true)
              {
                  if (groupCount <= 0) throw new ArgumentOutOfRangeException("groupCount", "groupCount must be a integer greater than zero.");
      
                  List<T>[] groups = CreateGroups<T>(groupCount);
      
                  source.Each((element, index) =>
                  {
                      int groupIndex = groupPicker(element, index);
      
                      if (groupIndex < 0 || groupIndex >= groups.Length)
                      {
                          // When allowing some elements to fall out, set failOnOutOfBounds to false
                          if (failOnOutOfBounds)
                          {
                              throw new Exception("Some better exception than this");
                          }
                      }
                      else
                      {
                          groups[groupIndex].Add(element);
                      }
                  });
      
                  return groups;
              }
      
              public static string ShowContent<T>(this IEnumerable<T> list)
              {
                  return "[" + string.Join(", ", list) + "]";
              }
          }
      }
      

      【讨论】:

        【解决方案6】:

        这个怎么样,包括参数检查,使用 emtpy 集。

        分两遍完成,应该很快,我没测试过。

        public static IList<Ilist<T>> Segment<T>(
                this IEnumerable<T> source,
                int segments)
        {
            if (segments < 1)
            {
                throw new ArgumentOutOfRangeException("segments");
            }
        
            var list = source.ToList();
            var result = new IList<T>[segments];
        
            // In case the source is empty.
            if (!list.Any())
            {
                for (var i = 0; i < segments; i++)
                {
                    result[i] = new T[0];
                }
        
                return result;
            }
        
            int remainder;
            var segmentSize = Math.DivRem(list.Count, segments, out remainder);
            var postion = 0;
            var segment = 0;
            while (segment < segments)
            {
                var count = segmentSize;
                if (remainder > 0)
                {
                    remainder--;
                    count++;
                }
        
                result[segment] = list.GetRange(position, count);
                segment++;
                position += count;
            }
        
            return result;
        }
        

        【讨论】:

          【解决方案7】:

          这里有一个优化的轻量级 O(N) 扩展方法解决方案

          public static void Bifurcate<T>(this IEnumerable<T> _list, int amountOfListsOutputted, IList<IList<T>> outLists)
          {
              var list = _list;
          
              var index = 0;
              outLists = new List<IList<T>>(amountOfListsOutputted);
          
              for (int i = 0; i < amountOfListsOutputted; i++)
              {
                  outLists.Add(new List<T>());
              }
          
              foreach (var item in list)
              {
                  outLists[index % amountOfListsOutputted].Add(item);
          
                  ++index;
              }
          }
          

          像这样简单地使用它:

          public static void Main()
          {
              var list = new List<int>(1000);
          
          
              //Split into 2
          
              list.Bifurcate(2, out var twoLists);
          
              var splitOne = twoLists[0];
              var splitTwo = twoLists[1];
          
              // splitOne.Count == 500
              // splitTwo.Count == 500
          
          
              //Split into 3
          
              list.Bifurcate(3, out var threeLists);
          
              var _splitOne = twoLists[0];
              var _splitTwo = twoLists[1];
              var _splitThree = twoLists[2];
          
              // _splitOne.Count == 334
              // _splitTwo.Count = 333
              // _splitThree.Count == 333
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2012-07-12
            • 2021-11-12
            • 2012-11-04
            • 2015-06-11
            • 1970-01-01
            • 2010-10-19
            • 2010-11-13
            相关资源
            最近更新 更多