【问题标题】:Why does the Collections.shuffle() algorithm work better than my implementation [duplicate]为什么 Collections.shuffle() 算法比我的实现更好[重复]
【发布时间】:2015-04-06 14:30:19
【问题描述】:

Collections.shuffle() 向后遍历Collection 的每个索引,然后将其与随机索引交换它包括或之前。我想知道为什么,所以我尝试做同样的事情,但与Collection 中的 any 随机索引进行交换。

这是 Collections.shuffle() 代码的洗牌部分:

for (int i=size; i>1; i--)
    swap(arr, i-1, rnd.nextInt(i));

这是我的算法:

Random r = new Random();
for (int i = 0; i < a.size(); i++) {
    int index = r.nextInt(a.size());
    int temp = a.get(i);
    a.set(i, a.get(index));
    a.set(index, temp);
}

当我在同一个ArrayList 上运行一百万次时,我发现Collections.shuffle() 的分布比我的代码更均匀。另外,在运行我的代码时:

[0, 1, 2, 3, 4]

似乎以下排列始终最常出现:

[1, 0, 3, 4, 2]
[1、2、3、4、0]
[1、2、0、4、3]
[0、2、3、4、1]
[1, 2, 3, 0, 4]

谁能解释一下原因?

【问题讨论】:

标签: java shuffle


【解决方案1】:

Collections.Shuffle() 进行 Fisher-Yates 洗牌。这是一种更均匀分布的洗牌形式,与您的算法相反,它不会重新洗牌之前可能已经洗牌的内容。

您的算法所做的(也称为 naive 实现)是它会随机选择任何数组索引并将其打乱,这意味着它很可能会选择相同的索引之前已经洗牌了。

Fisher-Yates 洗牌(也称为 Donald Knuth 洗牌)是一种无偏算法,它以相同的可能性对数组中的项目进行洗牌。它避免了两次“移动”相同对象的机会。

Here's a good explanation of the Fisher Yates Shuffle 由我们自己在 Coding Horror 的 Jeff Atwood 撰写,他讨论了随机 shuffle 与 Fisher Yates shuffle 的幼稚实现。

另请参阅有关 Java 实现的 SO question。 It mentions what you asked. 如果您愿意,也可以查看源代码,如那里所述。我在前 5 个链接中通过谷歌搜索 Collections.shuffle() 找到了它。

为了进一步讨论这一点,与幼稚的实现相比,使用 Fisher-Yates 洗牌总是一个好主意,尤其是在需要更高级别随机性的实现中(例如洗牌扑克牌)以避免引入赔率和不公平的游戏.如果基于我们幼稚的实现来实现机会游戏,那将不是一件好事,因为偏见会导致您观察到的情况,在这种情况下,相同的排列似乎比其他人。

最后,正如用户@jmruc 所提到的,这是一个关于可视化算法的非常好的教程,它包含 Fisher-Yates shuffle 以及其他算法,所有这些都精美呈现。如果您更像是一个可视化者,可能会帮助您理解这些概念:Visualizing Algorithms by Mike Bostock

【讨论】:

  • 这里是算法的visualization,并解释了为什么它比“随机”洗牌更好。
  • @jmruc 只是想寻找我很久以前偶然发现的那个网站。我可以将其添加到我的答案中,还是应该将其作为评论保留?
【解决方案2】:

这是Fisher-Yates的另一种解释。

考虑以下方法:

  1. 有两个列表,A 和 B。最初,所有元素都在 列表 A 所以列表 B 为空。
  2. 在每一步:

    从当前列表 A 中的元素中以均匀概率选择。

    置换列表 A 以使所选元素成为最后一个元素。

    从列表 A 中删除最后一个元素并将其附加到列表 B。

  3. 当列表 A 为空时,返回列表 B。

我发现此算法易于理解和可视化。

在第一步中选择给定项目的概率是1/n。给定项目在第二步被选中的概率是它在第一步未被选中的概率(n-1)/n,乘以它在第二步被选中的概率,因为它仍在列表 A 上,1/(n-1)。该产品是1/n

同样,在两个项目被移动后,它有((n-1)/n)*((n-2)/(n-1)) = (n-2)/n 仍然在列表 A 上的概率,因此有1/n 成为第三个被选中项目的概率。

一般来说,在k 项目被选中后仍然在列表 A 上的概率是((n-1)/n)*((n-2)/(n-1))*...*((n-k)/(n-k+1)) = (n-k)/n。鉴于该项目仍在列表 A 中,在下一步中被选中的概率为 1/(n-k),因此当列表 A 有 (n-k) 项目时,在该步骤中被选中的无条件概率为 ((n-k)/n)*(1/(n-k)) = 1/n

Fisher-Yates 就是这种算法,它有两个列表,其总长度始终为n,连接在一个数组中。在每一步,它以均匀概率从列表 A 中选择一个元素,置换列表 A 以使该元素与列表 B 相邻,然后移动边界,使其从列表 A 元素变为列表中最近添加的元素清单 B。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-29
    • 2021-11-06
    • 2013-02-18
    • 1970-01-01
    • 2011-04-28
    • 2020-04-27
    • 1970-01-01
    • 2012-09-27
    相关资源
    最近更新 更多