Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

haskell length runtime O(1) or O(n)

Tags:

haskell

I was working on a Haskell assignment and I was trying think of ways to make my code faster. For example, my factors function below finds the number of divisors of some integer.

factors :: Int -> Int
factors x = length [n | n <- [1..x], mod x n == 0]

However, it occurred to me that I could make my code faster by avoiding usage of "length".

factors :: Int -> Int
factors x = go 1
  where
    go :: Int -> Int
    go i
      | i == x        = 1
      | mod x i == 0  = 1 + go (i + 1)
      | otherwise     = go (i + 1)

I was wondering if Haskell's length function is O(n) like strlen() in C or O(1) like String.length() in Java.

Also, is there a better or more efficient of writing my code?

like image 554
King Kira Avatar asked Mar 07 '23 20:03

King Kira


2 Answers

In my estimation, contrary to the accepted answer, you can in fact infer the complexity of length (and many other functions) just by looking at the definition of [a]:

Prelude> :info []
data [] a = [] | a : [a]    -- Defined in ‘GHC.Types’

Lists are inductively-defined; you can see from that definition (which is almost just regular haskell) that at the top level a list is either the constructor [] or :. Clearly length must recurse n times on this structure and so would have to be O(n).

It's very important to be able to reason at least intuitively in this way, in particular about lists which are ubiquitous. e.g. quick what's the complexity of (!!)?

If you want to do a deep dive into formally reasoning about time complexity in the presence of laziness then you'll need to pick up "Purely Functional Data Structures" by Okasaki.

like image 94
jberryman Avatar answered Mar 20 '23 03:03

jberryman


Also, is there a better or more efficient of writing my code?

Integer factorization is one of the most famous problems. There surely have been proposed a lot of algorithms for that, even if I am not expert enough to make a recommendation (CS.SE is around the corner, and can help on that, if needed). None of such proposals is polynomial time, but this doesn't stop them to be faster than the trivial approach.

Even without looking at the literature, a few simple optimizations can be found.

  • The original code scans the whole list [1..x], but this is not needed. We could stop at sqrt x, since after that there are no longer divisors.

  • Even more: after we find a divisor m, we could divide x by m (as many times as possible), and recurse with this new number. E.g. if x = 1000 after we try m=2, we compute 1000 -> 500 -> 250 -> 125, and then find the new divisors (larger than 2) in 125. Note how this made the number much smaller.

I will leave implementing this strategies in Haskell as an exercise :-P

like image 21
chi Avatar answered Mar 20 '23 04:03

chi