Haskell列表中的第二个最小值

时间:2018-04-29 11:24:58

标签: list haskell

我正在尝试编写一个返回列表中第二个最小值的函数secondSmallest,如果没有这样的值,则返回Nothing。此外,如果列表的最小值出现多次,则它也是第二小的值。例如:

  • secondSmallest [1.0] - > Nothing
  • secondSmallest [1,1,2] - > Just 1
  • secondSmallest [5,3,7,2,3,1] - > Just 2

这是我到目前为止所拥有的:

secondSmallest :: Ord a => [a] -> Maybe a
secondSmallest []       = Nothing
secondSmallest [x]      = Nothing
secondSmallest (x:y:xs) = Just secondSmallest ((if x < y then x else y):xs)

5 个答案:

答案 0 :(得分:10)

使用sort

懒惰方法是使用sort :: Ord a => [a] -> [a]sort以惰性方式返回排序列表,并获取 k -th元素,其中 O(n)表示固定元素,并且在我们枚举 k -th元素的情况下,它将采用 O(n log k),因此我们可以对sort结果执行模式匹配,然后返回第二个元素,如:

import Data.List(sort)

secondSmallest :: Ord a => [a] -> Maybe a
secondSmallest l | (_:x:_) <- sort l = Just x
                 | otherwise = Nothing

使用累加器

另一种方法是使用累加器。累加器是递归中的参数,我们(可选)更新然后传递更新的版本。在这种情况下,我们可以使用包含最小和一个但最小元素的2元组。所以现在我们实现另一个函数

secondSmallest' :: Ord a => (a, a) -> [a] -> a
secondSmallest' (s1, s2) ...

其中s1 <= s2始终成立。现在我们需要考虑一些案例:

  1. 列表为空,在这种情况下我们到达列表的末尾,因此可以返回s2,到目前为止获得的第二个最小值

    secondSmallest' (_, s2) [] = s2
    
  2. 列表不为空,并且头x大于或等于s2,在这种情况下,到目前为止两个最小的元素保持不变,所以我们可以通过2元组不变,我们在列表的尾部递归。

    secondSmallest' s@(s1, s2) (x:xs) | x >= s2 = secondSmallest' s xs
    
  3. 如果头x大于或等于s1但小于s2,那么我们就知道这是最新的第二小,所以在这种情况下,我们用(s1, x)构造一个新的2元组并递归列表的尾部

                                      | x >= s1 = secondSmallest' (s1, x) xs
    
  4. 如果头x小于s1,那么我们获得了一个新的最小元素。在这种情况下,s1是最新的最小元素:

                                      | otherwise = secondSmallest' (x, s1) xs
    
  5. 所以现在我们实现了一个函数 - 假设我们已经有两个因此最小的元素 - 可以返回第二个最小的元素:

    secondSmallest' :: Ord a => (a, a) -> [a] -> a
    secondSmallest' (_, s2) [] = s2
    secondSmallest' s@(s1, s2) (x:xs) | x >= s2 = secondSmallest' s xs
                                      | x >= s1 = secondSmallest' (s1, x) xs
                                      | otherwise = secondSmallest' (x, s1) xs
    

    但是现在我们还没有解决主要问题,我们如何获得前2个最小的元素。基本上这里有三种情况:

    1. 如果列表包含至少两个元素,并且第一个元素小于或等于第二个元素,我们将第一个元素作为最小元素,将第二个元素作为第二个最小元素,并执行调用列表的其余部分,我们将结果包装在Just构造函数中:

      secondSmallest (x1:x2:xs) | x1 <= x2 = Just (secondSmallest' (x1, x2) xs)
      
    2. 如果列表包含至少两个元素但第一个元素大于第二个元素,那么我们将第一个元素作为第二个元素,将第二个元素作为最小元素,我们再次执行调用secondSmallest'函数,并将结果包装在Just构造函数中:

                                | otherwise = Just (secondSmallest' (x2, x1) xs)
      
    3. 如果列表包含的项目较少,我们无法计算第二个最小值,因为没有第二个最小值,所以我们返回Nothing

      secondSmallest _ = Nothing
      
    4. 所以我们得到了:

      secondSmallest :: Ord a => [a] -> Maybe a
      secondSmallest (x1:x2:xs) | x1 <= x2 = Just (secondSmallest' (x1, x2) xs)
                                | otherwise = Just (secondSmallest' (x2, x1) xs)
      secondSmallest _ = Nothing
      
      secondSmallest' :: Ord a => (a, a) -> [a] -> a
      secondSmallest' (_, s2) [] = s2
      secondSmallest' s@(s1, s2) (x:xs) | x >= s2 = secondSmallest' s xs
                                        | x >= s1 = secondSmallest' (s1, x) xs
                                        | otherwise = secondSmallest' (x, s1) xs
      

      请注意,secondSmallest'实际上是fold模式,因此我们可以将其编写为:

      secondSmallest :: Ord a => [a] -> Maybe a
      secondSmallest (x1:x2:xs) | x1 <= x2 = Just (secondSmallest' (x1, x2) xs)
                                | otherwise = Just (secondSmallest' (x2, x1) xs)
      secondSmallest _ = Nothing
      
      secondSmallest' :: Ord a => (a, a) -> [a] -> [a]
      secondSmallest' t = snd . foldr f t
          where f s@(s1, s2) x | x >= s2 = s
                               | x >= s1 = (s1, x)
                               | otherwise = (x, s1)
      

答案 1 :(得分:4)

一种选择是使用折叠。让我们从为累加器写一个类型开始:

import Data.List (foldl')

data Acc a = None
           | One a
           | Two {sml :: a, sndsml :: a}

finish :: Acc a -> Maybe a
finish Two{sndsml = r} = Just r
finish _ = Nothing

现在,

secondSmallest
  :: Ord a => [a] -> Maybe a
secondSmallest = finish . foldl' go None
  where
    go None a = One a
    go (One b) a
      | b < a = Two{sml=b, sndsml=a}
      | otherwise = Two{sml=b, sndsml=a}
    go r@Two{sml=b, sndsml=c} a
      | a < b = Two{sml=a,sndsml=b}
      | a < c = Two{sml=b,sndsml=a}
      | otherwise = r

-- For extra efficiency in some cases
{-# INLINABLE secondSmallest #-}

GHC非常聪明,可以将此定义编译成Willem Van Onsem的解决方案。然而,当通过一个好的制作人&#34;对于列表融合(例如,secondSmallest (map (\x -> x * (x - 50)) xs)),基于折叠的版本通常会更有效。

答案 2 :(得分:2)

dfeuer&#39; answer给了我一个想法,

import Data.List (foldl', sort)
import Data.Maybe (listToMaybe)

secondSmallest :: Ord a => [a] -> Maybe a
secondSmallest = listToMaybe . take 1 . drop 1 . foldl' g [] 
    where 
    g acc x = f acc `seq` take 2 (sort (x:acc))
    f [a,b] = a < b
    f _     = True

代码很短,但由于所有列表函数,可能效率低一点。

编辑:强制累加器。

答案 3 :(得分:2)

这是使用折叠的不同解决方案,其中元组{Maybe a, Maybe a)作为累加器。该算法类似于dfeuer的优秀答案。它不需要辅助类型,并且提取解决方案的功能已经在Prelude中。一个缺点是它确实有一个额外的模式匹配:有一个不可能的状态(Nothing, Just _),其中第二个最小的元素已经设置但不是最小的,我们检查它,以便我们的模式是详尽的,而dfeuer的Acc a类型仅编码有效的程序状态。这个贯穿始终的情况不会在运行时减慢程序的速度,因为它永远不会到达。编译器可能会将累加器参数解包并获得更高效的效果。

ETA: Willem Van Onsem在答案结束时给出了类似的解决方案。当且仅当列表具有至少两个元素时,它在调用折叠方面不同,这意味着折叠函数只需要匹配一个模式。在大多数情况下,这更有效,因为设置累加器初始值的开销只需要完成一次,但折叠函数中模式匹配的开销必须在每个元素上完成一次。它内部也不需要Maybe。我会留下这个,希望看到几种不同的方法是有帮助的。

import Data.List (foldl')

secondSmallest :: Ord a => [a] -> Maybe a
secondSmallest = snd . foldl' go (Nothing, Nothing) where
{- The accumulator of go is a pair whose first element is the smallest
 - element encountered so far and whose second element is the second-
 - smallest encountered so far.
 -}
  go (Nothing, Nothing) x = (Just x, Nothing)
  go (Just x, Nothing) y | y < x     = (Just y, Just x)
                         | otherwise = (Just x, Just y)
  go (Just x, Just y) z | z < x     = (Just z, Just x)
                        | z < y     = (Just x, Just z)
                        | otherwise = (Just x, Just y)
  go _ _ = error "Logic error: found second-smallest but not smallest element."

答案 4 :(得分:0)

我只有一个有效的非解决方案。这取决于我刚才写的最小功能。我的最小值是一个参数,一个列表。你的功能似乎正在寻找最低限度,所以这是我的。它必须放在一个包装器中,以满足您的所有要求。它在[]失败并返回给定x的{​​{1}},因此会再次失败。

[x]

如何使用它就像产生第二个最小元素一样。它找到第一个最小的,然后从找到的搜索列表中将其排除。

minf :: (Num b, Ord b) => [b] -> b
minf [x]    = x
minf (x:xs) = if x < head xs then minf $ x : tail xs else minf xs

2