【问题标题】:sorting integers fast in haskell在haskell中快速排序整数
【发布时间】:2013-01-05 06:05:13
【问题描述】:

haskell 库中是否有任何函数可以在 O(n) 时间内对整数进行排序? [通过,O(n) 我的意思是比比较排序更快并且特定于整数]

基本上我发现以下代码在排序上花费了很多时间(与对列表求和而不排序相比):

import System.Random
import Control.DeepSeq
import Data.List (sort)

genlist gen = id $!! sort $!! take (2^22) ((randoms gen)::[Int])

main = do
    gen <- newStdGen
    putStrLn $ show $ sum $ genlist gen

对列表求和不需要 deepseq,但我正在尝试这样做,但上面的代码对于我正在寻找的指针来说已经足够了。

时间:6 秒(无排序);大约 35 秒(带排序)

内存:约 80 MB(无排序);大约 310 MB(带排序)

注意 1: 内存对我来说是一个比时间更大的问题,因为我手头的任务出现内存不足错误(运行 30 分钟后内存使用量变为 3GB!)

我假设更快的算法也会提供 bettor 内存打印,因此需要 O(n) 时间。

注意 2:我正在寻找 Int64 的快速算法,但其他特定类型的快速算法也会有所帮助。


使用的解决方案:带有未装箱向量的 IntroSort 足以满足我的任务:

import qualified Data.Vector.Unboxed as V
import qualified Data.Vector.Algorithms.Intro as I

sort :: [Int] -> [Int]
sort = V.toList . V.modify I.sort . V.fromList

【问题讨论】:

  • O(n) 排序?我想你可以尝试实现spaghetti sort
  • 比较排序的复杂性不能低于O(n * log n)。由于范围是有限的,您可以使用桶排序(但这不会减少这里的内存使用量;)。您是否尝试过在此基础上构建 Data.IntSettoList
  • 使用 Data.IntSet 大约需要 24 秒,所以看起来确实更快,但内存占用为 320 MB ! [genlist gen = id $!! toList $!! ((fromList $!! take (2^22) ((randoms gen)::[Int])) :: IntSet)]
  • @DanielFischer:关于代码黑客或算法的任何想法,将有助于减少内存占用或以某种方式强制垃圾收集?
  • 我尝试了IntMapMap(由于可能重复,集合并不好),这对空间或时间都没有多大帮助。我能提供的最好的方法是在STUArray 上进行快速排序。 快得多,排序只需要很少的内存。然而,最终列表需要大量内存,无论您如何排序。

标签: sorting haskell int


【解决方案1】:

我会考虑为此使用向量而不是列表,因为列表的每个元素都有很多开销,而未装箱的向量本质上只是一个连续的字节块。 vector-algorithms 包包含您可以为此使用的各种排序算法,包括 radix sort,我希望它在您的情况下应该做得很好。

这是一个简单的示例,但如果您打算对其进行进一步处理,最好将结果保持为矢量形式。

import qualified Data.Vector.Unboxed as V
import qualified Data.Vector.Algorithms.Radix as R

sort :: [Int] -> [Int]
sort = V.toList . V.modify R.sort . V.fromList

另外,我怀疑您的示例运行时间的很大一部分来自随机数生成器,因为标准生成器的性能并不完全为人所知。您应该确保只对排序部分进行计时,如果您的程序中需要大量随机数,Hackage 上有更快的生成器可用。

【讨论】:

  • 除了丹尼尔指出的可变数组之外,这效果最好,thanx
  • @DanielFischer:Introsort?没有在hackage上找到它
  • @Karan 请注意,这也在后台使用了可变结构。 Introsort 在Data.Vector.Algorithms.Intro 中,也在vector-algorithms 包中。
  • @DanielFischer : 哇,不错,不到 9 秒就完成了,你之前写的很多代码的帖子吓到我了,但这似乎是一个不错的包装器
  • @DanielFischer 和 hammar:关于在 haskell 中使用这些向量/可变数组的任何不错的教程(不是通常的 wikibook/haskellwiki)?
【解决方案2】:

使用数组对数字进行排序是减少内存使用的正确方法。

但是,使用列表的最大值和最小值作为界限可能会导致超出内存使用量,甚至在maximum xs - minimum xs &gt; (maxBound :: Int) 时出现运行时失败。

所以我建议将列表内容写入一个未装箱的可变数组,对该数组进行就地排序(例如使用快速排序),然后再次从中构建一个列表。

import System.Random
import Control.DeepSeq
import Data.Array.Base (unsafeRead, unsafeWrite)
import Data.Array.ST
import Control.Monad.ST

myqsort :: STUArray s Int Int -> Int -> Int -> ST s ()
myqsort a lo hi
   | lo < hi   = do
       let lscan p h i
               | i < h = do
                   v <- unsafeRead a i
                   if p < v then return i else lscan p h (i+1)
               | otherwise = return i
           rscan p l i
               | l < i = do
                   v <- unsafeRead a i
                   if v < p then return i else rscan p l (i-1)
               | otherwise = return i
           swap i j = do
               v <- unsafeRead a i
               unsafeRead a j >>= unsafeWrite a i
               unsafeWrite a j v
           sloop p l h
               | l < h = do
                   l1 <- lscan p h l
                   h1 <- rscan p l1 h
                   if (l1 < h1) then (swap l1 h1 >> sloop p l1 h1) else return l1
               | otherwise = return l
       piv <- unsafeRead a hi
       i <- sloop piv lo hi
       swap i hi
       myqsort a lo (i-1)
       myqsort a (i+1) hi
   | otherwise = return ()


genlist gen = runST $ do
    arr <- newListArray (0,2^22-1) $ take (2^22) (randoms gen)
    myqsort arr 0 (2^22-1)
    let collect acc 0 = do
            v <- unsafeRead arr 0
            return (v:acc)
        collect acc i = do
            v <- unsafeRead arr i
            collect (v:acc) (i-1)
    collect [] (2^22-1)

main = do
    gen <- newStdGen
    putStrLn $ show $ sum $ genlist gen

相当快并且使用更少的内存。它仍然为列表使用大量内存,222Ints 占用 32MB 原始存储空间(使用 64 位 Ints),列表开销为 iirc 每个元素五个字,加起来约为 200MB,但不到原来的一半。

【讨论】:

  • 惊人的代码,它运行了大约 7.5 秒,我什至没有看到 32 MB 的使用量(通过 top 监控)
  • 谢谢,@hammar。完全分心,没有注意到。
  • 这将花费我一些时间来处理,但我们仍然可以在不使用可变事物的情况下做到这一点;我的意思是一个排序函数,它做了一些事情并把它扔掉,因为它以后不需要它并且只使用内存 O(n) (对于函数范式)???
  • @WillNess :我指的是在分配新内存时对旧结构进行垃圾收集(因此存在线性内存使用);但是,是的,编译器/GC 自动推断破坏性更新会非常酷(尤其是在明显的情况下,例如数组上的“//”函数);
  • @Karan btw 当心 Daniel 的 qsort 代码仅适用于随机输入(您在此处确实有 :))。作为练习,您可以将其更改为 (1) 随机选择枢轴,以及 (2) 使用三向分区(即,将等号与较小的和较大的分开 - 然后您不需要对等于)。我不知道如何做第一个。但这可能会使它变慢,在这里您不需要这些更改,因为您的输入 随机的。
【解决方案3】:

这是从 Richard Bird 的书《Pearls of Functional Algorithm Design》中摘录的(尽管我不得不对其进行一些编辑,因为书中的代码并没有完全按照所写的那样编译)。

import Data.Array(Array,accumArray,assocs)  

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs count]
        where count :: Array Int Int 
              count = accumArray (+) 0 range (zip xs (repeat 1))
              range = (0, maximum xs)

它通过创建一个由整数索引的数组来工作,其中值是每个整数在列表中出现的次数。然后它创建一个索引列表,根据计数重复它们在原始列表中出现的相同次数。

你应该注意它与列表中的最大值是线性的,而不是列表的长度,所以像[ 2^x | x &lt;- [0..n] ]这样的列表不会被线性排序。

【讨论】:

  • 可能是因为当您稍后添加时,它就列表中的最大元素而言是线性的(我使用的是 Int64)
  • 是的。而且,重新阅读您的原始问题,我也不确定这是否具有特别小的内存占用:)
  • 这就是我的系统挂起的原因:)
  • @Karan 这被称为“计数排序”顺便说一句。根据您的情况,您可能更喜欢将排序结果保留为 RLE 格式(如果您的范围更短,那么您将有更多重复项)。但是这里你的范围对于Array Int来说太宽了(而且你不需要打电话给maximum,你提前知道你的范围)。
  • @WillNess:什么是 RLE? (维基百科在图形中说了一些关于游程的内容)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-27
  • 1970-01-01
  • 2013-11-14
  • 2021-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多