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]
有人可以解释一下原因吗?
答案 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的另一种解释。
考虑以下方法:
每一步:
从列表A上当前的元素中以统一的概率选择。
置换列表A,使所选元素成为最后一个元素。
从列表A中删除最后一个元素,并将其附加到列表B。
我发现这个算法易于理解和可视化。
第一步选择给定项目的概率为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。