【问题标题】:Ways to get the middle of a list in Haskell?在 Haskell 中获得列表中间位置的方法?
【发布时间】:2023-03-27 21:02:01
【问题描述】:

我刚刚开始使用 Haskel 学习函数式编程。

我正在慢慢了解Erik Meijer's lectures on Channel 9(到目前为止我已经看过前 4 个),在第 4 个视频中,Erik 解释了 tail 的工作原理,它让我着迷。

我尝试编写一个返回列表中间的函数(偶数长度为 2 项,奇数长度为 1),我想听听其他人如何实现它

  • 最少的 Haskell 代码
  • 最快的 Haskell 代码

如果您能解释您的选择,我将不胜感激。

我的初学者代码如下所示:

middle as | length as > 2   = middle (drop 2 (reverse as))
          | otherwise       = as

【问题讨论】:

  • 您是否彻底测试过您的功能?中间的 "abcd" = "ba" 似乎不太对:)。
  • 我没有在小名单上测试过!回到绘图板......

标签: list haskell functional-programming


【解决方案1】:

仅供娱乐,来自不会说 Haskell 的人的解决方案:

编写一个递归函数,它接受两个参数 a1 和 a2,并将您的列表作为两个参数传入。在每次递归时,从 a2 中删除 2,从 a1 中删除 1。如果您没有 a2 的元素,那么您将位于 a1 的中间。您可以处理 a2 中仅剩下 1 个元素的情况,以回答您的“中间”是否需要 1 个或 2 个元素。

【讨论】:

  • 我喜欢它:D 我可能会在正常编程中使用它!
  • 我们似乎同意 Haskell 编程不是普通的编程 :) 但很高兴你喜欢它。
【解决方案2】:

基于 Carl 回答的 F# 解决方案:

let halve_list l =
    let rec loop acc1 = function
        | x::xs, [] -> List.rev acc1, x::xs
        | x::xs, [y] -> List.rev (x::acc1), xs
        | x::xs, y::y'::ys -> loop (x::acc1) (xs, ys)
        | [], _ -> [], []
    loop [] (l, l)

修改以获得列表中的中间元素也很容易:

let median l =
    let rec loop acc1 = function
        | x::xs, [] -> [List.head acc1; x]
        | x::xs, [y] -> [x]
        | x::xs, y::y'::ys -> loop (x::acc1) (xs, ys)
        | [], _ -> []
    loop [] (l, l)

更直观的方法是使用计数器:

let halve_list2 l =
    let rec loop acc = function
        | (_, []) -> [], []
        | (0, rest) -> List.rev acc, rest
        | (n, x::xs) -> loop (x::acc) (n - 1, xs)
    let count = (List.length l) / 2
    loop [] (count, l)

还有一个非常丑陋的修改来获得中间元素:

let median2 l =
    let rec loop acc = function
        | (n, [], isEven) -> []
        | (0, rest, isEven) ->
            match rest, isEven with
            | x::xs, true -> [List.head acc; x]
            | x::xs, false -> [x]
            | _, _ -> failwith "Should never happen"
        | (n, x::xs, isEven) -> loop (x::acc) (n - 1, xs, isEven)

    let len = List.length l
    let count = len / 2
    let isEven = if len % 2 = 0 then true else false
    loop [] (count, l, isEven)

获取列表的长度需要至少遍历其全部内容一次。幸运的是,编写自己的列表数据结构非常容易,它保存每个节点中列表的长度,让您在 O(1) 内获得长度。

【讨论】:

    【解决方案3】:

    我没有提出任何性能要求,尽管它只处理列表的元素一次(我的假设是计算 length t 是一个 O(N) 操作,所以我避免它),但这是我的解决方案:

    mid [] = []                      -- Base case: the list is empty ==> no midpt
    mid t = m t t                    -- The 1st t is the slow ptr, the 2nd is fast
      where m (x:_) [_] = [x]        -- Base case: list tracked by the fast ptr has
                                        -- exactly one item left ==> the first item
                                        -- pointed to by the slow ptr is the midpt.
            m (x:y:_) [_,_] = [x,y]  -- Base case: list tracked by the fast ptr has
                                        -- exactly two items left ==> the first two
                                        -- items pointed to by the slow ptr are the 
                                        -- midpts
            m (_:t) (_:_:u) = m t u  -- Recursive step: advance slow ptr by 1, and
                                        -- advance fast ptr by 2.
    

    这个想法是在列表中有两个“指针”,一个在递归中的每个点递增一个步骤,另一个递增 2。

    (本质上是 Carl Smotricz 建议的)

    【讨论】:

    • 我会将 [] 上的模式匹配移动到 mid 函数,这样我们就不必多次测试它:mid [] = []。不过看起来不错!
    • 函数式编程让我的思维尝试以前所未有的方式弯曲。谢谢你的回答,真的让我思考!
    • @jberryman,好点,因为该模式不会在递归中被击中,只会在对 mid 的初始调用中。我更新了代码以反映您的建议
    【解决方案4】:
    middle xs =
      let (ms, len) = go xs 0 [] len
      in  ms
    
    go (x:xs) i acc len =
      let acc_ = case len `divMod` 2 of
             (m, 0) -> if m  == (i+1) then (take 2 (x:xs))
                                      else acc
             (m, 1) -> if m  == i     then [x]
                                      else acc
      in go xs (i+1) acc_ len
    
    go [] i acc _ = (acc,i)
    

    此解决方案使用惰性求值仅遍历列表一次。在遍历列表时,它会计算长度,然后将其反馈给函数:

    let (ms, len) = go xs 0 [] len
    

    现在可以计算中间元素了:

    let acc' = case len `divMod` 2 of
    ...
    

    【讨论】:

    • 当然,Suppressingfire 的解决方案在运行时间和代码大小上都更胜一筹,所以忘记这个吧:D
    【解决方案5】:

    一个非常简单但不优雅且不那么简洁的解决方案可能是:

    middle :: [a] -> Maybe [a]
    middle xs
        | len <= 2 = Nothing
        | even len = Just $ take 2 . drop (half - 1) $ xs
        | odd len = Just $ take 1 . drop (half) $ xs
        where 
              len = length xs
              half = len `div` 2
    

    【讨论】:

      【解决方案6】:

      两个版本

      1. 使用模式匹配,tailinit

        middle :: [a] -> [a]
        middle l@(_:_:_:_) = middle $ tail $ init l
        middle l           = l
        
      2. 使用lengthtakesignummoddropdiv

        middle :: [a] -> [a]
        middle xs = take (signum ((l + 1) `mod` 2) + 1) $ drop ((l - 1) `div ` 2) xs
          where l = length xs
        

      第二个基本上是单行的(但使用where 以提高可读性)。

      【讨论】:

      • 我真的很喜欢第一个。如此简短。作为旁白; Haskell 中的倒勾是如何工作的?
      • @Matt:谢谢:)。反引号将二进制函数转换为中缀运算符。所以,fun :: a -&gt; b -&gt; c 可以称为fun a b,也可以称为a `fun` b
      • 最好说:middle 。尾巴 。初始化 $ l
      • 第一个的时间复杂度为O(n^3),不利于这种操作。您可以在 O(1) 中完成,请参阅 gwerns 答案。
      【解决方案7】:

      这会在列表上迭代两次。

      mid xs = m where
        l = length xs
        m | l `elem` [0..2] = xs
        m | odd l = drop (l `div` 2) $ take 1 $ xs
        m | otherwise = drop (l `div` 2 - 1) $ take 2 $ xs
      

      【讨论】:

        【解决方案8】:

        我为一个班轮而活,虽然这个例子只适用于奇数列表。我只是想伸展我的大脑!谢谢你的乐趣=)

        foo d = map (\(Just a) -> a) $ filter (/=Nothing) $ zipWith (\a b -> if a == b then Just a else Nothing) (Data.List.nub d) (Data.List.nub $ reverse d)
        

        【讨论】:

        • 看看Data.List.catMaybes 摆脱map . filter
        • 嗯,这很天真,如果列表的头部和尾部共享相同的元素 n-k 和 k,则不起作用,因为 n 是列表中的元素数。该死。嗯,很有趣!
        • 正义:我什至试图在没有小块的情况下做到这一点(只是想了一下,它解决了一个小问题,但一个更大更隐藏的问题仍然被评论),因为它不在前奏中。但是谢谢你,我不知道!
        【解决方案9】:

        我自己也不是很喜欢,但我试过这个。

        首先是测试(是的,你可以使用 Haskell 进行 TDD)

        module Main
        where
        import Test.HUnit
        import Middle
        main = do runTestTT tests
        tests = TestList [ test1
                         , test2
                         , test3
                         , test4
                         , test_final1
                         , test_final2
                         ]
        
        test1         =     [0]    ~=? middle [0]
        test2         =     [0, 1] ~=? middle [0, 1]
        test3         =     [1]    ~=? middle [0, 1, 2]
        test4         =     [1, 2] ~=? middle [0, 1, 2, 3]
        test_final1   =     [3]    ~=? middle [0, 1, 2, 3, 4, 5, 6]
        test_final2   =     [3, 4] ~=? middle [0, 1, 2, 3, 4, 5, 6, 7]
        

        我得出的解决方案:

        module Middle
        where
        
        middle a = midlen a (length a)
        
        midlen (a:xs) 1 = [a]
        midlen (a:b:xs) 2 = [a, b]
        midlen (a:xs) lg = midlen xs (lg - (2)) 
        

        它将遍历列表两次,一次是为了获得长度和一半以上以获得中间,但我不在乎它仍然是 O(n)(并且获得某物的中间意味着获得它的长度,所以没有理由避免它)。

        【讨论】:

        • 你其实无法避免。
        【解决方案10】:

        Carl's answer 启发的 Haskell 解决方案。

        middle = m =<< drop 1
           where m []  = take 1
                 m [_] = take 2
                 m (_:_:ys) = m ys . drop 1
        

        【讨论】:

        • 一个字都看不懂,但它看起来简洁明了,似乎反映了我算法的精神。 +1!
        • m =&lt;&lt; drop 1drop 1 &gt;&gt;= m 相同。因为drop 1 是一个函数,所以(r-&gt;)Monad 看起来很有趣,所以它等于\r -&gt; m ((drop 1) r) r\r -&gt; m (drop 1 r) r。我们可以更透明地将定义写成middle r = m (drop 1 r) rm 是做什么的?好吧,m [] r = take 1 r,所以如果r 有一个元素,m (drop 1 r) r = rm [_] r = take 2 r,所以如果r 有两个元素,m (drop 1 r) r = r。 (续)
        • 否则,m (drop 1 r) r = m (drop 2 (drop 1 r)) (drop 1 r) = m (drop 3 r) (drop 1 r)。现在我们看到这是龟兔方法:m 的第一个参数是兔子,第二个参数是乌龟。
        • 为了对称,我可以建议用m (_ : _ : ys) = m ys . drop 1 替换最后一行吗?
        【解决方案11】:

        我的解决方案,我喜欢保持简单:

        middle [] = []
        middle xs | odd (length xs) = [xs !! ((length xs) `div` 2)]
                  | otherwise = [(xs !! ((length xs) `div` 2)),(reverse $ xs) !! ((length xs)`div` 2)]
        

        在 Data.List 中使用 !! 作为函数来获取给定索引处的值,在本例中为列表长度的一半。

        编辑:它现在确实有效

        【讨论】:

        • 你测试过这段代码吗? length xs == oddInt 与函数进行比较。 length xs $ `div` 2 甚至不解析。
        • 我没有请原谅我。我会修好它,早上太晚了,不能好好思考。
        • 已修复。我确实喜欢它的简洁性和可读性。
        【解决方案12】:

        如果序列是一个链表,那么遍历这个链表是效率的主要因素。由于我们需要知道总长度,我们必须至少遍历列表一次。获取中间元素有两种等效方法:

        • 遍历列表一次以获取长度,然后将其遍历一半以获取中间元素。
        • 双步和单步同时遍历列表,这样当第一次遍历停止时,第二次遍历在中间。

        两者都需要相同数量的步骤。在我看来,第二个是不必要的复杂。

        在 Haskell 中,可能是这样的:

        middle xs = take (2 - r) $ drop ((div l 2) + r - 1) xs
                  where l = length xs
                        r = rem l 2
        

        【讨论】:

        • 如果列表(以及它附带的任何 crud)不适合 CPU 缓存,但大约一半 可以,那么龟兔方法可能会更快- 当乌龟到达它们时,兔子拉进来的conses仍然会在缓存中。这种方法还具有潜在的内存优势,尤其是在延迟生成列表的情况下:乌龟可以随时吃掉列表。
        【解决方案13】:

        这是我的版本。这只是一个快速的运行。我敢肯定它不是很好。

        middleList xs@(_:_:_:_) = take (if odd n then 1 else 2) $ drop en xs
            where n = length xs
                  en = if n < 5 then 1 else 2 * (n `div` 4)
        middleList xs = xs
        

        我试过了。 :)

        如果有人想评论并告诉我这个解决方案有多糟糕或有多好,我将不胜感激。我不是非常精通 Haskell。

        编辑:根据 kmc 关于#haskell-blah 的建议进行了改进

        编辑 2:现在可以接受长度小于 5 的输入列表。

        【讨论】:

        • 我能看到的明显改进是应该能够处理任意长度的列表
        • 它可以在 15 小时前。我刚刚添加了错误,因为 3 个元素以南的任何东西都是毫无意义的。您如何处理包含 2 个元素的列表?我猜我可以返回 1 和 2,但我不确定我为什么会这样做。
        • 去掉了错误行并为长度为 1 和两个列表添加了一些匹配项。
        【解决方案14】:

        我喜欢 Svante 的回答。我的版本:

        > middle :: [a] -> [a]
        > middle [] = []
        > middle xs = take (r+1) . drop d $ xs
        >  where
        >    (d,r) = (length xs - 1) `divMod` 2
        

        【讨论】:

          【解决方案15】:

          我尝试编写一个返回列表中间的函数(偶数长度为 2 项,奇数长度为 1),我想听听其他人如何实现它

          针对正确问题的正确数据结构。在这种情况下,您指定了仅在 有限 列表中有意义的内容,对吗?无限列表没有“中间”。所以只要阅读描述,我们就知道默认的 Haskell 列表可能不是最好的解决方案:即使我们不需要它,我们也可能为懒惰付出代价。请注意有多少解决方案难以避免2*O(n)O(n)。单链惰性列表不能很好地匹配准数组问题。

          幸运的是,我们在 Haskell 中确实有一个有限列表:它被称为 Data.Sequence

          让我们以最明显的方式解决它:'index (length / 2)'。

          根据文档,Data.Seq.length 为 O(1)。 Data.Seq.index 是O(log(min(i,n-i)))(我认为 i=index,n=length)。我们就叫它O(log n)吧。不错!

          请注意,即使我们一开始不是Seq 并且必须将[a] 转换为Seq,我们仍然可能会赢。 Data.Seq.fromList 是O(n)。因此,如果我们的竞争对手是 O(n)+O(n) 解决方案,例如 xs !! (length xs),则解决方案类似于

          middle x = let x' = Seq.fromList x in Seq.index(Seq.length x' `div` 2)
          

          会更好,因为它会是O(1) + O(log n) + O(n),简化为O(log n) + O(n),明显优于O(n)+O(n)

          (作为练习留给读者修改中间,如果长度为偶数则返回 2 项,如果长度为奇数则返回 1。毫无疑问,使用具有恒定时间长度和索引操作的数组可以做得更好,但是我觉得数组不是一个列表。)

          【讨论】:

          • 谢谢格温。这是一个有用的解释。我会研究一下 Data.Squence。
          • 很好的解释!格言“正确问题的正确数据结构”说明了一切。附带说明:O(log n) + O(n) 并不明显优于 O(n) + O(n)。两者都简化为 O(n),因此您必须知道常数因子才能真正判断哪个更好。
          • Carnieri:我确实考虑了一段时间,但是一个 O(n) 抵消了另一个,留下 log(n) vs n;顺便说一句,我想不出 log2(n) >= n 或 log10(n) >= n 的任何非负数。 (不过,自然日志将是反例。)鉴于 Big-O 的一般模糊性,我认为可以肯定地假设前者更好。
          • 不,您不能以这种方式取消 O(n) 项。否则你可以推断出 O(n) + O(n) 仍然 O(n),然后取消,得出没有对数项的一侧更好!两种算法都是简单的 O(n);您需要准确分析渐近分析中忽略的内容,以了解哪个更好,因此您无法从大字中做到这一点。
          【解决方案16】:

          另一种单行解决方案:

          --
          middle = ap (take . (1 +) . signum . (`mod` 2) . (1 +) . length) $ drop =<< (`div` 2) . subtract 1 . length
          --
          

          【讨论】:

            【解决方案17】:

            奇怪的是,这个非常明显的公式还没有出现:

            middle []    = []
            middle [x]   = [x]
            middle [x,y] = [x,y]
            middle xs    = middle $ init $ tail xs
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-05-02
              • 1970-01-01
              • 1970-01-01
              • 2023-04-05
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多