如何在Haskell中获得列表的中间位置?

时间:2009-11-14 18:36:49

标签: list haskell functional-programming

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

我正在慢慢地通过Erik Meijer's lectures on Channel 9(我看过目前为止的前4个),在第4个视频中,Erik解释了尾部是如何工作的,这让我很着迷。

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

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

如果你能解释你的选择,我将非常感激。

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

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

17 个答案:

答案 0 :(得分:17)

我没有做任何性能声明,虽然它只处理列表的元素一次(我的假设是计算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所建议的)

答案 1 :(得分:16)

只是为了你的娱乐,一个不会说Haskell的人的解决方案:

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

答案 2 :(得分:15)

两个版本

  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
    
  3. 第二个基本上是单行(但为了可读性而使用where。)

答案 3 :(得分:10)

  

我试过编写一个返回列表中间的函数(偶数长度为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个。毫无疑问,对于具有恒定时间长度和索引操作的数组,可以做得更好,但是我觉得数组不是列表。)

答案 4 :(得分:5)

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

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

答案 5 :(得分:2)

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

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

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

在Haskell中,它可能是这样的:

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

答案 6 :(得分:1)

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

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)中的长度。

答案 7 :(得分:1)

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

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

答案 8 :(得分:1)

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
...

答案 9 :(得分:0)

我自己并不是一个哈斯克勒,但我尝试过这个。

首先是测试(是的,你可以使用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 :(得分:0)

我住的是一个衬里,虽然这个例子只适用于奇数列表。我只想伸展自己的大脑!谢谢你的乐趣=)

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)

答案 11 :(得分:0)

我的解决方案,我喜欢简单易懂:

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中使用!!作为获取给定索引值的函数,在这种情况下,该值是列表长度的一半。

编辑:它实际上现在有效

答案 12 :(得分:0)

这在列表上迭代两次。

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

答案 13 :(得分:0)

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

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的输入列表。

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

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

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

答案 16 :(得分:0)

另一个单行解决方案:

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