就像背景一样,我知道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算法本身有任何了解,也会受到赞赏。
答案 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的功能实现。
答案 3 :(得分:1)
基于How to test randomness (case in point - Shuffling),我建议:
由相等数量的零和1组成的随机(中等大小)数组。重复并连接直到无聊。使用这些作为死硬测试的输入。如果你有一个很好的shuffle,那么你应该生成0和1的随机序列(需要注意的是,在中等大小的数组的边界处,零(或1)的累积过量为零,您希望测试检测到,但较大的媒体&#34;他们不太可能这样做。)
请注意,测试可以拒绝您的随机播放有三个原因:
如果任何测试拒绝,您必须解决这种情况。
diehard tests的各种改编(为了解析某些数字,我使用了source中的diehard page)。自适应的主要机制是使混洗算法充当均匀分布的随机比特源。
依旧......