【问题标题】:Finding unique (as in only occurring once) element haskell寻找唯一的(如只发生一次)元素haskell
【发布时间】:2013-04-17 22:34:42
【问题描述】:

我需要一个函数,它接受一个列表,如果存在则返回唯一元素,如果不存在则返回 []。如果存在许多独特的元素,它应该返回第一个(不要浪费时间去寻找其他元素)。 此外,我知道列表中的所有元素都来自(小且已知的)集合 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 中正确高效地做到这一点?

【问题讨论】:

    标签: algorithm haskell functional-programming


    【解决方案1】:

    好的,线性时间,来自有限域。运行时间将是 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],这在语义上对我很有吸引力,因为finite 本质上是Enum+Bounded,但是非常不灵活,因为现在需要在编译时知道域。所以我会选择这个稍微难看但更灵活的变体。

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

    【讨论】:

    • 希望我能多次投票。似乎其他人都忽略了“有限的小域”资格。
    • 公平地说,OQ 规定元素没有排序,只能进行比较。也就是说,添加一个Enum 实例显然是正确的做法,因为很难想象一个不符合条件的“有限的小域”,O(1) fromEnum...
    • 虽然我对一般情况(非有序元素)感兴趣,但我遇到的具体问题可以轻松使用有序类型。我从这个答案中学到了很多东西。
    • 然而,在我看来,它遍历了整个列表,即使它没有必要(如果 A 的所有元素已经出现两次)。对吗?
    • @PiotrLopusiewicz 啊,是的,确实如此。不过,可以更改此解决方案以允许这样做——而不是使用 Ints 作为树的值,而是使用精确跟踪问题结构的特殊幺半群的值:1 <> 1 = 22 <> 1 = 2 等。你还必须将严格的foldl in counts 更改为懒惰的foldr 以允许遍历延迟执行。如果您愿意,我可以发布更多详细信息。
    【解决方案2】:

    仅使用Eq 确实没有任何方法可以有效地做到这一点。您需要使用一些效率低得多的方法来构建相等元素的组,并且如果不扫描整个列表,您将无法知道仅存在一个特定元素。

    另外,请注意,为避免无用的比较,您需要一种方法来检查某个元素之前是否遇到过,而唯一的方法是拥有一个已知多次出现的元素列表,检查当前元素是否在该列表中的唯一方法是……将其与每个元素进行比较。

    如果您希望它比 O(非常可怕的东西)更快地工作,您需要 Ord 约束。


    好的,根据 cmets 中的说明,这是我认为您正在寻找的一个快速而肮脏的示例:

    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。如果它到达结果列表的末尾,则返回结果列表前面的那个。

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

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

    【讨论】:

    • "唯一的方法是拥有一个已知多次出现的元素列表" - 您可以“保留”您在进行过程中遇到的元素列表(从 list of A) 中的所有元素。如果此列表在任何时候变为空,则终止;我的意思是在命令式编程中有一种方法可以做到这一点,所以我希望在 Haskell 中也有某种方法。
    • @PiotrLopusiewicz:在你施加的约束下,用命令式语言也没有好办法。
    • 我可以这样做:我从 A 中所有元素的集合 C 开始(记住 - A 很小)。我逐个元素地遍历列表元素,将它们中的每一个元素与 C 中的每个元素进行比较。如果第二次遇到某个元素,我将其从 C 中删除。如果 C 仍然不为空,我继续。如果它在任何时候变空,我就会终止。这在线性的悲观情况下需要 numelA * n 操作。
    • @PiotrLopusiewicz:您在问题中的约束不允许您拥有“A 中所有元素的列表”。如果您想允许这样做,那么您建议的算法将直接转换为 Haskell。使用特定类型与多态函数非常不同。
    • “你在问题中的约束不允许你有一个“来自 A 的所有元素的列表”——也许我的英语不够好。我的意思是“小集合 A”是已知的。我将其转换为 haskell 仍然存在问题,因为它需要在我从一个元素到另一个元素时保持一些状态,这并不容易(对我来说)。
    【解决方案3】:

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

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

    如果您的元素不是Ordered,但只能针对Equality 进行测试,那么您真的没有比这样更好的选择了:

    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
    

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

    【讨论】:

    • 感谢惰性和 Piotr 保证元素集很小,即使最坏情况更差,平均情况和最佳情况也会比 N log N 好很多,因为平均和最佳情况排序仍为 N log N 时的情况。
    • "其次,至少,您别无选择,只能遍历整个列表:在查看所有其他元素之前,您无法确定任何给定元素是否真的是唯一的。" - 这不是真的 - 在遍历过程中可能会发现不存在唯一元素。我接受你关于可能类型的观点 - 谢谢。
    • 对不起,我错过了关于小/固定集的部分。让我想一想,也许会想出一个编辑...
    • 我接受了不同的答案,但这非常优雅,实际上是我的问题中最快的(第一个问题)。
    【解决方案4】:

    正如其他人所说,没有任何额外的约束,你不能在小于二次的时间内做到这一点,因为如果不了解元素,你就无法将它们保存在某种合理的数据结构中。

    如果我们能够比较元素,一个明显的 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) .

    但是,我们可以做得更好 - 我们可以使用 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 一次,但在许多不同元素的情况下,哈希表将不得不多次调整大小。

    【讨论】:

    • 由于 OP 知道元素的域,因此完美的哈希甚至可能适用于此。这将使哈希表成为一个很好的解决方案。或者,从 luqui 那里得到提示,如果您可以依赖 Enum,Bounded,那么可变数组/向量可能会更好。
    【解决方案5】:

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

    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 中才能付出代价。

    【讨论】:

    • 没关系。该集合很小,因此它只是该集合大小的二次方,而不是输入大小。在我看来,虽然它在遍历过程中不断与所有元素进行比较,但我仍然无法想象 Haskell 代码是如何工作的。
    【解决方案6】:

    这样的东西看起来不错。

    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)
    

    但是可以使用 fold 重写 helper,如下所示,这肯定更好。

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

    【讨论】:

      猜你喜欢
      • 2023-03-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多