Collections.shuffle() 真的足够随机吗?实际例子似乎否定了这种说法

Posted

技术标签:

【中文标题】Collections.shuffle() 真的足够随机吗?实际例子似乎否定了这种说法【英文标题】:Is Collections.shuffle() really random enough? Practical examples seem to deny this statement 【发布时间】:2012-03-30 21:26:07 【问题描述】:

我在java.util.List 中有 1000 个唯一对象,每个对象都引用一个图像,1000 个列表中的每个图像都是唯一的,现在我想将它们打乱,这样我就可以使用前 20 个对象并呈现他们给网站用户。 然后,用户可以单击“随机播放”按钮,然后我再次从头开始检索 1000 张图像并再次调用shuffle()。 然而,似乎在 1000 个图像对象中,我经常在 20 个图像选择之间一次又一次地看到相同的图像。

好像出了点问题,有什么更好的建议吗?

我的代码很简单:

List<String> imagePaths = get1000Images();
Collections.shuffle(imagePaths);

int i = 0;
for (String path: imagePaths) 
  ... do something with the path ...
  i++;
  if (i >= 20) break;

我知道Collections.shuffle() 分布良好: 例如见http://blog.ryanrampersad.com/2012/03/03/more-on-shuffling-an-array-correctly/

但是,我只是觉得在 1000 张图片中的 20 张图片中一遍又一遍地看到同一张图片的概率应该要小得多......

非常感谢您的意见。

【问题讨论】:

如果没有看到对所见内容的统计分析,就很难知道它是否异常。 我的猜测是您实际上已经多次获得相同的图像路径,或者实际上具有相同图像数据的多个图像路径。除此之外,这点信息很难说...... 看看我的回答here,可能会有所帮助。也许你应该插入不同的Random 实现? relevant dilbert @basZero:实际上,您应该为每个shuffle 创建一个Random 的新实例,除非您有强大的随机seed 来源。否则,重复使用同一个。也许SecureRandom 有好种子? 【参考方案1】:

看到不存在的模式是人类的天性。许多人将行星和恒星的模式视为生活的指导。

在 PI 的前 1000 位数字中有六个连续的 9。这是否意味着 PI 的数字不是随机的?不。该模式不会再次出现,超出您的预期。

话虽如此,Random 并不是完全随机的,它会在 2^48 次调用后重复。 (它使用 48 位种子)这意味着不可能使用它生成所有可能的 longdouble。如果您想要更多随机性,您可以使用 SecureRandom 和 shuffle。

听起来你想要的是这样的

List<String> imagePaths = new ArrayList<>();

// called repeatedly
if (imagePaths.size() <= 500) 
    imagePaths = get1000Images();
    Collections.shuffle(imagePaths);


for (String path: imagePaths.subList(0, 20)) 
  ... do something with the path ...


imagePaths = imagePaths.subList(20, imagePaths.size());

这将确保您在最近 500 次调用中看不到相同的图像。

【讨论】:

我也想过这个问题,实际上是今天早上 :) 感谢您抽出宝贵时间提供代码示例... 要使用 SecureRandom,您可以:Collections.shuffle(imagePaths, new SecureRandom());【参考方案2】:

如果您在 1000 张图片中显示 20 张图片,则在下一次迭代中看到这 20 张图片中的任何一张重复的概率大约为 0.34,因此看到图片重复出现您应该不会感到惊讶。

看到特定图像的机会仍然是千分之一,但如果您正在寻找二十张图像,则机会要高得多。

我们可以计算前 20 张图片中没有一张重复的概率为:

 980   979         961
———— × ——— × ... × ——— ≈ 0.66
1000   999         981

所以看到重复的概率是 1 减去这个,或大约 0.34。

并且在接下来的两次迭代中看到重复的图像的概率是:

1 - (0.66 × 0.66) ≈ 0.56

换句话说,您很有可能会在接下来的两个周期中看到重复的图像。 (这不包括在第三个周期中从第二个周期重复的图像,这只会使其更有可能。)

对于它的价值,这里有一些 Java 代码来进行上述计算:

float result = 1.0f;
int totalImages = 1000;
int displayedImages = 20;

for (int i = 0; i < displayedImages; i++) 
  result = result * (totalImages - displayedImages - i) / (totalImages - i);


System.out.println(result);

【讨论】:

@ChristofferHammarström - 已修复。 或者应该是1 - 0.67 = 0.33 上面的代码返回0.6649897,我将其四舍五入为0.66。我不确定确切的值是否重要,关键是您可以期望看到前 20 张图像中的一张大约每 3 次重复一张。【参考方案3】:

您的直觉对于特定图像是正确的 [您不太可能一遍又一遍地看到 特定图像],但对于一般图像 [您可能会看到 一些图片重复]。这是这些地方之一,很可能我们的自动直觉是错误的......

这让我想起了the birthday paradox,这与直觉相矛盾,并说 - 对于一组 23 人,其中 2 人生日相同的可能性是 0.5,远远超出直觉的预期!

【讨论】:

【参考方案4】:

我做了 52 张牌洗牌四次,并标记了每次迭代在完全相同的插槽中重复完全相同的牌,这给了我 208 张牌中的大约 14 张牌,大约 93.3% 是随机的。

【讨论】:

【参考方案5】:

根据您的问题,我编写了以下程序。我创建了连续整数列表并将其洗牌 10、100、1000 和 10000 次。在每一系列洗牌之后,我检查了数组第 5 位元素的值并创建了计数器数组:每个数字出现在第 5 位的次数。

这是程序:

public class MyTest 
    public static void main(String[] args) 
        int n = 10;
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0;  i < n;  i++) 
            list.add(i);
        

        int[] counters = new int[n];

        for(int shuffles : new int[] 10, 100, 1000, 10000) 
            Arrays.fill(counters, 0);
            for (int i = 0;  i < shuffles; i++) 
                Collections.shuffle(list);
                // check 5-th element
                int fifth = list.get(5);
                counters[fifth] = counters[fifth] + 1;
            
            System.out.println(shuffles + ": " + Arrays.toString(counters));
        
    

结果如下:

10: [0, 1, 1, 1, 2, 0, 0, 3, 2, 0] 100:[11、9、9、7、10、12、13、13、8、8] 1000:[100、101、107、101、95、96、109、83、93、115] 10000:[1015、942、990、1003、1015、1037、977、1060、950、1011]

如您所见,“随机性”取决于随机播放的次数。如果将数组洗牌 10 次,则最小计数器为 0,最大值为 3。 100 次随机播放的这些值之间的差异(以百分比计)要小得多。 10000 次随机播放的数字几乎相同。

我认为这个测试模拟了您的用例:您在打乱集合的特定位置显示图像。

请参阅@amit 的帖子,其中描述了 shuffle 的含义。

因此,您的解决方案是将您的阵列洗牌 10 次。

编辑:@Dave Webb 对此案给出了完美的解释。

第二个想法如下:你实际上不必shuffle你的1000个元素列表来从中取出20个第一个元素。取20个随机元素就足够了。您将获得相同的效果但更有效的解决方案:

Set<Image> show = new HashSet<Image>();
Random r = new Random(System.currentTimeMillis());
for (int i = 0;  show.size() < 20;  i++) 
    show.add(list.get(r.nextInt()));

【讨论】:

太好了,我喜欢你的建议,即选择 20 个随机条目而不是 10 次随机排列...只是一个旁注:选择 20 个随机元素也可能最终选择相同的两次。所以这应该稍微改变一下,但是你的代码示例是一个好的开始! @basZero,我的代码示例考虑了两次相同的元素:我使用 Set 来存储结果并迭代直到集合大小为 20。 真的,对不起,我以为你会用List @basZero 还有一个非常简单(而且效率更高)的解决方案可以避免这个问题; see here。是的,没错,如果我们要正确地做到这一点,我们只需重新实现已经在使用的 shuffle 算法。为了证明随机性,你永远相信你的直觉 - 这基本上总是错误的。对此有统计检验(卡方,Kolmogorov-Smirnov,..)。如果你想要一个统一的分布,也永远不要这样做nextInt() % size,这显然只在极少数情况下有效。 @basZero 从统计角度来看,多次调用 shuffle 没有任何意义(如果您不相信,我总是喜欢进行一些卡方检验,请注意“看起来并不随机”是无趣的)。但重点是:shuffle 算法所做的基本上是从列表中取出随机元素。因此,编辑后的解决方案基本上是一种效率不高的洗牌算法,其中存在一些错误。【参考方案6】:

使用该代码,如果您一遍又一遍地看到同一张图片,这意味着同一张图片在列表中多次出现。无论您从哪里获取 1000 张图片,都有重复的地方。

【讨论】:

我可以保证列表中的所有图像都是不同的。它们直接来自 lucene 索引,其中路径是 lucene 索引中的“主键” 如果您的代码确实是您所拥有的方式,您只是在列表上迭代并且在初始洗牌后不修改列表,那么唯一的方法您可以在您选择的 20 张图像中获得重复项,即列表中是否有重复项。 Collections.shuffle() 不插入副本,它只是对现有项目重新排序。 在随后的几次洗牌中,他一遍又一遍地在选定的 20 人中看到相同的图像。

以上是关于Collections.shuffle() 真的足够随机吗?实际例子似乎否定了这种说法的主要内容,如果未能解决你的问题,请参考以下文章

Collections.shuffle源码阅读

Collections.shuffle()源码分析

升序 Collections.sort(list) 降序 Collections.reserve(list) 随机 Collections.shuffle(list)

你还在遍历搜索集合?别逗了,Java 8 一行代码足矣,是真的优雅

map/reduce之间的shuffle,partition,combiner过程的详解

如何真正洗牌