【问题标题】:Removing duplicates from a list in Haskell without elem从没有elem的Haskell列表中删除重复项
【发布时间】:2013-04-13 02:07:29
【问题描述】:

我正在尝试定义一个从列表中删除重复项的函数。到目前为止,我有一个可行的实现:

rmdups :: Eq a => [a] -> [a]
rmdups [] = []
rmdups (x:xs)   | x `elem` xs   = rmdups xs
                | otherwise     = x : rmdups xs

但是,我想在不使用 elem 的情况下对其进行修改。最好的方法是什么?

我想使用我自己的函数而不是 nubnubBy 来执行此操作。

【问题讨论】:

标签: list haskell recursion


【解决方案1】:

您的代码和nub 都具有O(N^2) 复杂性。

您可以将复杂度提高到O(N log N),并通过排序、分组和只取每个组的第一个元素来避免使用elem

从概念上来说,

rmdups :: (Ord a) => [a] -> [a]
rmdups = map head . group . sort

假设您从列表[1, 2, 1, 3, 2, 4] 开始。通过排序得到,[1, 1, 2, 2, 3, 4];通过分组,你得到[[1, 1], [2, 2], [3], [4]];最后,通过占据每个列表的头部,你会得到[1, 2, 3, 4]

上面的完整实现只涉及扩展每个功能。

请注意,这需要对列表元素施加更强的Ord 约束,并且还会更改它们在返回列表中的顺序。

【讨论】:

  • 非常好,但请注意,这会对列表元素设置Ord 限制,而不仅仅是Eq,并且不会保留顺序。
  • 好点。记下这一点以及语义上的其他变化。
  • ordNub 提供了@scvalex 建议使用Ord 的复制粘贴就绪、稳定(保留顺序)变体。它还包含 \`, union` 和 intersect 的类似物。
【解决方案2】:

更简单。

import Data.Set 
mkUniq :: Ord a => [a] -> [a]
mkUniq = toList . fromList

O(n) 时间内将集合转换为元素列表:

toList :: Set a -> [a]

O(n log n) 时间内从元素列表中创建一个集合:

fromList :: Ord a => [a] -> Set a

在 python 中也不例外。

def mkUniq(x): 
   return list(set(x)))

【讨论】:

  • 优雅,但我认为 Set 不会保留顺序。
  • 是的,他没有在他的 OP 中提到订单保留。
【解决方案3】:

与@scvalex 的解决方案相同,以下具有O(n * log n) 复杂性和Ord 依赖项。与它不同的是,它保留了顺序,保留了第一次出现的项目。

import qualified Data.Set as Set

rmdups :: Ord a => [a] -> [a]
rmdups = rmdups' Set.empty where
  rmdups' _ [] = []
  rmdups' a (b : c) = if Set.member b a
    then rmdups' a c
    else b : rmdups' (Set.insert b a) c

基准测试结果

如您所见,基准测试结果证明此解决方案是最有效的。 你可以找到这个基准的来源here

【讨论】:

  • +1。 Set 对于大输入来说绝对是一种更高效的数据结构。我想看看标准库的nub 在这个图中的位置——懒惰对性能有影响吗?
  • 嘿。我打算使用 Set 插入来编写并提交一个版本,但你打败了我。好节目。
【解决方案4】:

如果没有elem(或您自己重新实现它),我认为您将无法做到这一点。

但是,您的实现存在语义问题。当元素重复时,您将保留 last 元素。就个人而言,我希望它保留第一个重复项并删除其余项。

*Main> rmdups "abacd"
"bacd"

解决方案是将“已见”元素作为状态变量线程化。

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

这或多或少是nub 在标准库中的实现方式(阅读源代码here)。 nub 实现的细微差别确保它是non-strict,而上面的removeDuplicates 是严格的(它在返回之前消耗整个列表)。

如果您不担心严格性,这里的原始递归实际上是多余的。 removeDuplicates可以和foldl一行实现:

removeDuplicates2 = foldl (\seen x -> if x `elem` seen
                                      then seen
                                      else seen ++ [x]) []

【讨论】:

  • @BradStevenson 这些解决方案基于非常低效的操作——elem 函数和(++) 在列表中具有O(n) 复杂性。尽管 Haskell 的惰性保护算法不会在每个循环中执行 (++),但与其他答案中提出的替代实现相比,这些实现仍然存在很大差距。见benchmark results
  • 我认为removeDuplicates3 = foldr (\x seen -> if x `elem` seen then seen else x : seen) [] 运行速度比 removeDuplicates2 快,因为(:) 操作是恒定的。
【解决方案5】:

回答这个问题为时已晚,但我想分享我的原创解决方案,不使用elem,不要假设Ord

rmdups' :: (Eq a) => [a] -> [a]
rmdups' [] = []
rmdups' [x] = [x]
rmdups' (x:xs) = x : [ k  | k <- rmdups'(xs), k /=x ]

此解决方案在输入末尾删除重复项,而问题实现在开头删除。例如,

rmdups "maximum-minimum"
-- "ax-nium"

rmdups' "maximum-minimum"
-- ""maxiu-n"

此外,此代码复杂度为 O(N*K),其中 N 是字符串的长度,K 是字符串中唯一字符的数量。 N >= K 因此,在最坏的情况下它将是 O(N^2) 但这意味着字符串中没有重复,这与您尝试删除字符串中的重复项不同。

【讨论】:

    【解决方案6】:

    Graham Hutton 在 p 上有一个 rmdups 函数。 86 Haskell 编程。它保持秩序。如下。

    rmdups :: Eq a => [a] -> [a]
    rmdups [] = []
    rmdups (x:xs) = x : filter (/= x) (rmdups xs)
    rmdups "maximum-minimum"
    

    "maxiu-n"

    这一直困扰着我,直到我看到 Hutton 的函数。然后,我又试了一次。有两个版本,第一个保留最后一个副本,第二个保留第一个。

    rmdups ls = [d|(z,d)<- zip [0..] ls, notElem d $ take z ls]
    rmdups "maximum-minimum"
    

    "maxiu-n"

    如果您想获取列表的第一个而不是最后一个重复元素,就像您尝试做的那样,只需在函数中将 take 更改为 drop 并将枚举 zip [0..] 更改为 zip [1..] .

    【讨论】:

      【解决方案7】:

      使用recursion-schemes

      import Data.Functor.Foldable
      
      dedup :: (Eq a) => [a] -> [a]
      dedup = para pseudoalgebra
          where pseudoalgebra Nil                 = []
                pseudoalgebra (Cons x (past, xs)) = if x `elem` past then xs else x:xs
      

      虽然这肯定更高级,但我认为它非常优雅,并展示了一些有价值的函数式编程范例。

      【讨论】:

        【解决方案8】:

        您也可以使用此压缩功能。

        cmprs ::Eq a=>[a] -> [a]
        --cmprs [] = [] --not necessary
        cmprs (a:as) 
            |length as == 1 = as
            |a == (head as) = cmprs as
            |otherwise = [a]++cmprs as
        

        【讨论】:

          【解决方案9】:

          我想在@fp_mora 的回答中补充说,在 Haskell 编程第 136 页上还有另一个稍微不同的实现:

          rmdups :: Eq a => [a] -> [a]
          rmdups [] = []
          rmdups (x : xs) = x : rmdups (filter (/= x) xs)
          

          我更容易理解这个。

          【讨论】:

            【解决方案10】:

            使用 dropWhile 也可以,但记得在使用它之前对列表进行排序

            rmdups :: (Eq a) => [a] -> [a]
            rmdups [] = []
            rmdups (x:xs) = x : (rmdups $ dropWhile (\y -> y == x) xs)
            

            【讨论】:

              【解决方案11】:

              ...或者使用来自 Data.List 的函数 union 应用于自身:

              import Data.List
              
              unique x = union x x
              

              【讨论】:

              • “第一个列表的重复项和元素会从第二个列表中删除,但如果第一个列表包含重复项,结果也会如此。”。参照documentation
              • unique x = union [] x 可能会更好。
              • 它慢得要命
              【解决方案12】:
              remove_duplicates (x:xs)
                | xs == []       = [x]
                | x == head (xs) = remove_duplicates xs
                | otherwise      = x : remove_duplicates xs
              

              您可以尝试这样做。我只是用我自己的实现替换了“elem”。它对我有用。

              【讨论】:

              • 这仅适用于连续重复(它们必须彼此相邻)。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-08-29
              • 1970-01-01
              • 2021-10-02
              • 2019-09-30
              • 1970-01-01
              • 1970-01-01
              • 2022-01-20
              相关资源
              最近更新 更多