查找唯一(仅发生一次)元素haskell

时间:2013-04-17 22:34:42

标签: algorithm haskell functional-programming

我需要一个函数,它接受一个列表并返回唯一元素(如果存在)或[]如果它不存在。如果存在许多独特元素,则应该返回第一个元素(不浪费时间去寻找其他元素)。 另外我知道列表中的所有元素都来自(小而且已知)集A. 例如,此功能可以完成Ints的工作:

unique :: Ord a => [a] -> [a]
unique li = first $ filter ((==1).length) ((group.sort) li)
    where first [] = []
          first (x:xs) = x

ghci> unique [3,5,6,8,3,9,3,5,6,9,3,5,6,9,1,5,6,8,9,5,6,8,9]
ghci> [1]

然而这不够好,因为它涉及排序(n log n),而它可以在线性时间内完成(因为A很小)。 另外,它需要列表元素的类型为Ord,而所有应该需要的是Eq。如果比较量尽可能小(例如,如果我们遍历列表并遇到元素el两次,我们不测试后续元素与el的相等性),那也是很好的。

这就是为什么例如:Counting unique elements in a list无法解决问题 - 所有答案都涉及排序或遍历整个列表以查找所有元素的计数。

问题是:如何在Haskell中正确有效地做到这一点?

6 个答案:

答案 0 :(得分:12)

好的,线性时间,来自有限域。运行时间为 O((m + d)log d),其中 m 是列表的大小, d 是大小域的,当 d 被修复时是线性的。我的计划是使用集合的元素作为trie的键,将计数作为值,然后通过trie查看计数为1的元素。

import qualified Data.IntTrie as IntTrie
import Data.List (foldl')
import Control.Applicative

计算每个元素。这会遍历列表一次,用结果构建一个trie( O(m log d)),然后返回一个在trie中查找结果的函数(运行时间 O(log) d))。

counts :: (Enum a) => [a] -> (a -> Int)
counts xs = IntTrie.apply (foldl' insert (pure 0) xs) . fromEnum
    where
    insert t x = IntTrie.modify' (fromEnum x) (+1) t

我们使用Enum约束将类型a的值转换为整数,以便在trie中对它们进行索引。 Enum实例是您假设a是一个小的有限集(Bounded将是另一部分,但见下文)的见证的一部分。

然后寻找独特的。

uniques :: (Eq a, Enum a) => [a] -> [a] -> [a]
uniques dom xs = filter (\x -> cts x == 1) dom
    where
    cts = counts xs

此函数将第一个参数作为整个域的枚举。我们可能需要Bounded a约束并使用[minBound..maxBound]代替,这在语义上很吸引我,因为有限内容基本上是Enum + Bounded,但由于现在域名需要,因此非常不灵活在编译时知道。所以我会选择这个稍微丑陋但更灵活的变体。

uniques遍历域名一次(懒惰,所以head . uniques dom只会遍历需要找到第一个唯一元素 - 不在列表中,而是在dom中),对于运行我们已经建立的查找功能的每个元素是 O(log d),所以过滤器需要 O(d log d),并构建表格计数需要 O(m log d)。所以uniques O((m + d)log d)中运行,当 d 被修复时,它是线性的。它至少需要Ω(m log d)才能从中获取任何信息,因为它必须遍历整个列表才能构建表格(你必须一直到达结束时)列表以查看元素是否重复,因此你不能做得比这更好。)

答案 1 :(得分:6)

只有Eq才真正有效地做到这一点。您需要使用一些效率低得多的方法来构建相等元素的组,并且您无法知道在不扫描整个列表的情况下只存在一个特定元素。

另外,请注意,为了避免无用的比较,您需要一种检查以查看之前是否遇到过元素的方法,并且唯一的方法是获得已知多次出现的元素列表,并且检查当前元素是否在该列表中的唯一方法是...将它与每个元素进行相等比较。

如果你想让它比O更快(非常可怕)你需要Ord约束。


好的,根据评论中的说明,这里有一个快速而又肮脏的例子,说明我想要的 :

unique [] _ _ = Nothing
unique _ [] [] = Nothing
unique _ (r:_) [] = Just r
unique candidates results (x:xs)
    | x `notElem` candidates = unique candidates results xs
    | x `elem` results       = unique (delete x candidates) (delete x results) xs
    | otherwise              = unique candidates (x:results) xs

第一个参数是候选人列表,最初应该是所有可能的元素。第二个参数是可能结果的列表,最初应为空。第三个参数是要检查的列表。

如果候选人用完,或到达列表末尾但没有结果,则返回Nothing。如果它到达列表的末尾并带有结果,则返回结果列表前面的那个。

否则,它会检查下一个输入元素:如果它不是候选者,则忽略它并继续。如果它在结果列表中我们已经看过两次,那么将其从结果和候选列表中删除并继续。否则,将其添加到结果中并继续。

不幸的是,这仍然需要扫描整个列表以查找单个结果,因为这是确保它实际上唯一的唯一方法。

答案 2 :(得分:2)

首先,如果您的函数最多只返回一个元素,那么您几乎肯定会使用Maybe a代替[a]来返回结果。

其次,至少,您别无选择,只能遍历整个列表:在查看所有其他元素之前,您无法确定任何给定元素是否实际上是唯一的。

如果你的元素不是Ord,但只能针对Eq uality进行测试,那么你真的没有比以下更好的选择:

firstUnique (x:xs)
  | elem x xs = firstUnique (filter (/= x) xs)
  | otherwise = Just x
firstUnique [] = Nothing

请注意,如果您不想要,则不需要过滤掉重复的元素 - 最坏的情况是二次方式。


编辑:

由于上述小/已知的一组可能元素,上述错过了提前退出的可能性。但是,请注意最坏的情况仍然需要遍历整个列表:所有必要的是至少有一个这些可能的元素从列表中缺失 ...

但是,在设置耗尽的情况下提供早期的实现:

firstUnique = f [] [<small/known set of possible elements>] where
  f [] [] _ = Nothing  -- early out
  f uniques noshows (x:xs)
    | elem x uniques = f (delete x uniques) noshows xs
    | elem x noshows = f (x:uniques) (delete x noshows) xs
    | otherwise      = f uniques noshows xs
  f []    _ [] = Nothing
  f (u:_) _ [] = Just u

请注意,如果您的列表中包含不应存在的元素(因为它们不在小/已知集合中),则上述代码将明确忽略它们...

答案 3 :(得分:2)

正如其他人所说的,没有任何额外的限制,你不能在不到二次的时间内做到这一点,因为在不了解元素的情况下,你不能将它们保存在一些合理的数据结构中。

如果我们能够比较元素,一个明显的 O(n log n)解决方案首先计算元素数,然后找到计数等于1的第一个:

import Data.List (foldl', find)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)

count :: (Ord a) => Map a Int -> a -> Int
count m x = fromMaybe 0 $ Map.lookup x m

add :: (Ord a) => Map a Int -> a -> Map a Int
add m x = Map.insertWith (+) x 1 m

uniq :: (Ord a) => [a] -> Maybe a
uniq xs = find (\x -> count cs x == 1) xs
  where
    cs = foldl' add Map.empty xs

请注意, log n 因素来自于我们需要在Map大小 n 上运行的事实。如果列表中只有 k 唯一元素,那么我们的地图大小最多为 k ,因此整体复杂度只有 O(n log k)

但是,我们可以做得更好 - 我们可以使用hash table代替地图来获取 O(n)解决方案。为此,我们需要ST monad在哈希映射上执行可变操作,我们的元素必须是Hashable。解决方案基本上和以前一样,只是因为在ST monad中工作而稍微复杂一点:

import Control.Monad
import Control.Monad.ST
import Data.Hashable
import qualified Data.HashTable.ST.Basic as HT
import Data.Maybe (fromMaybe)

count :: (Eq a, Hashable a) => HT.HashTable s a Int -> a -> ST s Int
count ht x = liftM (fromMaybe 0) (HT.lookup ht x)

add :: (Eq a, Hashable a) => HT.HashTable s a Int -> a -> ST s ()
add ht x = count ht x >>= HT.insert ht x . (+ 1)

uniq :: (Eq a, Hashable a) => [a] -> Maybe a
uniq xs = runST $ do
    -- Count all elements into a hash table:
    ht <- HT.newSized (length xs)
    forM_ xs (add ht)
    -- Find the first one with count 1
    first (\x -> liftM (== 1) (count ht x)) xs


-- Monadic variant of find which exists once an element is found.
first :: (Monad m) => (a -> m Bool) -> [a] -> m (Maybe a)
first p = f
  where
    f []        = return Nothing
    f (x:xs')   = do
        b <- p x
        if b then return (Just x)
             else f xs'

备注:

  • 如果您知道列表中只有少量不同的元素,则可以使用HT.new代替HT.newSized (length xs)。这将为您节省一些内存,并且可以通过xs进行一次传递,但是对于许多不同的元素,哈希表将需要多次调整大小。

答案 4 :(得分:1)

这是一个可以解决问题的版本:

unique :: Eq a => [a] -> [a]
unique =  select . collect []
  where
    collect acc []              = acc
    collect acc (x : xs)        = collect (insert x acc) xs

    insert x []                 = [[x]]
    insert x (ys@(y : _) : yss) 
      | x == y                  = (x : ys) : yss
      | otherwise               = ys : insert x yss

    select []                   = []
    select ([x] : _)            = [x]
    select ((_ : _) : xss)      = select xss

因此,首先我们遍历输入列表(collect),同时保持我们用insert更新的相等元素的列表列表。然后我们只需选择单个存储桶中出现的第一个元素(select)。

坏消息是这需要二次时间:对于collect中的每个访问过的元素,我们需要查看存储桶列表。我担心,只有能够将元素类型限制在Eq中才能付出代价。

答案 5 :(得分:0)

这样的东西看起来很不错。

unique = fst . foldl' (\(a, b) c -> if (c `elem` b) 
                                    then (a, b) 
                                    else if (c `elem` a) 
                                         then (delete c a, c:b) 
                                         else (c:a, b)) ([],[]) 

结果元组的第一个元素包含您期望的内容,包含唯一元素的列表。元组的第二个元素是如果元素已被丢弃则记住的进程的内存。

关于空间表现。
由于您的问题是设计,因此在显示结果之前,应至少遍历列表的所有元素。并且内部算法除了好的之外还必须保留废弃值的痕迹,但丢弃的值只出现一次。然后在最坏的情况下,所需的存储量等于输入列表的大小。正如你所说,预期的投入很小。

关于时间表现。
由于预期的输入很小而且默认情况下没有排序,因此尝试将列表排序到算法中是没用的,或者在应用它之前是没用的。事实上,我们几乎可以说,将元素放置在其有序位置(进入元组a的子列表b(a,b))的额外操作将花费相同的金额时间而不是检查该元素是否出现在列表中。


下面是一个更好,更明确的foldl'版本。

import Data.List (foldl', delete, elem)

unique :: Eq a => [a] -> [a]
unique = fst . foldl' algorithm ([], []) 
  where 
    algorithm (result0, memory0) current = 
         if (current `elem` memory0)
         then (result0, memory0)
         else if (current`elem` result0)
              then (delete current result0, memory) 
              else (result, memory0) 
            where
                result = current : result0
                memory = current : memory0

对于嵌套的if ... then ... else ...指令,在最坏的情况下遍历列表result两次,这可以避免使用以下辅助函数。

unique' :: Eq a => [a] -> [a]
unique' = fst . foldl' algorithm ([], []) 
  where 
    algorithm (result, memory) current = 
         if (current `elem` memory)
         then (result, memory)
         else helper current result memory []
            where
               helper current [] [] acc = ([current], [])
               helper current [] memory acc = (acc, memory)
               helper current (r:rs) memory acc 
                   | current == r    = (acc ++ rs, current:memory) 
                   | otherwise = helper current rs memory (r:acc)

但帮助者可以使用折叠重写如下,这肯定更好。

helper current [] _ = ([current],[])
helper current memory result = 
    foldl' (\(r, m) x -> if x==current 
                         then (r, current:m) 
                         else (current:r, m)) ([], memory) $ result