TL&DR 要获得比 Data.List.permutations 更快的代码,请跳至第二部分
第一部分
我对 Haskell 比较陌生,但 I had developed a very efficient permutations algorithm for JS。它几乎击败了堆算法,但在 JS 中,与列表上的惰性 Haskell iterate 函数相比,旋转数组的成本更高。因此,与上面提供的所有答案不同,这个似乎更有效率。
截至今天,内置的Data.List.permutations 仍然比这个快 2 倍,因为我根本不知道 Haskell 的性能限制。可能有人可以帮助我将这段代码向前推进一点。
所以我有一个辅助函数,它返回所提供列表的所有旋转的列表。比如
rotations [1,2,3] 将产生 [[1,2,3],[2,3,1],[3,1,2]]
因此 perms 函数是;
rotations :: [a] -> [[a]]
rotations xs = take (length xs) (iterate (\(y:ys) -> ys ++ [y]) xs)
perms :: [a] -> [[a]]
perms [] = [[]]
perms (x:xs) = concatMap (rotations.(x:)) (perms xs)
第二部分
所以我一直在思考如何让上面的代码更有效率。好的,Haskell 中的列表是链表,与 JavaScript 不同,长度不是您可以在 O(1) 时间内访问的属性,而是 O(n)。这是一个遍历整个该死列表的函数,基本上是计算列表中的所有项目。因此,如果重复使用非常昂贵。这恰好是我们在每次调用 rotate 函数时通过 take (length xs) 指令所做的。如果您的输入列表长度为 10-11 项或更多,我们实际上会调用它数百万次。削减它会产生巨大的节省。然后让我们不要让它计算相同长度列表的长度,而是让我们简单地提供它;
rotations :: Int -> [a] -> [[a]]
rotations len xs = take len (iterate (\(y:ys) -> ys ++ [y]) xs)
美丽。好吧,现在我们必须相应地稍微修改我们的perms 函数:
perms :: [a] -> [[a]]
perms [] = [[]]
perms il@(x:xs) = concatMap ((rotations len).(x:)) (perms xs)
where len = length il
很明显il 现在分配给i输入list 和len 缓存它的长度。现在这很漂亮,也很有趣,与默认的 Data.List.permutations 相比,它在 GHCI 中的运行速度像 1.33 倍,在使用 -O2 编译时快 3+ 倍。
import Data.List
perms :: [a] -> [[a]]
perms xs = run len xs
where
len = length xs
rotate :: [a] -> [a]
rotate (x:xs) = xs ++ [x]
rotations :: Int -> [a] -> [[a]]
rotations l xs = take l (iterate rotate xs)
run :: Int -> [a] -> [[a]]
run _ [] = [[]]
run _ [x] = [[x]]
run n (x:xs) = run (n-1) xs >>= rotations n . (x:)
--run n (x:xs) = concatMap ((rotations n).(x:)) (run (n-1) xs)
λ> length $ perms [1..13]
6227020800
(302.58 secs, 1,366,730,140,472 bytes)
λ> length $ permutations [1..13]
6227020800
(404.38 secs, 1,800,750,142,384 bytes)
问题是,如果您可以使 rotations 函数更高效,您可以获得更好的结果,虽然我已经进行了一些研究,但这个简单的代码似乎与 Haskell 中的代码一样好。
另一个重要的一点是,我相信这个算法也是可线程的(还没有测试过)但它应该是因为如果你检查run n (x:xs) = concatMap ((rotations n).(x:)) (run (n-1) xs) 部分,你可能会注意到我们有一个map 和rotations n . (x:)作用于前一组排列。我认为这正是我可以产生线程的地方。
进一步的想法……“我真的做对了……吗?”
我想我被这里的懒惰欺骗了。我相信像length $ perms [1..12] 这样的操作并没有真正强制解决排列,而是一直工作直到它知道排列列表的长度为12!。我的意思是包含的值可能仍然是 thunk。
所以我决定不使用length,而是使用any (== [11,1,7,2,10,3,8,4,12,5,9,6]) $ perms [1..12],其中[11,1,7,2,10,3,8,4,12,5,9,6] 是perms 算法的最后一个排列元素。所以现在我想它应该评估所有 thunk 以进行权益检查,直到它到达最后一个元素以返回 True。
当像这样检查perms 和permutations 以及它们自己的最后一个元素时,以相似的速度解决(permutations稍微快一些)。
欢迎提出任何想法...