【问题标题】:Randomizing list processing faster than Collections.shuffle()?随机列表处理比 Collections.shuffle() 更快?
【发布时间】:2015-10-27 06:26:26
【问题描述】:

我正在用 Java 开发一个基于代理的模型。我使用了一个分析器来减少任何低效率,以至于唯一阻碍它的是 Java 的 Collections.shuffle()

我的模型中的代理(它们是动物)需要以随机顺序进行处理,以便没有一个代理始终在其他代理之前处理。

我正在寻找:比 Java 的 Collections.shuffle() 更更快的随机播放方法,或者以随机顺序处理 ArrayList 中元素的替代方法这明显更快。如果您知道比 ArrayList 更快的数据结构,请务必回答。我考虑过 LinkedList 和 ArrayDeque,但它们并没有太大区别。

目前,我正在尝试洗牌的列表中有超过 1,000,000 个元素。随着时间的推移,这个数量会增加,并且洗牌变得越来越低效。

是否有更快的随机处理元素的替代数据结构或方法?

我只需要能够存储元素并以随机顺序处理它们。我不使用包含或任何比存储和迭代更复杂的东西。

这里有一些示例代码可以更好地解释我想要实现的目标:

更新:对于 ConcurrentModificationException,我很抱歉,我没有意识到我已经这样做了,我也不想让任何人感到困惑。在下面的代码中修复它。

ArrayList<Agent> list = new ArrayList<>();
void process()
{
    list.add(new Agent("Zebra"));
    Random r = new Random();
    for (int i = 0; i < 100000; i++)
    {
        ArrayList<Agent> newlist = new ArrayList<>();
        Collections.shuffle(list);//Something that will allow the order to be random (random quality does not matter to me), yet faster than a shuffle
        for (String str : list)
        {
            newlist.add(str);
            if(r.nextDouble() > 0.99)//1% chance of adding another agent to the list
            {
                newlist.add(new Agent("Lion"));
            }
        }
        list = newlist;
    }
}

另一个更新 我考虑过做 list.remove(rando.nextInt(list.size()) 但由于 ArrayLists 的 remove 是 O(n) 这样做会更糟糕,而不是 shuffle 这么大的列表大小。

【问题讨论】:

  • 如果你尝试一下,你会得到一个ConcurrentModificationException
  • 如果你想要统一的改组,我不希望任何更快的改组方法在物理上是可能的。
  • Fisher-Yates shuffle 对 n 大小的数组执行 n 交换操作。我非常怀疑你能在不到那个时间里做一个统一的洗牌。
  • 我不明白为什么每次迭代都需要对整个 List 进行洗牌,因为无论如何 90% 的 Strings 都会被忽略。难道你不能每次只选择一些随机索引,并且在整个方法结束时只洗牌一次吗?
  • 一些 cmets/answers似乎 被您发布的代码误导了 - 我假设它是 pseudocode,只是为了说明主意。如果您提供了有关真实应用案例的更多信息(或代码),可能会提供更好的建议。

标签: java performance random collections shuffle


【解决方案1】:

我会使用一个简单的 ArrayList,而 根本不会随机播放它。而是选择随机列表索引进行处理。为了避免两次处理列表元素,我会从列表中删除处理过的元素。

现在,如果列表非常大,删除随机条目本身将成为瓶颈。但是,可以通过删除 last 条目并将其移动到所选条目之前占用的位置来轻松避免这种情况:

public String pullRandomElement(List<String> list, Random random) {
    // select a random list index
    int size = list.size();
    int index = random.nextInt(size);
    String result = list.get(index);
    // move last entry to selected index
    list.set(index, list.remove(size - 1));
    return result;
}

不用说你应该选择一个列表实现,其中 get(index) 和 remove(lastIndex) 是快速 O(1),例如 ArrayList。您可能还想添加边缘情况处理(例如列表为空)。

【讨论】:

  • @Durandal ArrayList 删除 O(1) 是否用于从列表末尾删除?我认为它总是 O(n),就像 Big O 备忘单所说的那样……我明天早上会测试一下,它看起来很有希望! (这里真的很晚了)
  • @Marco13 它最终比 Collections.shuffle() 慢
  • @Marco13 我知道你来自哪里并听取了你的建议;将示例更改为将 Random 实例作为参数。虽然我认为一方面没有必要(示例是示例并期望应用常识),但另一方面 - 提供样式良好的示例同样有效,特别是考虑到 SO 的观众中符合常识的内容差异很大。
  • 所以毕竟:一种合理的方法,但可能不是实际问题的“解决方案”(在问题中并没有真正明确地解决 - 但也许 cmets 中的讨论将有助于弄清楚出来...)
  • @Marco13 再次为我造成的混乱感到抱歉。
【解决方案2】:

你可以这样使用:如果你已经有了项目列表,根据它的大小生成一个随机数并获取 nextInt。

ArrayList<String> list = new ArrayList<>();    
int sizeOfCollection = list.size();

Random randomGenerator = new Random();
int randomId = randomGenerator.nextInt(sizeOfCollection);
Object x = list.get(randomId);
list.remove(randomId);

【讨论】:

  • 这将完成什么? (请注意,棘手的部分不是从未打乱的列表中选择随机元素,而是以随机顺序准确处理每个元素一次
  • 啊,好吧,然后删除它...所以你随机选择你的元素并将它从你的列表中丢弃。如果您需要保留原始列表,您可以随时复制列表
  • @PatB Marco 是对的。我需要以随机顺序处理每个元素一次。 ArrayList 的删除是 O(n),在这么大的集合上最终比 Collections.shuffle() 慢。
【解决方案3】:

由于您的代码实际上并不依赖于列表的顺序,因此在处理结束时对其进行一次随机播放就足够了。

void process() {
    Random r = new Random();
    for (int i = 0; i < 100000; i++) {
        for (String str : list) {
             if(r.nextDouble() > 0.9) {
                list.add(str + str);
            }
        }
    }
    Collections.shuffle(list);
}

虽然这仍然会抛出ConcurrentModificationException,就像原始代码一样。

【讨论】:

  • 我上面显示的代码只是示例代码。我实现的实际模型要求始终以足够随机的顺序处理元素,以便模型没有偏差。
  • @Robotia 这个模型没有偏见,因为你随机决定处理每个元素,这不依赖于元素的位置。
  • 必须处理每个元素。
  • @Robotia 是的,但是是否被选中并不取决于它在列表中的位置,只取决于随机调用的结果。
【解决方案4】:

Collections.shuffle() 使用 Fisher-Yates 算法的现代变体: 来自https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

To shuffle an array a of n elements (indices 0..n-1):
  for i from n − 1 downto 1 do
       j ← random integer such that 0 ≤ j ≤ i
       exchange a[j] and a[i]

Collections.shuffle 将列表转换为数组,然后进行随机播放,只需使用 random.nextInt() 然后将所有内容复制回来。 (见http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/Collections.java#Collections.shuffle%28java.util.List%29

您可以通过避免复制数组和回写的开销来加快速度: 要么编写您自己的 ArrayList 实现,您可以在其中直接访问支持数组,或者通过反射访问 ArrayList 的字段“elementData”。

现在在该数组上使用与 Collections.shuffle 相同的算法,使用正确的 size()。 这加快了速度,因为它避免了整个数组的复制,比如 Collection.shuffle() :

通过反射的访问需要一点时间,所以这个解决方案只对更多的元素更快。

我不会推荐这个解决方案,除非你想赢得比赛,通过禁食洗牌,也通过执行时间。

与往常一样,在比较速度时,请确保在开始测量之前运行要测量的算法 1000 次来预热 VM。

【讨论】:

  • 我分别在 Integer[] 和 ArrayList 上尝试了 Fisher-Yates shuffle 与 Collections.shuffle(),有趣的是发现 Collections 更快。
  • 你也可以试试将 Collections.shuffle 的 src 代码应用到一个 int[] 上,不要忘记在测量前及时预热虚拟机,否则第二种算法可能会更快。 shuffle的src:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
【解决方案5】:

根据文档,Collections.shuffle()O(N) 时间内运行。

此方法以线性时间运行。如果指定列表没有实现 RandomAccess 接口并且很大,则此实现在打乱之前将指定列表转储到数组中,并将打乱后的数组转储回列表中。这避免了由于将“顺序访问”列表改组而导致的二次行为。

我建议您使用 public static void shuffle(List&lt;?&gt; list, Random rnd) 重载,尽管性能优势可能微不足道。

除非您允许一些偏差,例如部分改组(每次仅重新改组列表的一部分)或改组不足,否则提高性能将很困难。洗牌不足意味着编写自己的 Fisher-Yates 例程并在反向遍历期间跳过某些列表索引;例如,您可以跳过所有奇数索引。然而,您列表的末尾会比前面的更小,这是另一种形式的偏见。

如果您有一个固定的列表大小M,您可以考虑在应用程序启动时在内存中缓存一些不同的固定索引排列(0M-1 随机排列)的大量N。然后,您可以在迭代集合时随机选择这些预排序之一,并根据先前定义的特定排列进行迭代。如果N 很大(比如 1000 或更多),则整体偏差会很小(而且相对均匀)并且会非常快。但是您注意到您的列表增长缓慢,因此这种方法不可行。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-17
    • 1970-01-01
    • 1970-01-01
    • 2014-05-22
    • 2013-10-21
    • 1970-01-01
    相关资源
    最近更新 更多