为什么Collections.shuffle()算法比我的实现更好

时间:2015-04-06 14:30:20

标签: java shuffle

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

以下是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);
}

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

  

[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]

有人可以解释一下原因吗?

2 个答案:

答案 0 :(得分:37)

Collections.Shuffle()执行 Fisher-Yates随机播放。它是一种更均匀分布的改组形式,与你的算法相反,它不会重新洗牌之前已经洗过的东西。

你的算法所做的(也称为天真的实现)是它将随机选择任何数组索引并将其随机播放,这意味着它很有可能选择相同的数组索引。先前已被洗牌的指数。

Fisher-Yates shuffle(也称为 Donald Knuth Shuffle )是一种无偏见的算法,它以同样可能的概率对阵列中的项进行混洗。它避免了它移动的机会。相同的对象两次。

我们自己的Jeff Atwood在编码恐怖片中发表了{p> Here's a good explanation of the Fisher Yates Shuffle,他讨论了随机随机播放与费雪耶茨洗牌的天真实现。

另见关于Java实现的这个问题。 It mentions what you asked.如果您愿意,也可以查看源代码,如上所述。我通过Googling Collections.shuffle()在前5个链接中找到了它。

为了将来讨论这个问题,与天真的实现相比,使用Fisher-Yates shuffle总是一个好主意,特别是在需要更高级别随机性的实现中(例如随机扑克牌)以避免引入赔率和不公平的比赛。如果基于我们天真实施的机会游戏,因为偏见导致你所观察到的,同样的排列似乎显现出来,那将是一件好事。比其他人更频繁。

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

答案 1 :(得分:3)

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

考虑以下方法:

  1. 有两个列表,A和B.最初,所有元素都已打开 列表A所以列表B是空的。
  2. 每一步:

    从列表A上当前的元素中以统一的概率选择。

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

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

  3. 当列表A为空时,返回列表B。
  4. 我发现这个算法易于理解和可视化。

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

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

    一般情况下,选择1/n个项目后仍然在列表A上的概率为k。如果项目仍在列表A上,则在下一步选择的概率为((n-1)/n)*((n-2)/(n-1))*...*((n-k)/(n-k+1)) = (n-k)/n,因此在列表A具有1/(n-k)项目的步骤中选择的无条件概率为{{1} }。

    Fisher-Yates就是这个算法,它有两个列表,它们的总长度总是(n-k),在一个数组中连接起来。在每一步中,它以统一的概率从列表A中选择一个元素,置换列表A以将该元素放在列表B附近,然后移动边界,使其从列表A元素变为最近添加的元素列表B。