简体   繁体   中英

Performance of iterating a list by recursively examining the tail

I decided to try to learn Haskell by doing some of the CodinGame challenges (so this question is super beginner-level stuff I'm sure). One of them requires searching through a list of integers for the smallest difference between any two values. I've previously solved it in Clojure by doing this:

(ns Solution
  (:gen-class))

(defn smallest-difference [values]
  (let [v (sort values)]
    (loop [[h & t] v curr-min 999999]
      (if (nil? t) curr-min
        (let [dif (- (first t) h)]
          (recur t (if (> curr-min dif) dif curr-min)))))))

(defn -main [& args]
  (let [horse-strengths (repeatedly (read) #(read))]
      (let [answer (smallest-difference horse-strengths)]
        (println answer)))) 

I tried to implement the same solution in Haskell, with the following:

readHorses :: Int -> [Int] -> IO [Int]
readHorses n h
    | n < 1 = return h
    | otherwise = do
        l <- getLine
        let hn = read l :: Int
        readHorses (n - 1) (hn:h)

findMinDiff :: [Int] -> Int -> Int
findMinDiff h m
    | (length h) < 2    = m
    | (h!!1 - h!!0) < m = findMinDiff (tail h) (h!!1 - h!!0)
    | otherwise         = findMinDiff (tail h) m



main :: IO ()
main = do
    hSetBuffering stdout NoBuffering -- DO NOT REMOVE
    input_line <- getLine
    let n = read input_line :: Int
    hPrint stderr n
    horses <- readHorses n []
    hPrint stderr "Read all horses"
    print (findMinDiff (sort horses) 999999999)
    return ()

This times-out for large inputs (99999 values) where the Clojure solution does not. They look pretty similar to me however.

It at least superficially seems that reading the values and constructing the list isn't the problem, as "Read all horses" is printed before the timeout.

How can I make the Haskell version more performant?

You're calculating the length of the list in every recursion into findMinDiff . Since length takes O(n) time findMinDiff takes O(n^2) time instead of O(n) .

You can write the same thing with pattern matching instead of length , !! , and tail

findMinDiff :: [Int] -> Int -> Int
findMinDiff (h0 : hs@(h1 : _)) m = 
    if h1 - h0 < m
    then findMinDiff hs (h1 - h0)
    else findMinDiff hs m
findMinDiff _ m = m

By the way, a completely alternative implementation could be written as follows. (Pseudocode follows)

Take the list

h = [h0, h1, h2 ...

Remove one element

drop 1 h = [h1, h2, h3 ...

Compute pointwise differences

zipWith (-) (drop 1 h) h = [h1-h0, h2-h1, h3-h2, ...

Then take the minimum. Full code:

minDiff :: [Int] -> Int
minDiff h = minimum (zipWith (-) (drop 1 h) h)

Note that this will crash on the empty list. On the other hand, there's no need of the 9999999 hack. Thanks to laziness, it runs in constant space, too.

For a better error handling:

minDiff :: [Int] -> Int
minDiff [] = error "minDiff: empty list"
minDiff h = minimum (zipWith (-) (drop 1 h) h)

or even, more pedantically (but respecting totality):

minDiff :: [Int] -> Maybe Int
minDiff [] = Nothing
minDiff h  = Just (minimum (zipWith (-) (drop 1 h) h))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM