我需要一个函数,它接受一个列表并返回唯一元素(如果存在)或[]如果它不存在。 如果存在许多独特元素,则应该返回第一个元素(不浪费时间去寻找其他元素)。 另外我知道列表中的所有元素都来自(小而且已知)集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的相等性)也会很好

这就是为什么例如: 计算列表中的唯一元素并不能解决问题 - 所有答案都涉及排序或遍历整个列表以查找所有元素的计数。

问题是:如何在Haskell中正确有效地完成它?

===============>>#1 票数: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

计算每个元素。 这遍历列表一次,用结果( O(m log d) )构建一个trie,然后返回一个在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)才能从中获取任何信息,因为它必须遍历整个列表才能构建表(你必须一直到列表的末尾才能看到元素是否是反复,所以你不能比这更好)。

===============>>#2 票数: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 如果它到达带有结果的列表末尾,则返回结果列表前面的那个。

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

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

===============>>#3 票数:2

首先,如果你的函数最多只返回一个元素,你几乎肯定会使用Maybe a而不是[a]来返回你的结果。

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

如果你的元素不是Ord ered,但只能测试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

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

===============>>#4 票数: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因子来自于我们需要在大小为nMap上操作的事实。 如果列表只有k个唯一元素,那么我们的地图大小最多为k ,因此总体复杂度将只为O(n log k)

但是,我们可以做得更好 - 我们可以使用哈希表而不是地图来获得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但在许多不同的元素的情况下,哈希表将不得不重新调整几次。

===============>>#5 票数: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才需要付出代价。

===============>>#6 票数: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)的子列表ab的额外操作将花费相同的时间而不是检查此元素是否出现在列表中。


下面是一个更好,更明确的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

  ask by Piotr Lopusiewicz translate from so

未解决问题?本站智能推荐: