【问题标题】:Find duplicates in an unsorted sequence efficiently有效地查找未排序序列中的重复项
【发布时间】:2012-03-14 19:04:28
【问题描述】:

我需要一种非常有效的方法来查找未排序序列中的重复项。这是我想出来的,但它有一些缺点,即它

  1. 不必要地计算超过 2 次的出现次数
  2. 在产生重复之前消耗整个序列
  3. 创建几个中间序列

module Seq = 
  let duplicates items =
    items
    |> Seq.countBy id
    |> Seq.filter (snd >> ((<) 1))
    |> Seq.map fst

不管有什么缺点,我看不出有理由用两倍的代码来替换它。是否有可能用相对简洁的代码来改进这一点?

【问题讨论】:

标签: performance algorithm f# ienumerable


【解决方案1】:

更优雅的功能解决方案:

let duplicates xs =
  Seq.scan (fun xs x -> Set.add x xs) Set.empty xs
  |> Seq.zip xs
  |> Seq.choose (fun (x, xs) -> if Set.contains x xs then Some x else None)

使用scan 来累积到目前为止看到的所有元素的集合。然后使用zip 将每个元素与其之前的元素集组合起来。最后,使用choose 过滤掉之前见过的元素集合中的元素,即重复项。

编辑

其实我原来的答案是完全错误的。首先,您不希望输出中有重复项。其次,您需要性能。

这是一个纯函数式解决方案,可实现您所追求的算法:

let duplicates xs =
  (Map.empty, xs)
  ||> Seq.scan (fun xs x ->
      match Map.tryFind x xs with
      | None -> Map.add x false xs
      | Some false -> Map.add x true xs
      | Some true -> xs)
  |> Seq.zip xs
  |> Seq.choose (fun (x, xs) ->
      match Map.tryFind x xs with
      | Some false -> Some x
      | None | Some true -> None)

这使用一个映射来跟踪每个元素之前是否曾被看到过一次或多次,然后如果该元素之前只被看到过一次,即第一次被复制,则发出该元素。

这是一个更快的命令式版本:

let duplicates (xs: _ seq) =
  seq { let d = System.Collections.Generic.Dictionary(HashIdentity.Structural)
        let e = xs.GetEnumerator()
        while e.MoveNext() do
          let x = e.Current
          let mutable seen = false
          if d.TryGetValue(x, &seen) then
            if not seen then
              d.[x] <- true
              yield x
          else
            d.[x] <- false }

这比您的任何其他答案快 2 倍左右(在撰写本文时)。

使用for x in xs do 循环枚举序列中的元素比直接使用GetEnumerator 慢得多,但生成自己的Enumerator 并不比使用yield 的计算表达式快得多。

请注意,DictionaryTryGetValue 成员允许我通过改变堆栈分配的值来避免内部循环中的分配,而 F# 提供的 TryGetValue 扩展成员(并由 kvb 在他/她的回答中使用)分配它的返回元组。

【讨论】:

  • +1 表示聪明,但它的性能比我原来的解决方案差得多。
  • @Daniel 哎呀,我忘了它应该是有效的! :-)
  • 对命令式版本进行了非常好的微改进。顺便说一句,我很确定 Keith (kvb) 是一个“他”。 :-)
【解决方案2】:

这是一个命令式的解决方案(诚然稍长):

let duplicates items =
    seq {
        let d = System.Collections.Generic.Dictionary()
        for i in items do
            match d.TryGetValue(i) with
            | false,_    -> d.[i] <- false         // first observance
            | true,false -> d.[i] <- true; yield i // second observance
            | true,true  -> ()                     // already seen at least twice
    }

【讨论】:

  • 我觉得这很好,但觉得值得一问。
【解决方案3】:

这是我能想到的最好的“功能性”解决方案,它不会预先消耗整个序列。

let duplicates =
    Seq.scan (fun (out, yielded:Set<_>, seen:Set<_>) item -> 
        if yielded.Contains item then
            (None, yielded, seen)
        else
            if seen.Contains item then
                (Some(item), yielded.Add item, seen.Remove item)
            else
                (None, yielded, seen.Add item)
    ) (None, Set.empty, Set.empty)
    >> Seq.Choose (fun (x,_,_) -> x)

【讨论】:

  • 为什么选择 Seq.skip?您可以将 Seq.filter 和 Seq.map 组合替换为 Seq.choose
  • 不错,我忘了选择。跳过是早期代码的产物。
  • 你可以摆脱 seen.Remove - 可能会获得一点速度,然后你的解决方案会像我的一样 - 集合会相交 - 除非我的解决方案预先消耗了序列,所以我认为你的更好(因此 +1)。
【解决方案4】:

假设您的序列是有限的,此解决方案需要在序列上运行一次:

open System.Collections.Generic
let duplicates items =
   let dict = Dictionary()
   items |> Seq.fold (fun acc item -> 
                             match dict.TryGetValue item with
                             | true, 2 -> acc
                             | true, 1 -> dict.[item] <- 2; item::acc
                             | _ -> dict.[item] <- 1; acc) []
         |> List.rev

你可以提供序列的长度作为Dictionary的容量,但它需要再次枚举整个序列。

编辑: 要解决第二个问题,可以按需生成副本:

open System.Collections.Generic
let duplicates items =
   seq {
         let dict = Dictionary()
         for item in items do
            match dict.TryGetValue item with
            | true, 2 -> ()
            | true, 1 -> dict.[item] <- 2; yield item
            | _ -> dict.[item] <- 1
   }

【讨论】:

    【解决方案5】:

    功能解决方案:

    let duplicates items = 
      let test (unique, result) v =
        if not(unique |> Set.contains v) then (unique |> Set.add v ,result) 
        elif not(result |> Set.contains v) then (unique,result |> Set.add v) 
        else (unique, result)
      items |> Seq.fold test (Set.empty, Set.empty) |> snd |> Set.toSeq
    

    【讨论】:

    • [1;1;1;2;3;4;4;5] 导致它打印 1 两次。
    • 我们的算法非常相似,除了你的集合相交而我的不相交。我想知道,哪个会更快?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-02
    • 1970-01-01
    • 2021-01-30
    • 1970-01-01
    • 2011-05-10
    • 1970-01-01
    相关资源
    最近更新 更多