从列表中取出直到遇到重复

时间:2015-02-27 00:39:03

标签: haskell

在Haskell中,takeWhile允许用户从(可能是无限的)列表中获取条目,直到某个条件不成立为止。

但是,此条件不能取决于列表的先前条目。

如果我遇到第一个副本(如本例中所述),我怎样take来自(可能是无限的)列表中的条目?

*Main> takeUntilDuplicate [1,2,3,4,5,1,2,3,4]
[1,2,3,4,5]

4 个答案:

答案 0 :(得分:8)

解决此问题的一种方法是在遍历列表时更新一段状态,类似于您在命令式语言中所做的操作。这需要与State monad一起工作,这可能需要一些学习和玩耍来获得它,如果这是你第一次,但相信我,这是值得学习的。让我们从导入开始:

import Control.Monad.State
import Data.Set (Set)
import qualified Data.Set as Set

我们要保留的状态是在列表中看到的那些元素Set。首先,让我们编写一对简单的State动作来管理一组看到的元素:

-- Add an element to the context Set
remember :: Ord a => a -> State (Set a) ()
remember a = modify (Set.insert a)

-- Test if the context set contains an element
haveSeen :: Ord a => a -> State (Set a) Bool
haveSeen a = do seen <- get
                return (a `Set.member` seen)

现在我们要将这两者合并为一个检查重复的动作:

isDuplicate :: Ord a => a -> State (Set a) Bool
isDuplicate a = do seen <- haveSeen a
                   remember a
                   return seen

您已经提到了takeWhile功能。我们将按照类似的方针构建我们的解决方案。这是takeWhile的定义:

-- different name to avoid collision
takeWhile' :: (a -> Bool) -> [a] -> [a]
takeWhile' _ [] =  []
takeWhile' p (a:as)
    | p a =  a : takeWhile p as
    | otherwise =  []

我们可以修改此函数以使用包含在monad中的Bool的谓词:

takeWhileM :: Monad m => (a -> m Bool) -> [a] -> m [a]
takeWhileM _ [] = return []
takeWhileM p (a:as) = 
    do test <- p a
       if test
       then do rest <- takeWhileM p as
               return (a:rest)
       else return []

但这里的关键区别是因为takeWhileM中的测试是monadic,我们可以使用上面的有状态isDuplicate。因此,每次我们使用isDuplicate测试列表的元素时,我们还会在Set中记录该元素,该元素正在通过计算。所以现在我们可以这样写takeUntilDuplicate

takeUntilDuplicate :: Ord a => [a] -> [a]
takeUntilDuplicate as = evalState (takeUntilDuplicate' as) Set.empty
    where takeUntilDuplicate' :: Ord a => [a] -> State (Set a) [a]
          takeUntilDuplicate' = takeWhileM (fmap not . isDuplicate)

使用示例(带有无限列表参数):

 >>> takeUntilDuplicate (cycle [1..5])
 [1,2,3,4,5]

而且很简单的是,这些代码中的一些可以重用于类似的问题。

答案 1 :(得分:5)

假设您正在处理Ord个实例,您可以这样做。这与Luis Casillas's answer基本相同,但使用折叠代替State表示。我们的每个答案都使用了不同的普遍适用的技术。路易斯包括对他的一个很好的解释;我的经典解释是格雷厄姆赫顿的“折叠的普遍性和表现力的教程”。

import Data.Set (member)
import qualified Data.Set as S

takeUntilDuplicate :: Ord a => [a] -> [a]
takeUntilDuplicate xs = foldr go (const []) xs S.empty
  where
    go x cont set
      | x `member` set = []
      | otherwise      = x : cont (S.insert x set)

如果您实际上正在处理Int(或任何可以快速转换为Int的内容),您可以使用Data.IntSet或{{1}大幅改善效果而不是Data.HashSet

答案 2 :(得分:4)

您的观点takeWhile不起作用,因为您没有针对各个元素的上下文信息,这表明了以下策略:获取它。

This我的回答引用了decorate-with-context操作,我称之为picks(因为它向您展示了选择要聚焦的一个元素的所有方法)。这是我们应该为每个容器事物免费提供的一般装饰与其上下文操作。对于列表,它是

picks :: [x] -> [(x, ([x], [x]))] -- [(x-here, ([x-before], [x-after]))]
picks [] = []
picks (x : xs) = (x, ([], xs)) : [(y, (x : ys, zs)) | (y, (ys, zs)) <- picks xs]

它适用于无限列表,而我们就是这样。

现在有了

takeUntilDuplicate :: Eq x => [x] -> [x]
takeUntilDuplicate = map fst . takeWhile (\ (x, (ys, _)) -> not (elem x ys)) . picks

(奇怪的是,如果没有给出上述类型的签名,我会因为Eq的歧义而拒绝上述单行。我很想在这里问一个关于它的问题。哦,这是单态限制。多么烦人。

备注。 picks使用snoc-lists(右侧增长的列表)代表{{1}}提供的“元素之前”组件是很有意义的。 ),更好地保持共享和视觉从左到右。

答案 3 :(得分:2)

您可以使用this duplicate removal function的修改版本:

takeUntilDuplicate :: Eq a => [a] -> [a]
takeUntilDuplicate = helper []
    where helper seen [] = seen
          helper seen (x:xs)
              | x `elem` seen = seen
              | otherwise = helper (seen ++ [x]) xs

请注意,对于大型列表,elem效率非常低。假设a(列表中的数据类型)是Ord类型,可以使用Data.Set进行成员资格查找来改进此功能:

import qualified Data.Set as Set

takeUntilDuplicate' :: (Eq a, Ord a) => [a] -> [a]
takeUntilDuplicate' = helper Set.empty []
    where helper seenSet seen [] = seen
          helper seenSet seen (x:xs)
              | x `Set.member` seenSet = seen
              | otherwise = helper (Set.insert x seenSet) (seen ++ [x]) xs

如果您不关心返回结果的顺序,可以通过返回Set来进一步提高函数的效率:

导入限定数据。设置为Set import Data.Set(Set)

takeUntilDuplicateSet :: (Eq a, Ord a) => [a] -> Set a
takeUntilDuplicateSet = helper Set.empty
    where helper seenSet [] = seenSet
          helper seenSet (x:xs)
              | x `Set.member` seenSet = seenSet
              | otherwise = helper (Set.insert x seenSet) xs