5

Is there any function in haskell libraries that sorts integers in O(n) time?? [By, O(n) I mean faster than comparison sort and specific for integers]

Basically I find that the following code takes a lot of time with the sort (as compared to summing the list without sorting) :

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

Summing a list doesn't require deepseq but what I am trying for does, but the above code is good enough for the pointers I am seeking.

Time : 6 seconds (without sort); about 35 seconds (with sort)

Memory : about 80 MB (without sort); about 310 MB (with sort)

Note 1 : memory is a bigger issue than time for me here as for the task at hand I am getting out of memory errors (memory usage becomes 3GB! after 30 minutes of run-time)

I am assuming faster algorithms will provide bettor memory print too, hence looking for O(n) time.

Note 2 : I am looking for fast algorithms for Int64, though fast algorithms for other specific types will also be helpful.


Solution Used : IntroSort with unboxed vectors was good enough for my task:

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
Karan
  • 559
  • 5
  • 12
  • `O(n)` sorting? I guess you could try implementing [spaghetti sort](https://en.wikipedia.org/wiki/Spaghetti_sort). – huon Apr 01 '12 at 13:53
  • 3
    A comparison sort cannot have lesser complexity than `O(n * log n)`. Since the range is finite, you could use a bucket sort (but that wouldn't reduce the memory usage here ;). Have you tried building a `Data.IntSet` and `toList` on that? – Daniel Fischer Apr 01 '12 at 13:55
  • using Data.IntSet it takes about 24 seconds, so it does seem faster but the memory footprint is 320 MB !! [ `genlist gen = id $!! toList $!! ((fromList $!! take (2^22) ((randoms gen)::[Int])) :: IntSet)`] – Karan Apr 01 '12 at 14:05
  • @DanielFischer : Any ideas about code-hacks or algorithms that will help reduce the memory footprint or force garbage collection in some way ? – Karan Apr 01 '12 at 14:28
  • I tried `IntMap` and `Map` (sets are no good due to possible duplicates), that didn't help much with either space or time. The best I can offer is quicksort on a `STUArray`. **Much** faster, and the sorting needs only little memory. The final list, however, needs a lot of memory, no matter how you sort it. – Daniel Fischer Apr 01 '12 at 14:46
  • Judy arrays are much faster and with much lower memory requirements than IntMaps - see http://blog.malde.org/posts/frequency-counting.html . The downside is living in IO. For limited range/dense data structures, vectors are still faster. – Ketil Dec 03 '13 at 09:31

3 Answers3

9

I would consider using vectors instead of lists for this, as lists have a lot of overhead per-element while an unboxed vector is essentially just a contiguous block of bytes. The vector-algorithms package contains various sorting algorithms you can use for this, including radix sort, which I expect should do well in your case.

Here's a simple example, though it might be a good idea to keep the result in vector form if you plan on doing further processing on it.

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

Also, I suspect that a significant portion of the run time of your example is coming from the random number generator, as the standard one isn't exactly known for its performance. You should make sure that you're timing only the sorting part, and if you need a lot of random numbers in your program, there are faster generators available on Hackage.

hammar
  • 138,522
  • 17
  • 304
  • 385
  • except for the mutable arrays Daniel pointed to, this works best, thanx – Karan Apr 01 '12 at 15:21
  • @DanielFischer : Introsort ? didn't find it on hackage – Karan Apr 01 '12 at 15:22
  • 1
    @Karan Note that this also uses mutable structures under the hood. Introsort is in `Data.Vector.Algorithms.Intro`, also in the `vector-algorithms` package. – Daniel Fischer Apr 01 '12 at 15:25
  • @DanielFischer : wow, nice, worked in under 9 seconds, your previous post with lot of code scared me, but this seems like a nice wrapper – Karan Apr 01 '12 at 15:30
  • 1
    @DanielFischer and hammar : Any nice tutorial on the use of these vectors/mutable-arrays in haskell (not the usual wikibook/haskellwiki) ? – Karan Apr 01 '12 at 15:34
4

The idea to sort the numbers using an array is the right one for reducing the memory usage.

However, using the maximum and minimum of the list as bounds may cause exceeding memory usage or even a runtime failure when maximum xs - minimum xs > (maxBound :: Int).

So I suggest writing the list contents to an unboxed mutable array, sorting that inplace (e.g. with quicksort), and then building a list from that again.

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

is reasonably fast and uses less memory. It still uses a lot of memory for the list, 222 Ints take 32MB storage raw (with 64-bit Ints), with the list overhead of iirc five words per element, that adds up to ~200MB, but less than half of the original.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
  • amazing code, it ran in about 7.5 secs and i didn't see even 32 MB usage (was monitoring via `top`) – Karan Apr 01 '12 at 14:54
  • Thanks,@hammar. Was totally distracted and didn't notice. – Daniel Fischer Apr 01 '12 at 14:54
  • this will take me a bit to process, but still can we do this without using mutable things; i mean a sort function that does something and throws it aways as it doesn't need it later and uses memory O(n) only (for functional paradigm that is) ??? – Karan Apr 01 '12 at 14:56
  • @Karan Well, I see a bit more than 32MB while sorting via top and `+RTS -s`, but for the final list, a lot is needed, so `+RTS -s` reports 275M total used, with ~130MB max residency. – Daniel Fischer Apr 01 '12 at 14:57
  • @Karan I don' understand your second comment. Can you elaborate? – Daniel Fischer Apr 01 '12 at 14:59
  • This solution uses mutable data structures. I am asking if we can sort using non-mutable structures (more haskell style) in O(n) memory. Ie (sort xs) does xs -> xs' -> ..... -> finalxs where it throws away the old structure and maintains O(n) as maximum memory consumption. – Karan Apr 01 '12 at 15:06
  • one example (note time is worse than than n*log n) : sort xs swaps the first two elements it sees in the wrong order producing a new xs (i.e. swap does change existing structure but produces new one); it then sorts the new xs and throws away the old one – Karan Apr 01 '12 at 15:07
  • Sure one can do that, it's just vastly more efficient to use mutable structures for such tasks, so that's what is used. One can hide the use of the mutable structure and expose only the API, so the users of the library cannot do unsafe stuff with it. – Daniel Fischer Apr 01 '12 at 15:18
  • @Karan re: "throwing away old structure" I once made similar case in comments to [this post](http://stackoverflow.com/questions/9593988/in-haskell-how-would-you-go-about-generating-a-list-of-all-prime-numbers-upto-a/9605197#9605197). I too thought old structure should be thrown away and destructive update automatically used by implementation in such cases. I was told then it is not the case, in Haskell. I would much prefer this alternative to hand-coded Monads: being able to write seemingly functional code and be certain the implementation will use destructive update behind the scenes. – Will Ness Apr 09 '12 at 10:13
  • 1
    @WillNess : i was referring to garbage collection of old structures while new memory is allocated (so there is linear memory usage); but yes, it would super cool for compiler/GC to automatically deduce destructive updates (especially in obvious cases like '//' function on arrays); – Karan Apr 10 '12 at 15:27
  • 1
    @Karan btw beware Daniel's qsort code is only good for random input (which you do have here :) ). As an exercise, you can change it to (1) random pick the pivot, and (2) use three-way partition (i.e. keeping the equals separate from the lesser and the greater ones - and then you don't need to sort the equals). I don't know how to do the first one though. But it would probably made it slower, and here you don't need these changes, as your input *is* random. – Will Ness Apr 10 '12 at 15:45
  • @WillNess : i didn't use this quicksort; i used introsort for unboxed vector and it was good enough for the task at hand – Karan Apr 10 '12 at 16:01
  • @Karan perhaps you could write this up and add your conclusions into the question (what you ended up using, how did it solve your problem, etc.), for the benefit of future readers. Right now it's hidden in the comments here. :) – Will Ness Apr 10 '12 at 16:44
2

This is taken from Richard Bird's book, Pearls of Functional Algorithm Design, (though I had to edit it a little, as the code in the book didn't compile exactly as written).

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)

It works by creating an Array indexed by integers where the values are the number of times each integer occurs in the list. Then it creates a list of the indexes, repeating them the same number of times they occurred in the original list according to the counts.

You should note that it is linear with the maximum value in the list, not the length of the list, so a list like [ 2^x | x <- [0..n] ] would not be sorted linearly.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • probably because as you added later it's linear in terms of the max element in the list (and I am using Int64) – Karan Apr 01 '12 at 14:22
  • Yes. And, re-reading your original question, I'm not sure this has a particularly small memory footprint either :) – Peter Hall Apr 01 '12 at 14:28
  • that's why my system hanged :) – Karan Apr 01 '12 at 14:30
  • @Karan This is called "count sort" BTW. Depending on your situation you might prefer keeping the sorted result in RLE format (that's if your range is shorter so you'd have more duplicates). But here your range is too wide for `Array Int` (and you don't need to call `maximum`, you know your range in advance). – Will Ness Apr 10 '12 at 16:40
  • @WillNess : what's RLE ? (wikipedia says something about run-length in graphics) – Karan Apr 10 '12 at 17:35
  • @Karan run-length encoding: pairs of value and its multiplicity. Here, `assocs count` returns just that. You could also use *bucket sort*, with the narrowed range: `concat.map snd.assocs.accumArray (flip insert) [] (-65536,65536).map (\n->(quot n (2^15),n)) $ xs`. Here `65536 == quot (2^31) (2^15)`. You might play with the numbers to see what works best (if at all). – Will Ness Apr 10 '12 at 19:23