我正在尝试编写一个返回列表中第二个最小值的函数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)
答案 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
始终成立。现在我们需要考虑一些案例:
列表为空,在这种情况下我们到达列表的末尾,因此可以返回s2
,到目前为止获得的第二个最小值
secondSmallest' (_, s2) [] = s2
列表不为空,并且头x
大于或等于s2
,在这种情况下,到目前为止两个最小的元素保持不变,所以我们可以通过2元组不变,我们在列表的尾部递归。
secondSmallest' s@(s1, s2) (x:xs) | x >= s2 = secondSmallest' s xs
如果头x
大于或等于s1
但小于s2
,那么我们就知道这是最新的第二小,所以在这种情况下,我们用(s1, x)
构造一个新的2元组并递归列表的尾部
| x >= s1 = secondSmallest' (s1, x) xs
如果头x
小于s1
,那么我们获得了一个新的最小元素。在这种情况下,s1
是最新的最小元素:
| otherwise = secondSmallest' (x, s1) xs
所以现在我们实现了一个函数 - 假设我们已经有两个因此最小的元素 - 可以返回第二个最小的元素:
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个最小的元素。基本上这里有三种情况:
如果列表包含至少两个元素,并且第一个元素小于或等于第二个元素,我们将第一个元素作为最小元素,将第二个元素作为第二个最小元素,并执行调用列表的其余部分,我们将结果包装在Just
构造函数中:
secondSmallest (x1:x2:xs) | x1 <= x2 = Just (secondSmallest' (x1, x2) xs)
如果列表包含至少两个元素但第一个元素大于第二个元素,那么我们将第一个元素作为第二个元素,将第二个元素作为最小元素,我们再次执行调用secondSmallest'
函数,并将结果包装在Just
构造函数中:
| otherwise = Just (secondSmallest' (x2, x1) xs)
如果列表包含的项目较少,我们无法计算第二个最小值,因为没有第二个最小值,所以我们返回Nothing
:
secondSmallest _ = Nothing
所以我们得到了:
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)
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