这个改组算法有什么问题,如果有的话,我怎么知道呢?

时间:2010-10-15 17:25:24

标签: algorithm functional-programming shuffle

就像背景一样,我知道Fisher-Yates完美的洗牌。它的O(n)复杂性和保证的一致性是一个很好的混乱,我不会使用它...在一个允许就地更新数组的环境中(所以在大多数情况下,如果不是全部, 命令性编程环境)。

可悲的是,函数式编程世界并没有让你访问可变状态。

然而,由于Fisher-Yates,我没有很多关于如何设计改组算法的文献。完全解决这个问题的几个地方之前做了这么简单的说法,实际上,“所以这里是Fisher-Yates,这是你需要知道的所有洗牌”。最后,我必须提出自己的解决方案。

我想出的解决方案是这样的,可以随机播放任何数据列表:

  • 如果列表为空,则返回空集。
  • 如果列表中有一个项目,则返回该项目。
  • 如果列表非空,则使用随机数生成器对列表进行分区,并将算法递归地应用于每个分区,然后汇总结果。

在Erlang代码中,它看起来像这样:

shuffle([])  -> [];
shuffle([L]) -> [L];
shuffle(L)   ->
  {Left, Right} = lists:partition(fun(_) -> 
                                    random:uniform() < 0.5 
                                  end, L),
  shuffle(Left) ++ shuffle(Right).

(如果这看起来像是一种疯狂的快速排序,那么,基本上就是这就是它。)

所以这就是我的问题:同样的情况使得找到非Fisher-Yates的改组算法变得困难,这使得查找分析混合算法的工具同样困难。在分析PRNG的均匀性,周期性等方面,我可以找到很多文献,但没有关于如何分析洗牌的大量信息。 (事实上​​,我在分析洗牌时发现的一些信息是完全错误的 - 很容易通过简单的技术欺骗。)

所以我的问题是这样的:我如何分析我的改组算法(假设random:uniform()调用那里是否有生成具有良好特征的适当随机数的任务)?我可以使用哪些数学工具来判断,在1 ... 100的整数列表中,是否有100,000次洗牌运行给了我合理的改组结果?我已经做了一些我自己的测试(例如,比较增量到shuffles中的减量),但我想知道更多。

如果对该shuffle算法本身有任何了解,也会受到赞赏。

4 个答案:

答案 0 :(得分:73)

答案 1 :(得分:21)

您的算法是基于排序的随机播放,如维基百科文章中所述。

一般来说,基于排序的shuffle的计算复杂度与底层排序算法相同(例如O( n log n )平均值,O( n ²)基于快速排序的混乱的最坏情况),虽然分布不是完全均匀,但它应该接近均匀,足以满足大多数实际目的。

Oleg Kiselyov提供以下文章/讨论:

更详细地介绍了基于排序的混洗的局限性,并且还提供了Fischer-Yates策略的两种改编:一个天真的O( n ²)和一个二元树 - 基于O( n log n )之一。

  

可悲的是,函数式编程世界并没有让你访问可变状态。

事实并非如此:虽然纯函数编程避免了副作用,但它支持使用一流效果访问可变状态,而不需要副作用。

在这种情况下,您可以使用Haskell的可变数组来实现本教程中描述的变异Fischer-Yates算法:

附录

你的shuffle排序的具体基础实际上是一个无限密钥radix sort:正如gasche指出的那样,每个分区对应一个数字分组。

这个的主要缺点与任何其他无限键排序shuffle相同:没有终止保证。虽然终止的可能性随着比较的进行而增加,但从来没有上限:最坏情况的复杂性是O(∞)。

答案 2 :(得分:3)

我之前做过类似的事情,特别是你可能对Clojure的向量感兴趣,这些向量是功能性和不可变的,但仍具有O(1)随机访问/更新特性。这两个要点有几个实现“从这个M大小的列表随机取N个元素”;如果你让N = M,它们中的至少一个会变成Fisher-Yates的功能实现。

https://gist.github.com/805546

https://gist.github.com/805747

答案 3 :(得分:1)

基于How to test randomness (case in point - Shuffling),我建议:

由相等数量的零和1组成的随机(中等大小)数组。重复并连接直到无聊。使用这些作为死硬测试的输入。如果你有一个很好的shuffle,那么你应该生成0和1的随机序列(需要注意的是,在中等大小的数组的边界处,零(或1)的累积过量为零,您希望测试检测到,但较大的媒体&#34;他们不太可能这样做。)

请注意,测试可以拒绝您的随机播放有三个原因:

  • shuffle算法不好,
  • 洗牌机使用的随机数生成器或在初始化期间是坏的,或
  • 测试实施不好。

如果任何测试拒绝,您必须解决这种情况。

diehard tests的各种改编(为了解析某些数字,我使用了source中的diehard page)。自适应的主要机制是使混洗算法充当均匀分布的随机比特源。

  • 生日间隔:在 n 零的数组中,插入日志 n 。洗牌。重复直到无聊。构造一个距离的分布,与指数分布进行比较。您应该使用不同的初始化策略执行此实验 - 前端的那些,末端的那些,中间的那些,随机散布的那些。 (后者最大的危险是初始化随机化不良(关于混洗随机化),导致混乱的拒绝。)这实际上可以用相同值的块来完成,但是它具有在分布中引入相关性的问题(一个和两个不能在一次洗牌中处于同一位置。)
  • 重叠排列:多次改变五个值。验证120个结果的可能性大致相同。 (卡方测试,119自由度 - 死硬测试(cdoperm5.c)使用99个自由度,但这(通常)是使用输入序列的重叠子序列引起的顺序相关的伪像。)
  • 矩阵的等级:从2 *(6 * 8)^ 2 = 4608位改变相等数量的0和1,选择6个非重叠的8位子串。将这些视为6乘8的二进制矩阵并计算其等级。重复100,000个矩阵。 (统一排名为0-4。排名则为6,5或0-4。)排名的预期分数为0.773118,0.217439,0.0044443。卡方比较具有两个自由度的观察到的分数。 31乘31和32乘32的测试是相似的。分别汇集0-28和0-29的等级。预期的分数是0.2887880952,0.5775761902,0.1283502644,0.0052854502。卡方检验有三个自由度。

依旧......

您可能还希望利用dieharder和/或ent进行类似的改编测试。