在Java中将列表均匀分配到子列表中

4

我希望将一个列表均匀地分成指定数量的子列表。例如,我有一个包含1到10个元素的列表,我想要3个子列表。它们应该如下所示:

SL1 -> {1, 2, 3, 4}
SL2 -> {5, 6, 7}
SL3 -> {8, 9, 10}

重要提示: 每个列表包含的内容不重要,例如 SL1 可以包含 {1, 5, 7, 10}。最重要的是有两个大小为3的列表和一个大小为4的列表。

我尝试了几种方法,包括使用 Iterables.partition,但这并没有帮助。

我唯一想到可行的方法是:

public Iterable<List<Integer>> distributeEvenlyQueryListIntoLists(final LinkedList<Integer> bigList, final Integer numberOfSublists) {
    List<List<Integer>> result = new ArrayList<>();

    // Creates as many lists as needed
    for (int i = 0; i < numberOfSublists; i++) {
        result.add(new ArrayList<>());
    }

    while (bigList.iterator().hasNext()) {
        for (int i = 0; i < numberOfSublists; i++) {
            if (!bigList.iterator().hasNext()) {
                break;
            }
            result.get(i).add(bigList.poll());
        }
    }
    return result;
}

传递的 bigList 不必是一个 LinkedList,它可以是任何 Iterable

我特别讨厌第一个循环,因为我需要创建子列表。

谢谢!

3个回答

6
只需按照轮询模式进行分发:
public <T> List<List<T>> partition(Iterable<T> iterable, int partitions){
    List<List<T>> result = new ArrayList<>(partitions);
    for(int i = 0; i < partitions; i++)
        result.add(new ArrayList<>());

    Iterator<T> iterator = iterable.iterator()
    for(int i = 0; iterator.hasNext(); i++)
        result.get(i % partitions).add(iterator.next());

    return result;
}

使用此代码进行样本运行:

List<String> l = Stream.iterate(0, i->i + 1).limit(25).map(i->Integer.toString(i)).collect(Collectors.toList());
System.out.println(partition(l, 4).toString());

生成

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

基本思路是每次对结果集中的每个列表添加一个元素。这样可以保证两个列表之间元素数量的差异永远不会超过1。

作为另一种选择,您可以使用guava的Iterables.partition实现,该实现采用了稍微不同的方法。


太好了,我喜欢这个!有没有办法去掉第一个循环?它实际上是我原来解决方案中最困扰我的问题。 - user3083022
@user3083022并不是真的。你可以使用一些技巧来使其看起来不同,但最终你还是必须创建所有这些列表实例。 - user4668606
第一个循环用于创建所有子列表对象,这是有效不可避免的。你可以将它合并到后面的循环中,但这样会在后面的循环中增加一个条件语句(检查子列表是否存在;如果不存在则创建),这可能会增加执行时间,因为该检查将针对大列表中的每个元素进行一次。 - Paul Brinkley
@user3083022,可能的解决方案是Stream.of(ArrayList::new, i->ArrayList::new).limit(partitions).collect(Collectors.toList()),但这种方法有点hackish。我脑海中刚想到的另一种替代方法是将输入限制为类型List<T>并使用subList,这将创建原始列表的视图。 - user4668606
我觉得我没有理解你提到的另一种方法。你能详细说明一下吗? - user3083022
显示剩余3条评论

2
如果你不喜欢创建子列表,那就意味着你在寻找快速解决方案。如果你有原始的 List,并且计划不改变原来的 List,请考虑使用 List.subList()。
int subSize = bigList.length() / numSubs;
int numBigSubs = 0; // # of subs that need to be one bigger
if (bigList.length() % numSubs > 0) {
     subSize++;
     numBigSubs = bigList.length() % numSubs;
}
int from = 0;
int to  = subSize;
List<List<Integer>> subList = new ArrayList<List<Integer>>(numSubs);
for (int i = 0; i < numSubs; i++) {
    List<Integer> newSub = bigList.subList(from, to);
    subList.add (newSub);
    from = to;
    to += subSize;
    if (i >= numBigSubs && numBigSubs > 0) to--; 
}
注意:我是在没有测试的情况下写的 - 如果失败了,请原谅,希望有人能编辑它使其正常工作。

再次强调,这样做的巨大优势就是速度非常快 - 所有子列表都只是查看更大列表的视图。缺点是,如果您更改列表,则一切都无法保证。


这段代码并没有按预期执行。假设“input-list % numSubs = 3”。您的代码将生成“numSubs - 1”个具有“subSize”元素的列表和一个带有“subSize - 3”个元素的列表。 - user4668606
我刚刚在编辑代码以处理这个问题,所以它可能解决了你的担忧。现在,如果 big.length() == 15 && numSubs==4,那么 subSize 将被设置为 big.length() / numSubs + 1 == 4,然后你应该得到子列表 0-4、4-8、8-12、12-15,这应该是正确的。你觉得呢? - Paul Brinkley
(OP没有完全说明,但我猜测您想要循环调度,即所有子列表的长度应该相差不超过1,如果必要的话,前面的子列表应该更大。) - Paul Brinkley
我假设你实际在运行这段代码(我目前没有编译器),所以如果它确实做了你说的事情,我承认我束手无策。最后一个子列表应该包含一个 15 元素输入列表的第 12-14 个元素,这是完全正确的,对吗? - Paul Brinkley
1
@PaulBrinkley 运行良好,除了计算 numBigSubs 中出现了一些问题,但这不太难修复。 - user4668606
显示剩余9条评论

0

你可以使用org.apache.commons.collections4.ListUtils来创建相等大小的子列表。

List<String> bigList = ...
int batchSize = 1000;
List<List<String>> smallerLists = ListUtils.partition(bigList, batchSize);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接