我的Fisher-Yates洗牌有什么问题吗?

时间:2013-04-26 18:26:01

标签: haskell shuffle

意识到当某些东西看起来好得令人难以置信时,通常情况下,我想我会提出这个问题,希望能够清除任何一个小鬼。我回顾了一些我能找到的相关主题,但我的问题仍然存在。

我对Haskell相对较新,在我的实验中,我编写了一个基本的Fisher-Yates shuffle,如下所示。

shuffle :: RandomGen g => [a] -> g -> ([a],g)
shuffle [] g0 = ([],g0)
shuffle [x] g0 = ([x],g0)
shuffle xs g0 = (x:newtail,g2)
  where (i,g1) = randomR (0, length $ tail xs) g0
        (xs1,x:xs2) = splitAt i xs
        (newtail,g2) = shuffle (xs1++xs2) g1

这个实现当然使用beaucoup内存用于大型列表,但速度很快 - 在我的笔记本电脑上平均5s用于30M ints而Std C ++ shuffle用于2.3s)。事实上,它比其他地方的其他Haskell实现要快得多。(例如,http://www.haskell.org/haskellwiki/Random_shuffle

鉴于我见过的其他Haskell shuffle既复杂又慢,我想知道加速/简单是否只是我作为一个无懈可击的记忆猪的奖励,或者如果我错过了一些微小但至关重要的细节使我的算法偏向。我没有进行过广泛的测试,但初看起来似乎表明了排列的均匀分布。

我希望通过更多Haskell和/或改组经验来评估更多的眼睛。非常感谢所有花时间回复的人。

1 个答案:

答案 0 :(得分:8)

让我们做一些适当的基准测试。以下是一些代码,将您的随机播放重命名为shuffle1,并将我个人喜欢的变体改为shuffle2

import System.Random

import Control.Monad

import Control.Monad.ST.Strict
import Data.STRef.Strict

import Data.Vector.Mutable

import Prelude as P

import Criterion.Main


shuffle1 :: RandomGen g => [a] -> g -> ([a], g)
shuffle1 [] g0 = ([],g0)
shuffle1 [x] g0 = ([x],g0)
shuffle1 xs g0 = (x:newtail,g2)
  where (i,g1) = randomR (0, P.length $ P.tail xs) g0
        (xs1,x:xs2) = P.splitAt i xs
        (newtail,g2) = shuffle1 (xs1++xs2) g1


shuffle2 :: RandomGen g => [a] -> g -> ([a], g)
shuffle2 xs g0 = runST $ do
    let l = P.length xs
    v <- new l
    sequence_ $ zipWith (unsafeWrite v) [0..] xs

    let loop g i | i <= 1 = return g
                 | otherwise = do
            let i' = i - 1
                (j, g') = randomR (0, i') g
            unsafeSwap v i' j
            loop g' i'

    gFinal <- loop g0 l
    shuffled <- mapM (unsafeRead v) [0 .. l - 1]
    return (shuffled, gFinal)


main = do
    let s1 x = fst $ shuffle1 x g0
        s2 x = fst $ shuffle2 x g0
        arr = [0..1000] :: [Int]
        g0 = mkStdGen 0
    -- make sure these values are evaluated before the benchmark starts
    print (g0, arr)

    defaultMain [bench "shuffle1" $ nf s1 arr, bench "shuffle2" $ nf s2 arr]

所以,让我们看看一些结果:

carl@ubuntu:~/hask$ ghc -O2 shuffle.hs
[1 of 1] Compiling Main             ( shuffle.hs, shuffle.o )
Linking shuffle ...
carl@ubuntu:~/hask$ ./shuffle 
(1 1,[0, .. <redacted for brevity>])
warming up
estimating clock resolution...
mean is 5.762060 us (160001 iterations)
found 4887 outliers among 159999 samples (3.1%)
  4751 (3.0%) high severe
estimating cost of a clock call...
mean is 42.13314 ns (43 iterations)

benchmarking shuffle1
mean: 10.95922 ms, lb 10.92317 ms, ub 10.99903 ms, ci 0.950
std dev: 193.8795 us, lb 168.6842 us, ub 244.6648 us, ci 0.950
found 1 outliers among 100 samples (1.0%)
variance introduced by outliers: 10.396%
variance is moderately inflated by outliers

benchmarking shuffle2
mean: 256.9394 us, lb 255.5414 us, ub 258.7409 us, ci 0.950
std dev: 8.042766 us, lb 6.460785 us, ub 12.28447 us, ci 0.950
found 1 outliers among 100 samples (1.0%)
  1 (1.0%) high severe
variance introduced by outliers: 26.750%
variance is moderately inflated by outliers

好吧,我的系统非常嘈杂,不应该用于对数字相似的东西进行严格的基准测试。但这在这里几乎不重要。在包含1001个元素的列表中,shuffle2shuffle1快约40倍。由于O(n)和O(n ^ 2)之间的差异,只有较大的列表才会增加。我确信无论你的测试代码是什么时间,都不是随机算法。

实际上,我有一个猜测。您的版本很懒,可以逐步返回结果。如果您在调用发电机后从未接触过发电机,那么5秒是获得前几个结果的合理时间段。也许这就是你的时间安排。