【问题标题】:Performance of iterating a list by recursively examining the tail通过递归检查尾部来迭代列表的性能
【发布时间】:2017-05-29 02:36:44
【问题描述】:

我决定尝试通过做一些CodinGame 挑战来学习 Haskell(所以我敢肯定这个问题是超级初学者级别的东西)。其中之一需要在整数列表中搜索任意两个值之间的最小差异。我以前通过这样做在 Clojure 中解决了它:

(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)))) 

我尝试在 Haskell 中实现相同的解决方案,如下:

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 ()

对于 Clojure 解决方案没有的大输入(99999 个值)会超时。然而,它们看起来和我很相似。

至少从表面上看,读取值和构建列表似乎不是问题,因为“读取所有马匹”是在超时之前打印的。

如何使 Haskell 版本的性能更高?

【问题讨论】:

  • length h &lt; 2 将遍历整个列表。我会为[][_] 使用模式匹配,即使这需要少量重复。 (在最后一种情况下,我会使用 (x1:x2:xs) 并避免部分使用 !! - 这不会提高速度,但它更习惯用语)
  • 谢谢!我非常感谢 findMindDiff 的示例实现,它说明了这个@chi。

标签: performance haskell recursion clojure


【解决方案1】:

您在每次递归到findMinDiff 时计算列表的length。因为length 需要O(n) 时间findMinDiff 需要O(n^2) 时间而不是O(n)

你可以用pattern matching代替length!!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

【讨论】:

  • 请注意,if...then...else 只是一个表达式,因此您可以分解出函数调用和第一个参数:findMinDiff hs (if h1 - h0 &lt; m then h1 - h0 else m)。您还可以将表达式替换为调用min
  • @chepner 是的,但此答案的目标是使代码尽可能与 OP 相似,以展示保护长度和模式匹配之间的区别。如果我可以用警卫写if ... then ... else,而不会让它变得可怕,我会的。
  • @Orphid 更详细的方式将在(h0 : h1 : hs') 上进行模式匹配,然后为尾部调用重构列表h1 : hs'。由于h1 : hs' 已经在模式中,我使用hs@(h1 : hs') 用新名称捕获它,然后因为hs' 没有在任何地方使用,用_ 替换它。
  • @Orphid 您可以将模式h0 : hs@(h1 : _) 解读为“如果输入列表具有h0 : hs 的形式(因此它是非空的),而尾部hs 的形式为h1 : _(同样,非空),然后进行如下操作:...”。然后,您可以使用h0,h1,hs 来表达其自然含义。
  • @Orphid 是的:只有当第一个模式失败时,即length h &lt; 2 时,才会到达该行。
【解决方案2】:

顺便说一下,一个完全替代的实现可以写成如下。 (后面是伪代码)

拿清单

h = [h0, h1, h2 ...

删除一个元素

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

计算逐点差异

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

然后取最小值。完整代码:

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

请注意,这将在空列表上崩溃。另一方面,不需要9999999 hack。由于懒惰,它也可以在恒定空间中运行。

为了更好地处理错误:

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

甚至,更迂腐(但尊重整体):

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

【讨论】:

  • 是的 - 事实上,在我完成挑战后,我能够查看其他人的解决方案。 exlevan 的评分似乎最高,也使用了minimumzipWith
  • @Orphid 了解库函数肯定会有所帮助,但作为练习,在没有太多库帮助的情况下学会解决这个问题更为重要。如果可能的话,我建议避免使用length, head, tail, !!(也许还有守卫),因为在许多初学者的练习中它们是不需要的,只会导致代码脆弱。在 Haskell 中,模式匹配比其他任何东西都好。
猜你喜欢
  • 2011-11-14
  • 1970-01-01
  • 2011-10-04
  • 2016-05-09
  • 2016-06-14
  • 2018-12-29
  • 1970-01-01
  • 2021-02-09
  • 2017-03-31
相关资源
最近更新 更多